1.AI助手模版、样式修改成谢提供的对话模版(有特效)。

2.点击AI分析后,对话弹出类似商品链接引入,用户点击确认后进行分析。
3.AI分析对话新增提示词编辑,根据组件名称:模块编号进行获取提示词进行模块分析。
4.商品大盘首页字体调整,数据卡片中的字体调小一点,文字描述超长不显示,鼠标放上去显示全量。
This commit is contained in:
2026-03-20 08:47:39 +08:00
parent f3a5569f55
commit bb5ab23430
18 changed files with 449 additions and 63 deletions

View File

@@ -142,5 +142,6 @@
"package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
},
"terminal.integrated.scrollback": 10000,
"nuxt.isNuxtApp": false
"nuxt.isNuxtApp": false,
"java.compile.nullAnalysis.mode": "automatic"
}

148
AIRBT.html Normal file
View File

@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 决策通</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.ai-drawer { transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); transform: translateX(110%); }
.ai-drawer.active { transform: translateX(0); }
.ai-drawer.fullscreen { width: 85vw !important; height: 80vh !important; right: 7.5vw !important; top: 10vh !important; border-radius: 1.5rem !important; transform: translateX(0); }
.typing::after { content: '|'; animation: blink 1s infinite; margin-left: 2px; color: #3B82F6; }
@keyframes blink { 50% { opacity: 0; } }
.custom-scroll::-webkit-scrollbar { width: 5px; }
.custom-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
#overlay { display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4); backdrop-filter: blur(4px); z-index: 55; }
#overlay.active { display: block; }
#fileInput, #imageInput { display: none; }
</style>
</head>
<body class="bg-[#F1F5F9]">
<div id="overlay" onclick="closeAI()"></div>
<div class="fixed bottom-[85px] right-6 z-50" id="aiFab">
<button onclick="openAI()" class="flex items-center gap-2 bg-white border border-blue-100 px-5 py-3 rounded-full shadow-2xl hover:scale-105 transition-all group">
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white shadow-lg shadow-blue-200">
<i class="fas fa-robot text-sm"></i>
</div>
<span class="text-blue-600 font-bold text-sm tracking-wide">AI 决策助手</span>
</button>
</div>
<div id="aiPanel" class="fixed top-0 right-0 h-full w-[420px] bg-white shadow-2xl z-[60] ai-drawer flex flex-col overflow-hidden border-l border-slate-100">
<div class="px-6 py-4 border-b flex items-center justify-between bg-white shrink-0">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-600 rounded-xl flex items-center justify-center text-white shadow-md shadow-blue-100">
<i class="fas fa-robot text-lg"></i>
</div>
<div>
<h3 class="font-bold text-slate-800 text-sm">AI 决策助手</h3>
</div>
</div>
<div class="flex items-center gap-1">
<button onclick="toggleFullscreen()" class="w-9 h-9 rounded-lg hover:bg-slate-100 flex items-center justify-center text-slate-500">
<i class="fas fa-expand-alt" id="expandIcon"></i>
</button>
<button onclick="closeAI()" class="w-9 h-9 rounded-lg hover:bg-red-50 flex items-center justify-center text-slate-400 hover:text-red-500">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="chatBox" class="flex-1 overflow-y-auto p-6 space-y-6 bg-[#FBFCFE] custom-scroll">
<div class="flex gap-4 items-start">
<div class="w-8 h-8 bg-blue-50 text-blue-600 rounded-lg flex items-center justify-center shrink-0 mt-1 border border-blue-100">
<i class="fas fa-robot text-xs"></i>
</div>
<div class="bg-white border border-slate-200 p-4 rounded-2xl rounded-tl-none shadow-sm text-sm text-slate-700">
您好!请发送数据或输入指令。
</div>
</div>
</div>
<div class="p-4 bg-white border-t border-slate-100 shrink-0">
<div class="max-w-4xl mx-auto flex flex-col">
<div class="flex gap-1 mb-2">
<input type="file" id="fileInput" accept=".xls,.xlsx,.csv" onchange="handleFileUpload(event, 'excel')">
<input type="file" id="imageInput" accept="image/*" onchange="handleFileUpload(event, 'image')">
<button onclick="triggerInput('image')" class="w-8 h-8 flex items-center justify-center text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-md transition-all">
<i class="fas fa-image"></i>
</button>
<button onclick="triggerInput('excel')" class="w-8 h-8 flex items-center justify-center text-slate-400 hover:text-emerald-600 hover:bg-emerald-50 rounded-md transition-all">
<i class="fas fa-file-excel"></i>
</button>
</div>
<div class="flex items-center gap-3 bg-slate-100 rounded-2xl px-5 py-1.5 border border-transparent focus-within:bg-white focus-within:border-blue-500 transition-all">
<input id="chatInput" type="text" placeholder="输入指令..."
class="flex-1 bg-transparent py-2.5 text-sm outline-none text-slate-700 font-medium"
onkeypress="if(event.key==='Enter') sendMsg()">
<button onclick="sendMsg()" class="w-9 h-9 bg-blue-600 text-white rounded-xl flex items-center justify-center hover:bg-blue-700 shadow-lg shadow-blue-100 active:scale-95 transition-all">
<i class="fas fa-paper-plane text-xs"></i>
</button>
</div>
</div>
</div>
</div>
<script>
const panel = document.getElementById('aiPanel');
const chatBox = document.getElementById('chatBox');
const input = document.getElementById('chatInput');
const overlay = document.getElementById('overlay');
const fab = document.getElementById('aiFab');
const icon = document.getElementById('expandIcon');
function openAI() { panel.classList.add('active'); overlay.classList.add('active'); fab.style.opacity = '0'; }
function closeAI() { panel.classList.remove('active'); panel.classList.remove('fullscreen'); overlay.classList.remove('active'); fab.style.opacity = '1'; icon.className = 'fas fa-expand-alt'; }
function toggleFullscreen() { panel.classList.toggle('fullscreen'); icon.className = panel.classList.contains('fullscreen') ? 'fas fa-compress-alt' : 'fas fa-expand-alt'; }
function triggerInput(type) { document.getElementById(type === 'image' ? 'imageInput' : 'fileInput').click(); }
function handleFileUpload(event, type) {
const file = event.target.files[0];
if (!file) return;
const contentHtml = type === 'image' ? `<img src="${URL.createObjectURL(file)}" class="rounded-lg max-w-full">` : `<div class="flex items-center gap-3 bg-white/10 p-3 rounded-xl border border-white/20"><div class="w-8 h-8 bg-emerald-500 rounded flex items-center justify-center text-white shrink-0"><i class="fas fa-file-excel text-xs"></i></div><p class="text-xs font-bold truncate">${file.name}</p></div>`;
const msgHtml = `<div class="flex justify-end"><div class="bg-blue-600 text-white p-3 rounded-2xl rounded-tr-none text-sm max-w-[85%] shadow-lg">${contentHtml}</div></div>`;
chatBox.insertAdjacentHTML('beforeend', msgHtml);
chatBox.scrollTop = chatBox.scrollHeight;
setTimeout(() => {
const aiId = 'ai-' + Date.now();
const aiHtml = `<div class="flex gap-4 items-start"><div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white shrink-0 mt-1 shadow-sm"><i class="fas fa-robot text-xs"></i></div><div class="bg-white border border-slate-200 p-4 rounded-2xl rounded-tl-none shadow-sm text-sm text-slate-700 flex-1"><p id="${aiId}" class="typing leading-relaxed"></p></div></div>`;
chatBox.insertAdjacentHTML('beforeend', aiHtml);
chatBox.scrollTop = chatBox.scrollHeight;
typeWriter(`正在分析您的文件数据...`, aiId);
}, 800);
}
function sendMsg() {
const text = input.value.trim();
if(!text) return;
const userHtml = `<div class="flex justify-end"><div class="bg-blue-600 text-white p-4 rounded-2xl rounded-tr-none text-sm max-w-[80%] shadow-lg shadow-blue-100/50">${text}</div></div>`;
chatBox.insertAdjacentHTML('beforeend', userHtml);
input.value = '';
chatBox.scrollTop = chatBox.scrollHeight;
setTimeout(() => {
const aiId = 'ai-' + Date.now();
const aiHtml = `<div class="flex gap-4 items-start"><div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white shrink-0 mt-1 border border-blue-100"><i class="fas fa-robot text-xs"></i></div><div class="bg-white border border-slate-200 p-4 rounded-2xl rounded-tl-none shadow-sm text-sm text-slate-700 flex-1"><p id="${aiId}" class="typing leading-relaxed"></p></div></div>`;
chatBox.insertAdjacentHTML('beforeend', aiHtml);
chatBox.scrollTop = chatBox.scrollHeight;
typeWriter(`正在处理您的指令...`, aiId);
}, 600);
}
function typeWriter(text, elementId) {
let i = 0;
const el = document.getElementById(elementId);
function type() { if (i < text.length) { el.innerHTML += text.charAt(i); i++; chatBox.scrollTop = chatBox.scrollHeight; setTimeout(type, 30); } else { el.classList.remove('typing'); } }
type();
}
</script>
</body>
</html>

50
package-lock.json generated
View File

@@ -35,6 +35,7 @@
"element-plus": "2.11.1",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"html2canvas": "^1.4.1",
"jsencrypt": "^3.3.2",
"jsoneditor": "^10.1.3",
"lodash-es": "^4.17.21",
@@ -7813,6 +7814,15 @@
"node": ">=0.10.0"
}
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
@@ -8987,6 +8997,15 @@
"node": ">=12 || >=16"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.1.0.tgz",
@@ -12286,6 +12305,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz",
@@ -18834,6 +18866,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
@@ -19806,6 +19847,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",

View File

@@ -51,6 +51,7 @@
"element-plus": "2.11.1",
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"html2canvas": "^1.4.1",
"jsencrypt": "^3.3.2",
"jsoneditor": "^10.1.3",
"lodash-es": "^4.17.21",

39
pnpm-lock.yaml generated
View File

@@ -86,6 +86,9 @@ importers:
highlight.js:
specifier: ^11.9.0
version: 11.10.0
html2canvas:
specifier: ^1.4.1
version: 1.4.1
jsencrypt:
specifier: ^3.3.2
version: 3.3.2
@@ -2531,6 +2534,10 @@ packages:
balanced-match@2.0.0:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base@0.11.2:
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
engines: {node: '>=0.10.0'}
@@ -2842,6 +2849,9 @@ packages:
resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
engines: {node: '>=12 || >=16'}
css-line-break@2.1.0:
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
@@ -3798,6 +3808,10 @@ packages:
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
html2canvas@1.4.1:
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
engines: {node: '>=8.0.0'}
htmlparser2@3.10.1:
resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
@@ -5480,6 +5494,9 @@ packages:
resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
engines: {node: '>=8'}
text-segmentation@1.0.3:
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@@ -5706,6 +5723,9 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
@@ -8603,6 +8623,8 @@ snapshots:
balanced-match@2.0.0: {}
base64-arraybuffer@1.0.2: {}
base@0.11.2:
dependencies:
cache-base: 1.0.1
@@ -8962,6 +8984,10 @@ snapshots:
css-functions-list@3.2.3: {}
css-line-break@2.1.0:
dependencies:
utrie: 1.0.2
css-select@4.3.0:
dependencies:
boolbase: 1.0.0
@@ -10129,6 +10155,11 @@ snapshots:
html-void-elements@3.0.0: {}
html2canvas@1.4.1:
dependencies:
css-line-break: 2.1.0
text-segmentation: 1.0.3
htmlparser2@3.10.1:
dependencies:
domelementtype: 1.3.1
@@ -11880,6 +11911,10 @@ snapshots:
text-extensions@2.4.0: {}
text-segmentation@1.0.3:
dependencies:
utrie: 1.0.2
text-table@0.2.0: {}
through@2.3.8: {}
@@ -12163,6 +12198,10 @@ snapshots:
util-deprecate@1.0.2: {}
utrie@1.0.2:
dependencies:
base64-arraybuffer: 1.0.2
uuid@10.0.0: {}
vanilla-picker@2.12.3:

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup>
import { isDark } from '@/utils/is'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import routerSearch from '@/components/RouterSearch/index.vue'
import AiAssistantProvider from '@/components/AiAssistant/AiAssistantProvider.vue'
defineOptions({ name: 'APP' })
@@ -14,11 +14,11 @@ const currentSize = computed(() => appStore.getCurrentSize)
const greyMode = computed(() => appStore.getGreyMode)
const { wsCache } = useCache()
// 根据浏览器当前主题设置系统主题色
// 默认主题为白色(浅色),无缓存时使用浅色;有缓存则沿用用户选择
const setDefaultTheme = () => {
let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)
if (isDarkTheme === null) {
isDarkTheme = isDark()
isDarkTheme = false
}
appStore.setIsDark(isDarkTheme)
}
@@ -26,8 +26,10 @@ setDefaultTheme()
</script>
<template>
<ConfigGlobal :size="currentSize">
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
<routerSearch />
<AiAssistantProvider>
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
<routerSearch />
</AiAssistantProvider>
</ConfigGlobal>
</template>
<style lang="scss">

View File

@@ -13,5 +13,7 @@ const prefixCls = getPrefixCls('backtop')
<ElBacktop
:class="`${prefixCls}-backtop`"
:target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
:right="24"
:bottom="90"
/>
</template>

View File

@@ -59,6 +59,10 @@ service.interceptors.request.use(
// 设置超时时间为 24 小时86400000ms确保无论多久都等待响应
config.timeout = 24 * 60 * 60 * 1000 // 24小时 = 86400000毫秒
}
// AI 图片对话流式接口:不超时
if (config.url && config.url.includes('/ydoyun/ai-chat/stream')) {
config.timeout = 0
}
// 是否需要设置 token
let isToken = (config!.headers || {}).isToken === false

View File

@@ -35,6 +35,7 @@ provide('reload', reload)
<template>
<section
id="app-main-content"
:class="[
'p-[var(--app-content-padding)] w-full bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
{

View File

@@ -599,6 +599,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
hidden: true
},
children: [
{
path: 'image-chat',
component: () => import('@/views/ydoyun/aichat/index.vue'),
name: 'AiImageChat',
meta: {
title: 'AI 图片对话',
icon: 'ep:chat-dot-round',
noCache: false
}
},
{
path: 'image/square',
component: () => import('@/views/ai/image/square/index.vue'),

View File

@@ -7,7 +7,9 @@
<div class="kpi-value">{{ kpi.value }}</div>
<div class="kpi-footer">
<span class="kpi-trend" :class="kpi.trendClass">{{ kpi.trendText }}</span>
<span class="kpi-desc">{{ kpi.desc }}</span>
<el-tooltip :content="kpi.desc" placement="top" :show-after="300" class="kpi-desc-tooltip">
<span class="kpi-desc">{{ kpi.desc }}</span>
</el-tooltip>
</div>
<!-- 迷你柱状图 -->
<div v-if="kpi.miniBars" class="mini-bars">
@@ -77,7 +79,7 @@ defineProps<{
}
.kpi-value {
font-size: 32px;
font-size: 24px;
font-weight: 700;
color: #1e293b;
line-height: 1.2;
@@ -90,8 +92,10 @@ defineProps<{
gap: 8px;
font-size: 12px;
margin-bottom: 8px;
min-width: 0;
.kpi-trend {
flex-shrink: 0;
font-weight: 500;
&.trend-up {
@@ -103,7 +107,16 @@ defineProps<{
}
}
.kpi-desc-tooltip {
flex: 1;
min-width: 0;
overflow: hidden;
}
.kpi-desc {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #9ca3af;
}
}

View File

@@ -162,11 +162,12 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import dayjs from 'dayjs'
import { ReportApi } from '@/api/ydoyun/report/reportpage'
import { ElMessage } from 'element-plus'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
const REPORT_ID = 6
@@ -190,7 +191,10 @@ interface MiddleClassItem {
defineOptions({ name: 'MiddleClassRanking' })
const route = useRoute()
const { setPageLoading } = useAiAssistant()
const loading = ref(false)
watch(loading, (v) => setPageLoading(v), { immediate: true })
onUnmounted(() => setPageLoading(false))
const dateRange = ref<[string, string] | null>([
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
dayjs().format('YYYY-MM-DD')

View File

@@ -520,13 +520,14 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import dayjs from 'dayjs'
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
import { ReportApi } from '@/api/ydoyun/report/reportpage'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
const REPORT_ID = 6
@@ -585,7 +586,10 @@ defineOptions({ name: 'ProductCardsPage' })
const route = useRoute()
const router = useRouter()
const { setPageLoading } = useAiAssistant()
const loading = ref(false)
watch(loading, (v) => setPageLoading(v), { immediate: true })
onUnmounted(() => setPageLoading(false))
const userStore = useUserStore()
const username = computed(() => userStore.user?.username || '')

View File

@@ -212,6 +212,11 @@
</el-radio-button>
</el-radio-group>
</div>
<div class="view-toolbar-right">
<el-button link type="primary" size="small" @click="openPromptEdit">
<Icon icon="ep:edit" /> 编辑提示词
</el-button>
</div>
</div>
<!-- 标签分类筛选与主页供应商表现一致 -->
@@ -294,8 +299,15 @@
<!-- 底部 -->
<div class="card-footer">
<span class="footer-label">&nbsp;</span>
<span class="footer-link">详情 </span>
<el-button
type="primary"
size="default"
class="ai-analysis-btn"
@click="onCardAiAnalysis($event)"
>
<el-icon class="btn-icon"><ChatDotRound /></el-icon>
AI分析
</el-button>
</div>
</div>
</div>
@@ -360,17 +372,29 @@
</el-table>
</div>
</el-card>
<AiPromptEditDialog
v-model="promptEditVisible"
:module-key="SUPPLIER_PERFORMANCE_MODULE_KEY"
module-name="供应商表现"
:initial-prompt="promptMap[SUPPLIER_PERFORMANCE_MODULE_KEY]"
@saved="loadPromptMap"
/>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { Grid, List } from '@element-plus/icons-vue'
import { Grid, List, ChatDotRound } from '@element-plus/icons-vue'
import { Icon } from '@/components/Icon'
import { ReportApi } from '@/api/ydoyun/report/reportpage'
import { useUserStore } from '@/store/modules/user'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
import { AiModulePromptApi } from '@/api/ydoyun/aiModulePrompt'
import AiPromptEditDialog from './AiPromptEditDialog.vue'
interface ColumnCfg {
title: string
@@ -380,10 +404,41 @@ interface ColumnCfg {
colorKey?: string
}
const SUPPLIER_PERFORMANCE_COMPONENT = 'SupplierPerformance'
const SUPPLIER_PERFORMANCE_MODULE_KEY = `${SUPPLIER_PERFORMANCE_COMPONENT}:main`
const REPORT_ID = 6
const route = useRoute()
const userStore = useUserStore()
const { openWithScreenshot, setPageLoading } = useAiAssistant()
const loading = ref(false)
watch(loading, (v) => setPageLoading(v), { immediate: true })
onUnmounted(() => setPageLoading(false))
const promptMap = ref<Record<string, string>>({})
const promptEditVisible = ref(false)
const openPromptEdit = () => {
promptEditVisible.value = true
}
const loadPromptMap = async () => {
try {
const map = await AiModulePromptApi.getPromptMapByComponent(SUPPLIER_PERFORMANCE_COMPONENT)
promptMap.value = map || {}
} catch {
promptMap.value = {}
}
}
/** 卡片 AI 分析:截图当前卡片并打开 AI 对话框,代入模块名+提示词 */
function onCardAiAnalysis(e: MouseEvent) {
const card = (e.currentTarget as HTMLElement).closest('.supplier-card')
if (card instanceof HTMLElement) {
const prompt = promptMap.value[SUPPLIER_PERFORMANCE_MODULE_KEY]
openWithScreenshot(card, { moduleName: '供应商表现', prompt })
}
}
/** 展示形式:卡片 | 列表 */
const displayMode = ref<'card' | 'table'>('card')
@@ -906,6 +961,7 @@ const handleReset = () => {
}
onMounted(async () => {
await loadPromptMap()
// 等待下一个 tick确保路由参数完全加载
await nextTick()
@@ -952,6 +1008,10 @@ onMounted(async () => {
gap: 16px;
margin-bottom: 16px;
.view-toolbar-right {
margin-left: auto;
}
.view-switch {
display: flex;
align-items: center;
@@ -1469,36 +1529,38 @@ onMounted(async () => {
/* 底部 */
.card-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 2px solid var(--el-border-color-lighter);
margin-top: 20px;
padding: 18px 8px 0;
border-top: 1px solid var(--el-border-color-lighter);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
justify-content: flex-end;
min-height: 52px;
}
.footer-label {
font-size: 10px;
color: var(--el-text-color-placeholder);
font-weight: 900;
text-transform: uppercase;
letter-spacing: 2px;
font-style: italic;
.ai-analysis-btn {
font-size: 14px !important;
font-weight: 600 !important;
letter-spacing: 0.5px;
padding: 10px 24px !important;
border-radius: 12px !important;
border: none !important;
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%) !important;
color: #fff !important;
box-shadow: 0 2px 10px rgba(64, 158, 255, 0.22);
transition: all 0.25s ease;
.btn-icon {
font-size: 16px;
margin-right: 8px;
vertical-align: -0.12em;
}
}
.footer-link {
font-size: 10px;
color: #3b82f6;
font-weight: 900;
text-transform: uppercase;
cursor: pointer;
transition: all 0.2s;
}
.footer-link:hover {
text-decoration: underline;
color: #2563eb;
.ai-analysis-btn:hover {
background: linear-gradient(135deg, var(--el-color-primary-light-3) 0%, var(--el-color-primary) 100%) !important;
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.32);
}
@media (max-width: 1200px) {

View File

@@ -168,11 +168,12 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import dayjs from 'dayjs'
import { ReportApi } from '@/api/ydoyun/report/reportpage'
import { ElMessage } from 'element-plus'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
const REPORT_ID = 6
@@ -195,7 +196,10 @@ interface SupplierContributionData {
defineOptions({ name: 'SupplierRanking' })
const route = useRoute()
const { setPageLoading } = useAiAssistant()
const loading = ref(false)
watch(loading, (v) => setPageLoading(v), { immediate: true })
onUnmounted(() => setPageLoading(false))
const dateRange = ref<[string, string] | null>([
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
dayjs().format('YYYY-MM-DD')

View File

@@ -371,13 +371,14 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { useRoute } from 'vue-router'
import dayjs from 'dayjs'
import { Search, Grid, List } from '@element-plus/icons-vue'
import { ReportApi } from '@/api/ydoyun/report/reportpage'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
defineOptions({ name: 'CategoryCardListComponents' })
@@ -472,7 +473,10 @@ const getTimeRange = (range: string) => {
return { rq: start.format('YYYY-MM-DD'), rq2: end.format('YYYY-MM-DD') }
}
const { setPageLoading } = useAiAssistant()
const loading = ref(false)
watch(loading, (v) => setPageLoading(v), { immediate: true })
onUnmounted(() => setPageLoading(false))
/** 展示形式:卡片 | 列表 */
const displayMode = ref<'card' | 'table'>('card')
const columns = ref<{ title: string; key: string; order?: number; labelKey?: string; colorKey?: string }[]>([])

View File

@@ -434,13 +434,14 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
import { ReportApi } from '@/api/ydoyun/report/reportpage'
import { useUserStore } from '@/store/modules/user'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
import type { ProductDetailData, ProductSizeStatus, UiStatus } from './index.vue'
defineOptions({ name: 'ProductDetailPage' })
@@ -449,7 +450,10 @@ const REPORT_ID = 6
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const { setPageLoading } = useAiAssistant()
const loading = ref(false)
watch(loading, (v) => setPageLoading(v), { immediate: true })
onUnmounted(() => setPageLoading(false))
const productList = ref<ProductDetailData[]>([])
/** 标签分类筛选 */
const labelFilter = ref<string>('all')

View File

@@ -196,9 +196,13 @@
</div>
</el-card>
<!-- KPI 卡片区域加载中显示特效 -->
<div
class="kpi-grid"
<!-- KPI 卡片区域 -->
<div class="kpi-section">
<div class="module-header">
<span class="module-title">KPI 指标</span>
</div>
<div
class="kpi-grid"
v-loading="loadingKpi"
element-loading-text="加载中..."
element-loading-custom-class="kpi-loading"
@@ -211,7 +215,9 @@
<div v-if="kpi.value != null && kpi.value !== ''" class="kpi-value">{{ kpi.value }}</div>
<div v-if="(formatTrendText(kpi) || (kpi.descs != null && kpi.descs !== ''))" class="kpi-footer">
<span v-if="formatTrendText(kpi)" class="kpi-trend" :class="kpi.trend">{{ formatTrendText(kpi) }}</span>
<span v-if="kpi.descs != null && kpi.descs !== ''" class="kpi-desc">{{ kpi.descs }}</span>
<el-tooltip v-if="kpi.descs != null && kpi.descs !== ''" :content="kpi.descs" placement="top" :show-after="300" class="kpi-desc-tooltip">
<span class="kpi-desc">{{ kpi.descs }}</span>
</el-tooltip>
</div>
<!-- 迷你柱状图 -->
<div v-if="kpi.miniBars" class="mini-bars">
@@ -237,11 +243,12 @@
/>
</div>
</el-card>
</div>
</div>
<!-- 供应商表现 (Top 10) + 中类销售排名 Top 5 -->
<div class="middle-section">
<!-- 供应商表现 (Top 10)KBStyle 筛选 + 紧凑表格 + 展开更多 -->
<!-- 供应商表现 (Top 10) -->
<el-card
class="kb-cat-card"
shadow="never"
@@ -253,9 +260,11 @@
<el-icon class="kb-title-icon"><StarFilled /></el-icon>
<h3 class="kb-card-title">供应商表现 (Top 10)</h3>
</div>
<el-button type="primary" link size="small" @click="handleViewMoreCategories" class="more-button">
更多 <el-icon><ArrowRight /></el-icon>
</el-button>
<div class="header-actions">
<el-button type="primary" link size="small" @click="handleViewMoreCategories" class="more-button">
更多 <el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</div>
<div class="kb-card-content">
<div class="filter-bar">
@@ -489,16 +498,18 @@
<el-icon class="category-title-icon"><StarFilled /></el-icon>
<h3 class="category-card-title">中类销售排名 Top 5</h3>
</div>
<el-button
type="primary"
link
size="small"
@click="handleViewMoreMiddleClass"
class="more-button"
>
更多
<el-icon><ArrowRight /></el-icon>
</el-button>
<div class="header-actions">
<el-button
type="primary"
link
size="small"
@click="handleViewMoreMiddleClass"
class="more-button"
>
更多
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
</div>
<div class="category-pie-wrap">
<Echart
@@ -795,11 +806,11 @@
</el-table-column>
</el-table>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onBeforeUnmount, onDeactivated, nextTick } from 'vue'
import { ref, reactive, computed, watch, onMounted, onUnmounted, onBeforeUnmount, onDeactivated, nextTick } from 'vue'
import dayjs from 'dayjs'
import type { EChartsOption } from 'echarts'
import Echart from '@/components/Echart/src/Echart.vue'
@@ -817,6 +828,7 @@ import { ElMessage } from 'element-plus'
import { Icon } from '@/components/Icon'
import { useRouter } from 'vue-router'
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
defineOptions({ name: 'ProductDashboard' })
@@ -1233,6 +1245,15 @@ const loadingCategory = ref(false)
const loadingPie = ref(false)
/** 商品明细列表加载状态 */
const loadingProductList = ref(false)
const { setPageLoading } = useAiAssistant()
watch(
[loadingKpi, loadingCategory, loadingPie, loadingProductList],
([a, b, c, d]) => setPageLoading(!!(a || b || c || d)),
{ immediate: true }
)
onUnmounted(() => setPageLoading(false))
const searchKeyword = ref('')
const hoverDetail = ref<ProductDetailData | null>(null)
@@ -2861,7 +2882,7 @@ onDeactivated(() => {
}
.kpi-value {
font-size: 32px;
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
line-height: 1.2;
@@ -2874,8 +2895,10 @@ onDeactivated(() => {
gap: 8px;
font-size: 12px;
margin-bottom: 8px;
min-width: 0;
.kpi-trend {
flex-shrink: 0;
font-weight: 500;
&.up {
@@ -2891,7 +2914,16 @@ onDeactivated(() => {
}
}
.kpi-desc-tooltip {
flex: 1;
min-width: 0;
overflow: hidden;
}
.kpi-desc {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--el-text-color-placeholder);
}
}
@@ -4197,4 +4229,5 @@ onDeactivated(() => {
.kb22-legend-item { display: flex; align-items: center; gap: 4px; font-size: 9px; color: var(--el-text-color-placeholder); }
.kb22-dot { width: 6px; height: 6px; border-radius: 50%; }
}
</style>