Compare commits
5 Commits
13fe5d9124
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e762188322 | |||
| bb5ab23430 | |||
| f3a5569f55 | |||
| fccdcaf89a | |||
| 39246c9483 |
2
.env
2
.env
@@ -1,5 +1,5 @@
|
|||||||
# 标题
|
# 标题
|
||||||
VITE_APP_TITLE=衣朵云管理系统
|
VITE_APP_TITLE=Ai 决策通
|
||||||
|
|
||||||
# 项目本地运行端口号
|
# 项目本地运行端口号
|
||||||
VITE_PORT=80
|
VITE_PORT=80
|
||||||
|
|||||||
4
.env.dev
4
.env.dev
@@ -4,8 +4,8 @@ NODE_ENV=production
|
|||||||
VITE_DEV=true
|
VITE_DEV=true
|
||||||
|
|
||||||
# 请求路径
|
# 请求路径
|
||||||
VITE_BASE_URL='http://localhost:48080'
|
#VITE_BASE_URL='http://localhost:48080'
|
||||||
#VITE_BASE_URL='http://118.253.178.8:48080'
|
VITE_BASE_URL='http://118.253.178.8:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ NODE_ENV=development
|
|||||||
VITE_DEV=true
|
VITE_DEV=true
|
||||||
|
|
||||||
# 请求路径
|
# 请求路径
|
||||||
#VITE_BASE_URL='http://118.253.178.8:48080'
|
VITE_BASE_URL='http://118.253.178.8:48080'
|
||||||
VITE_BASE_URL='http://localhost:48080'
|
#VITE_BASE_URL='http://localhost:48080'
|
||||||
|
|
||||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
|
||||||
VITE_UPLOAD_TYPE=server
|
VITE_UPLOAD_TYPE=server
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -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"
|
"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,
|
"terminal.integrated.scrollback": 10000,
|
||||||
"nuxt.isNuxtApp": false
|
"nuxt.isNuxtApp": false,
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic"
|
||||||
}
|
}
|
||||||
|
|||||||
148
AIRBT.html
Normal file
148
AIRBT.html
Normal 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>
|
||||||
@@ -7,11 +7,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="衣朵云管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
content="Ai 决策通 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="衣朵云管理系统 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
content="Ai 决策通 基于 vue3 + CompositionAPI + typescript + vite3 + element plus 的后台开源免费管理系统!"
|
||||||
/>
|
/>
|
||||||
<title>%VITE_APP_TITLE%</title>
|
<title>%VITE_APP_TITLE%</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
"element-plus": "2.11.1",
|
"element-plus": "2.11.1",
|
||||||
"fast-xml-parser": "^4.3.2",
|
"fast-xml-parser": "^4.3.2",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"jsoneditor": "^10.1.3",
|
"jsoneditor": "^10.1.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
@@ -7813,6 +7814,15 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -8987,6 +8997,15 @@
|
|||||||
"node": ">=12 || >=16"
|
"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": {
|
"node_modules/css-select": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.1.0.tgz",
|
||||||
@@ -12286,6 +12305,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
@@ -18834,6 +18866,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -19806,6 +19847,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
"element-plus": "2.11.1",
|
"element-plus": "2.11.1",
|
||||||
"fast-xml-parser": "^4.3.2",
|
"fast-xml-parser": "^4.3.2",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"jsoneditor": "^10.1.3",
|
"jsoneditor": "^10.1.3",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
|||||||
39
pnpm-lock.yaml
generated
39
pnpm-lock.yaml
generated
@@ -86,6 +86,9 @@ importers:
|
|||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.9.0
|
specifier: ^11.9.0
|
||||||
version: 11.10.0
|
version: 11.10.0
|
||||||
|
html2canvas:
|
||||||
|
specifier: ^1.4.1
|
||||||
|
version: 1.4.1
|
||||||
jsencrypt:
|
jsencrypt:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@@ -2531,6 +2534,10 @@ packages:
|
|||||||
balanced-match@2.0.0:
|
balanced-match@2.0.0:
|
||||||
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
|
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:
|
base@0.11.2:
|
||||||
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
|
resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2842,6 +2849,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
|
resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
|
||||||
engines: {node: '>=12 || >=16'}
|
engines: {node: '>=12 || >=16'}
|
||||||
|
|
||||||
|
css-line-break@2.1.0:
|
||||||
|
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
||||||
|
|
||||||
css-select@4.3.0:
|
css-select@4.3.0:
|
||||||
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
|
||||||
|
|
||||||
@@ -3798,6 +3808,10 @@ packages:
|
|||||||
html-void-elements@3.0.0:
|
html-void-elements@3.0.0:
|
||||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||||
|
|
||||||
|
html2canvas@1.4.1:
|
||||||
|
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
htmlparser2@3.10.1:
|
htmlparser2@3.10.1:
|
||||||
resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
|
resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==}
|
||||||
|
|
||||||
@@ -5480,6 +5494,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
|
resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
text-segmentation@1.0.3:
|
||||||
|
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
||||||
|
|
||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
@@ -5706,6 +5723,9 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
utrie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||||
|
|
||||||
uuid@10.0.0:
|
uuid@10.0.0:
|
||||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -8603,6 +8623,8 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@2.0.0: {}
|
balanced-match@2.0.0: {}
|
||||||
|
|
||||||
|
base64-arraybuffer@1.0.2: {}
|
||||||
|
|
||||||
base@0.11.2:
|
base@0.11.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cache-base: 1.0.1
|
cache-base: 1.0.1
|
||||||
@@ -8962,6 +8984,10 @@ snapshots:
|
|||||||
|
|
||||||
css-functions-list@3.2.3: {}
|
css-functions-list@3.2.3: {}
|
||||||
|
|
||||||
|
css-line-break@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
utrie: 1.0.2
|
||||||
|
|
||||||
css-select@4.3.0:
|
css-select@4.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
@@ -10129,6 +10155,11 @@ snapshots:
|
|||||||
|
|
||||||
html-void-elements@3.0.0: {}
|
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:
|
htmlparser2@3.10.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 1.3.1
|
domelementtype: 1.3.1
|
||||||
@@ -11880,6 +11911,10 @@ snapshots:
|
|||||||
|
|
||||||
text-extensions@2.4.0: {}
|
text-extensions@2.4.0: {}
|
||||||
|
|
||||||
|
text-segmentation@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
utrie: 1.0.2
|
||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
through@2.3.8: {}
|
through@2.3.8: {}
|
||||||
@@ -12163,6 +12198,10 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
utrie@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
base64-arraybuffer: 1.0.2
|
||||||
|
|
||||||
uuid@10.0.0: {}
|
uuid@10.0.0: {}
|
||||||
|
|
||||||
vanilla-picker@2.12.3:
|
vanilla-picker@2.12.3:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { isDark } from '@/utils/is'
|
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||||
import routerSearch from '@/components/RouterSearch/index.vue'
|
import routerSearch from '@/components/RouterSearch/index.vue'
|
||||||
|
import AiAssistantProvider from '@/components/AiAssistant/AiAssistantProvider.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'APP' })
|
defineOptions({ name: 'APP' })
|
||||||
|
|
||||||
@@ -14,11 +14,11 @@ const currentSize = computed(() => appStore.getCurrentSize)
|
|||||||
const greyMode = computed(() => appStore.getGreyMode)
|
const greyMode = computed(() => appStore.getGreyMode)
|
||||||
const { wsCache } = useCache()
|
const { wsCache } = useCache()
|
||||||
|
|
||||||
// 根据浏览器当前主题设置系统主题色
|
// 默认主题为白色(浅色),无缓存时使用浅色;有缓存则沿用用户选择
|
||||||
const setDefaultTheme = () => {
|
const setDefaultTheme = () => {
|
||||||
let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)
|
let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)
|
||||||
if (isDarkTheme === null) {
|
if (isDarkTheme === null) {
|
||||||
isDarkTheme = isDark()
|
isDarkTheme = false
|
||||||
}
|
}
|
||||||
appStore.setIsDark(isDarkTheme)
|
appStore.setIsDark(isDarkTheme)
|
||||||
}
|
}
|
||||||
@@ -26,8 +26,10 @@ setDefaultTheme()
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ConfigGlobal :size="currentSize">
|
<ConfigGlobal :size="currentSize">
|
||||||
|
<AiAssistantProvider>
|
||||||
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
|
<RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
|
||||||
<routerSearch />
|
<routerSearch />
|
||||||
|
</AiAssistantProvider>
|
||||||
</ConfigGlobal>
|
</ConfigGlobal>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface TenantVO {
|
|||||||
accountCount: number
|
accountCount: number
|
||||||
websites: string[]
|
websites: string[]
|
||||||
createTime: Date
|
createTime: Date
|
||||||
|
tenantPrompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TenantPageReqVO extends PageParam {
|
export interface TenantPageReqVO extends PageParam {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import request from '@/config/axios'
|
import request from '@/config/axios'
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
/** 自定义标签类型:product-产品, store-店铺, supplier-供货商 */
|
/** 自定义标签类型:product-产品, store-店铺, supplier-供货商, member-会员 */
|
||||||
export type CustomTagType = 'product' | 'store' | 'supplier'
|
export type CustomTagType = 'product' | 'store' | 'supplier' | 'member'
|
||||||
|
|
||||||
/** 自定义标签信息 */
|
/** 自定义标签信息 */
|
||||||
export interface CustomTag {
|
export interface CustomTag {
|
||||||
|
|||||||
111
src/api/ydoyun/tagconfig/index.ts
Normal file
111
src/api/ydoyun/tagconfig/index.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
|
||||||
|
/** 标签同步配置 */
|
||||||
|
export interface TagConfig {
|
||||||
|
id?: number
|
||||||
|
databaseId?: number
|
||||||
|
cronExpression?: string
|
||||||
|
/** 是否开启自动同步 */
|
||||||
|
autoSync?: boolean
|
||||||
|
lastSyncTime?: string
|
||||||
|
nextSyncTime?: string
|
||||||
|
/** 剔除大类(多选,逗号分隔) */
|
||||||
|
productCategoryIds?: string
|
||||||
|
/** 剔除仓库(多选,逗号分隔) */
|
||||||
|
syncWarehouseIds?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标准标签 */
|
||||||
|
export interface Tag {
|
||||||
|
id?: number
|
||||||
|
name?: string
|
||||||
|
type?: string
|
||||||
|
expression?: string
|
||||||
|
color?: string
|
||||||
|
sqlScript?: string
|
||||||
|
/** 已拼接参数的SQL(执行时直接使用,不展示) */
|
||||||
|
sqlScriptResolved?: string
|
||||||
|
useParams?: boolean
|
||||||
|
params?: string
|
||||||
|
/** 参数值 JSON,如 {"参数1":"28","参数2":"90"} */
|
||||||
|
paramValues?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标签同步历史 */
|
||||||
|
export interface TagSyncHistory {
|
||||||
|
id?: number
|
||||||
|
tagConfigId?: number
|
||||||
|
syncType?: string
|
||||||
|
syncTime?: string
|
||||||
|
syncStatus?: string
|
||||||
|
recordCount?: number
|
||||||
|
errorMessage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统一保存请求(配置+标签) */
|
||||||
|
export interface TagConfigSaveAllReq {
|
||||||
|
config?: TagConfig | null
|
||||||
|
tags?: Tag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagConfigApi = {
|
||||||
|
getTagConfigPage: (params: any) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/page', params }),
|
||||||
|
getTagConfig: (id: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/get?id=' + id }),
|
||||||
|
getTagConfigByDatabaseId: (databaseId: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/get-by-database?databaseId=' + databaseId }),
|
||||||
|
getTagConfigByTenant: () =>
|
||||||
|
request.get({ url: '/ydoyun/tag-config/get-by-tenant' }),
|
||||||
|
saveAll: (data: TagConfigSaveAllReq) =>
|
||||||
|
request.post({ url: '/ydoyun/tag-config/save', data }),
|
||||||
|
createTagConfig: (data: TagConfig) =>
|
||||||
|
request.post({ url: '/ydoyun/tag-config/create', data }),
|
||||||
|
updateTagConfig: (data: TagConfig) =>
|
||||||
|
request.put({ url: '/ydoyun/tag-config/update', data }),
|
||||||
|
deleteTagConfig: (id: number) =>
|
||||||
|
request.delete({ url: '/ydoyun/tag-config/delete?id=' + id }),
|
||||||
|
manualSync: (tagConfigId: number) =>
|
||||||
|
request.post({ url: '/ydoyun/tag-config/manual-sync?tagConfigId=' + tagConfigId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagApi = {
|
||||||
|
getTagPage: (params: any) =>
|
||||||
|
request.get({ url: '/ydoyun/tag/page', params }),
|
||||||
|
getTag: (id: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag/get?id=' + id }),
|
||||||
|
getTagListByType: (type?: string) =>
|
||||||
|
request.get({ url: '/ydoyun/tag/list-by-type', params: { type } }),
|
||||||
|
createTag: (data: Tag) =>
|
||||||
|
request.post({ url: '/ydoyun/tag/create', data }),
|
||||||
|
updateTag: (data: Tag) =>
|
||||||
|
request.put({ url: '/ydoyun/tag/update', data }),
|
||||||
|
deleteTag: (id: number) =>
|
||||||
|
request.delete({ url: '/ydoyun/tag/delete?id=' + id }),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagSyncHistoryApi = {
|
||||||
|
getTagSyncHistoryPage: (params: any) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-sync-history/page', params }),
|
||||||
|
getTagSyncHistory: (id: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-sync-history/get?id=' + id }),
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标签同步详情 */
|
||||||
|
export interface TagSyncDetail {
|
||||||
|
id?: number
|
||||||
|
syncHistoryId?: number
|
||||||
|
tagId?: number
|
||||||
|
tagName?: string
|
||||||
|
tagType?: string
|
||||||
|
sqlExecuted?: string
|
||||||
|
execStatus?: string
|
||||||
|
recordCount?: number
|
||||||
|
errorMessage?: string
|
||||||
|
execOrder?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagSyncDetailApi = {
|
||||||
|
getDetailListBySyncHistoryId: (syncHistoryId: number) =>
|
||||||
|
request.get({ url: '/ydoyun/tag-sync-detail/list-by-sync-history-id?syncHistoryId=' + syncHistoryId }),
|
||||||
|
}
|
||||||
@@ -13,5 +13,7 @@ const prefixCls = getPrefixCls('backtop')
|
|||||||
<ElBacktop
|
<ElBacktop
|
||||||
:class="`${prefixCls}-backtop`"
|
:class="`${prefixCls}-backtop`"
|
||||||
:target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
|
:target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
|
||||||
|
:right="24"
|
||||||
|
:bottom="90"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ service.interceptors.request.use(
|
|||||||
// 设置超时时间为 24 小时(86400000ms),确保无论多久都等待响应
|
// 设置超时时间为 24 小时(86400000ms),确保无论多久都等待响应
|
||||||
config.timeout = 24 * 60 * 60 * 1000 // 24小时 = 86400000毫秒
|
config.timeout = 24 * 60 * 60 * 1000 // 24小时 = 86400000毫秒
|
||||||
}
|
}
|
||||||
|
// AI 图片对话流式接口:不超时
|
||||||
|
if (config.url && config.url.includes('/ydoyun/ai-chat/stream')) {
|
||||||
|
config.timeout = 0
|
||||||
|
}
|
||||||
|
|
||||||
// 是否需要设置 token
|
// 是否需要设置 token
|
||||||
let isToken = (config!.headers || {}).isToken === false
|
let isToken = (config!.headers || {}).isToken === false
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ provide('reload', reload)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
|
id="app-main-content"
|
||||||
:class="[
|
:class="[
|
||||||
'p-[var(--app-content-padding)] w-full bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
|
'p-[var(--app-content-padding)] w-full bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,10 +66,13 @@ watch(
|
|||||||
]"
|
]"
|
||||||
to="/"
|
to="/"
|
||||||
>
|
>
|
||||||
<img
|
<svg
|
||||||
class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
|
class="logo-svg h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
|
||||||
src="@/assets/imgs/logo.png"
|
viewBox="0 0 100 100"
|
||||||
/>
|
>
|
||||||
|
<path d="M50 2L2 25.5v49L50 98l48-23.5v-49L50 2zm35.2 65.7L50 88.3 14.8 67.7V32.3L50 11.7l35.2 20.6v35.4z" />
|
||||||
|
<path d="M38.1 48.9h23.8L50 28.3 38.1 48.9zM50 71.7l11.9-20.6H38.1L50 71.7z" />
|
||||||
|
</svg>
|
||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -86,3 +89,10 @@ watch(
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logo-svg {
|
||||||
|
fill: var(--logo-title-text-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default {
|
|||||||
small: '小'
|
small: '小'
|
||||||
},
|
},
|
||||||
login: {
|
login: {
|
||||||
welcome: '衣朵云管理系统',
|
welcome: 'Ai 决策通',
|
||||||
message: '开箱即用的管理系统',
|
message: '开箱即用的管理系统',
|
||||||
tenantname: '租户名称',
|
tenantname: '租户名称',
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
BarChart,
|
BarChart,
|
||||||
FunnelChart,
|
FunnelChart,
|
||||||
GaugeChart,
|
GaugeChart,
|
||||||
|
HeatmapChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
MapChart,
|
MapChart,
|
||||||
PictorialBarChart,
|
PictorialBarChart,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AriaComponent,
|
AriaComponent,
|
||||||
|
CalendarComponent,
|
||||||
DataZoomComponent,
|
DataZoomComponent,
|
||||||
GridComponent,
|
GridComponent,
|
||||||
LegendComponent,
|
LegendComponent,
|
||||||
@@ -37,6 +39,7 @@ echarts.use([
|
|||||||
AriaComponent,
|
AriaComponent,
|
||||||
ParallelComponent,
|
ParallelComponent,
|
||||||
VisualMapComponent,
|
VisualMapComponent,
|
||||||
|
CalendarComponent,
|
||||||
BarChart,
|
BarChart,
|
||||||
LineChart,
|
LineChart,
|
||||||
PieChart,
|
PieChart,
|
||||||
@@ -45,7 +48,8 @@ echarts.use([
|
|||||||
PictorialBarChart,
|
PictorialBarChart,
|
||||||
RadarChart,
|
RadarChart,
|
||||||
GaugeChart,
|
GaugeChart,
|
||||||
FunnelChart
|
FunnelChart,
|
||||||
|
HeatmapChart
|
||||||
])
|
])
|
||||||
|
|
||||||
export default echarts
|
export default echarts
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ const router = createRouter({
|
|||||||
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL带#,createWebHistory URL不带#
|
||||||
strict: true,
|
strict: true,
|
||||||
routes: remainingRouter as RouteRecordRaw[],
|
routes: remainingRouter as RouteRecordRaw[],
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 })
|
scrollBehavior: () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve({ left: 0, top: 0 }), 0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
export const resetRouter = (): void => {
|
export const resetRouter = (): void => {
|
||||||
|
|||||||
@@ -599,6 +599,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
children: [
|
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',
|
path: 'image/square',
|
||||||
component: () => import('@/views/ai/image/square/index.vue'),
|
component: () => import('@/views/ai/image/square/index.vue'),
|
||||||
@@ -782,7 +792,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
name: 'SupplierPerformance',
|
name: 'SupplierPerformance',
|
||||||
meta: {
|
meta: {
|
||||||
title: '供应商详情',
|
title: '供应商详情',
|
||||||
noCache: true,
|
|
||||||
hidden: true,
|
hidden: true,
|
||||||
canTo: true
|
canTo: true
|
||||||
},
|
},
|
||||||
@@ -831,6 +840,16 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: 'custom-tag',
|
||||||
|
name: 'CustomTag',
|
||||||
|
meta: {
|
||||||
|
title: '定制标签',
|
||||||
|
noCache: true,
|
||||||
|
canTo: true
|
||||||
|
},
|
||||||
|
component: () => import(/* webpackChunkName: "custom-tag" */ '@/views/ydoyun/customtag/index.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'product-custom-tag',
|
path: 'product-custom-tag',
|
||||||
name: 'ProductCustomTag',
|
name: 'ProductCustomTag',
|
||||||
|
|||||||
@@ -1,111 +1,397 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="login-page">
|
||||||
:class="prefixCls"
|
<!-- 动态背景 -->
|
||||||
class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px"
|
<div class="bg-container">
|
||||||
>
|
<div class="blob blob-1"></div>
|
||||||
<div class="relative mx-auto h-full flex">
|
<div class="blob blob-2"></div>
|
||||||
<div
|
<div class="grid-base"></div>
|
||||||
:class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
|
<div class="flow-line h-line" style="top: 20%; left: -150px; animation-delay: 0s"></div>
|
||||||
>
|
<div class="flow-line h-line" style="top: 60%; left: -150px; animation-delay: 3s"></div>
|
||||||
<!-- 左上角的 logo + 系统标题 -->
|
<div class="flow-line v-line" style="left: 30%; top: -150px; animation-delay: 1.5s"></div>
|
||||||
<div class="relative flex items-center text-white">
|
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
|
||||||
<span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 左边的背景图 + 欢迎语 -->
|
|
||||||
<div class="h-[calc(100%-60px)] flex items-center justify-center">
|
<!-- 右上角主题、语言 -->
|
||||||
<TransitionGroup
|
<div class="login-header-actions">
|
||||||
appear
|
|
||||||
enter-active-class="animate__animated animate__bounceInLeft"
|
|
||||||
tag="div"
|
|
||||||
>
|
|
||||||
<img key="1" alt="" class="w-350px" src="@/assets/svgs/001.png" />
|
|
||||||
<div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
|
|
||||||
<div key="3" class="mt-5 text-14px font-normal text-white">
|
|
||||||
{{ t('login.message') }}
|
|
||||||
</div>
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
|
|
||||||
>
|
|
||||||
<!-- 右上角的主题、语言选择 -->
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-between at-2xl:justify-end at-xl:justify-end"
|
|
||||||
style="color: var(--el-text-color-primary);"
|
|
||||||
>
|
|
||||||
<div class="flex items-center at-2xl:hidden at-xl:hidden">
|
|
||||||
<img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
|
|
||||||
<span class="text-20px font-bold" >{{ underlineToHump(appStore.getTitle) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-end space-x-10px h-48px">
|
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<LocaleDropdown />
|
<LocaleDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="container">
|
||||||
|
<!-- 左侧面板 -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="brand-box">
|
||||||
|
<div class="logo-main">
|
||||||
|
<svg viewBox="0 0 100 100" class="logo-svg">
|
||||||
|
<path d="M50 2L2 25.5v49L50 98l48-23.5v-49L50 2zm35.2 65.7L50 88.3 14.8 67.7V32.3L50 11.7l35.2 20.6v35.4z" />
|
||||||
|
<path d="M38.1 48.9h23.8L50 28.3 38.1 48.9zM50 71.7l11.9-20.6H38.1L50 71.7z" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右边的登录界面 -->
|
<div class="brand-name">Ai 决策通</div>
|
||||||
<Transition appear enter-active-class="animate__animated animate__bounceInRight">
|
|
||||||
<div
|
|
||||||
class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
|
|
||||||
>
|
|
||||||
<!-- 账号登录 -->
|
|
||||||
<LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
<h2 class="sub-title">服鞋行业AI决策管理平台</h2>
|
||||||
|
<p class="desc">
|
||||||
|
专业的服鞋供应链 AI 决策中心。通过实时大数据分析与行业领先的智能算法,助力用户实现高效配补货决策及数字化经营管理。
|
||||||
|
</p>
|
||||||
|
<div class="features">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14h-2v-4h2v4zm4 0h-2V7h2v10zm-8 0H8v-7h2v7z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
|
AI智能配补货
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 17.08l1.5 1.41z"/></svg>
|
||||||
|
</div>
|
||||||
|
AI报表平台
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M22 2H2v20h20V2zM10 7h4v2h-4V7zm8 10H6v-2h12v2zm0-4H6v-2h12v2zm0-4h-2V7h2v2z"/></svg>
|
||||||
|
</div>
|
||||||
|
AI管理工具包
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="icon-box">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>
|
||||||
|
</div>
|
||||||
|
AI智能标签
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧登录面板 -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="login-card">
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
长沙决策通科技有限公司 © 2026 版权所有
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
|
||||||
import { underlineToHump } from '@/utils'
|
|
||||||
|
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
<script lang="ts" setup>
|
||||||
import { useAppStore } from '@/store/modules/app'
|
|
||||||
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
|
||||||
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
|
||||||
|
import { LoginForm } from './components'
|
||||||
import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
|
|
||||||
|
|
||||||
defineOptions({ name: 'Login' })
|
defineOptions({ name: 'Login' })
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const appStore = useAppStore()
|
|
||||||
const { getPrefixCls } = useDesign()
|
|
||||||
const prefixCls = getPrefixCls('login')
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$prefix-cls: #{$namespace}-login;
|
.login-page {
|
||||||
|
--primary-blue: #004fb0;
|
||||||
|
--btn-grad: linear-gradient(180deg, #8cbaff 0%, #3e81e0 100%);
|
||||||
|
--glass: rgba(255, 255, 255, 0.35);
|
||||||
|
--grid-size: 70px;
|
||||||
|
|
||||||
.#{$prefix-cls} {
|
position: relative;
|
||||||
overflow: auto;
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__left {
|
.bg-container {
|
||||||
&::before {
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-base {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
background-image: linear-gradient(rgba(0, 79, 176, 0.08) 1.5px, transparent 1.5px),
|
||||||
z-index: -1;
|
linear-gradient(90deg, rgba(0, 79, 176, 0.08) 1.5px, transparent 1.5px);
|
||||||
|
background-size: var(--grid-size) var(--grid-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob {
|
||||||
|
position: absolute;
|
||||||
|
filter: blur(80px);
|
||||||
|
opacity: 0.5;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.blob-1 {
|
||||||
|
width: 700px;
|
||||||
|
height: 700px;
|
||||||
|
background: #9ec9ff;
|
||||||
|
top: -15%;
|
||||||
|
left: -5%;
|
||||||
|
}
|
||||||
|
.blob-2 {
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
background: #7fb4ff;
|
||||||
|
bottom: -15%;
|
||||||
|
right: -5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-line {
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(90deg, transparent, #4388e4, transparent);
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.h-line {
|
||||||
|
height: 1.5px;
|
||||||
|
width: 150px;
|
||||||
|
animation: h-move 7s linear infinite;
|
||||||
|
}
|
||||||
|
.v-line {
|
||||||
|
width: 1.5px;
|
||||||
|
height: 150px;
|
||||||
|
background: linear-gradient(180deg, transparent, #4388e4, transparent);
|
||||||
|
animation: v-move 9s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes h-move {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100vw);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes v-move {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100vh);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header-actions {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 24px;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
max-width: 1300px;
|
||||||
background-image: url('@/assets/svgs/login-bg.svg');
|
margin: 0 auto;
|
||||||
background-position: center;
|
padding: 40px;
|
||||||
background-repeat: no-repeat;
|
display: flex;
|
||||||
content: '';
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
flex: 1.3;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-main {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: linear-gradient(135deg, #0052d4, #4364f7);
|
||||||
|
border-radius: 14px;
|
||||||
|
margin-right: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 82, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-svg {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--primary-blue);
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
color: #5c6e82;
|
||||||
|
font-size: 15px;
|
||||||
|
max-width: 500px;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 260px);
|
||||||
|
gap: 20px;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid white;
|
||||||
|
padding: 18px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: 0.3s;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.feature-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: #2a5fb2;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
flex: 0.7;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 420px;
|
||||||
|
background: var(--glass);
|
||||||
|
backdrop-filter: blur(30px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 50px 45px;
|
||||||
|
box-shadow: 0 40px 80px rgba(0, 79, 176, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.left-panel {
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
.right-panel {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.brand-name {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
.sub-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.features {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 40px 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- 暗色主题下字体颜色跟随反转 -->
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.dark .login-form {
|
.dark .login-page {
|
||||||
.el-divider__text {
|
.brand-name {
|
||||||
background-color: var(--login-bg-color);
|
color: var(--el-color-primary-light-3);
|
||||||
}
|
}
|
||||||
|
.sub-title {
|
||||||
.el-card {
|
color: var(--el-text-color-primary);
|
||||||
background-color: var(--login-bg-color);
|
}
|
||||||
|
.desc {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.feature-card {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
background: var(--el-bg-color-overlay);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,71 +4,68 @@
|
|||||||
ref="formLogin"
|
ref="formLogin"
|
||||||
:model="loginData.loginForm"
|
:model="loginData.loginForm"
|
||||||
:rules="LoginRules"
|
:rules="LoginRules"
|
||||||
class="login-form"
|
class="login-form-styled"
|
||||||
label-position="top"
|
label-position="top"
|
||||||
label-width="120px"
|
label-width="0"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<el-row class="mx-[-10px]">
|
<div class="login-header">
|
||||||
<el-col :span="24" class="px-10px">
|
<div class="welcome-text">欢迎登录</div>
|
||||||
<el-form-item>
|
</div>
|
||||||
<LoginFormTitle class="w-full" />
|
|
||||||
</el-form-item>
|
<!-- 租户名 -->
|
||||||
</el-col>
|
<div v-if="loginData.tenantEnable === 'true'" class="input-group">
|
||||||
<el-col :span="24" class="px-10px">
|
<el-form-item prop="tenantName">
|
||||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginData.loginForm.tenantName"
|
v-model="loginData.loginForm.tenantName"
|
||||||
:placeholder="t('login.tenantNamePlaceholder')"
|
:placeholder="t('login.tenantNamePlaceholder')"
|
||||||
:prefix-icon="iconHouse"
|
class="styled-input"
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
<el-col :span="24" class="px-10px">
|
|
||||||
|
<!-- 用户名 -->
|
||||||
|
<div class="input-group">
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginData.loginForm.username"
|
v-model="loginData.loginForm.username"
|
||||||
:placeholder="t('login.usernamePlaceholder')"
|
:placeholder="t('login.usernamePlaceholder')"
|
||||||
:prefix-icon="iconAvatar"
|
class="styled-input"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
<el-col :span="24" class="px-10px">
|
|
||||||
|
<!-- 密码 -->
|
||||||
|
<div class="input-group">
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="loginData.loginForm.password"
|
v-model="loginData.loginForm.password"
|
||||||
:placeholder="t('login.passwordPlaceholder')"
|
:placeholder="t('login.passwordPlaceholder')"
|
||||||
:prefix-icon="iconLock"
|
class="styled-input"
|
||||||
show-password
|
show-password
|
||||||
type="password"
|
type="password"
|
||||||
@keyup.enter="getCode()"
|
@keyup.enter="getCode()"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
<el-col :span="24" class="px-10px mt-[-20px] mb-[-20px]">
|
|
||||||
<el-form-item>
|
<!-- 记住我 -->
|
||||||
<el-row justify="space-between" style="width: 100%">
|
<div class="links-row">
|
||||||
<el-col :span="6">
|
|
||||||
<el-checkbox v-model="loginData.loginForm.rememberMe">
|
<el-checkbox v-model="loginData.loginForm.rememberMe">
|
||||||
{{ t('login.remember') }}
|
{{ t('login.remember') }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
</el-col>
|
</div>
|
||||||
</el-row>
|
|
||||||
</el-form-item>
|
<!-- 登录按钮 -->
|
||||||
</el-col>
|
<button
|
||||||
<el-col :span="24" class="px-10px">
|
type="button"
|
||||||
<el-form-item>
|
class="login-btn"
|
||||||
<XButton
|
:disabled="loginLoading"
|
||||||
:loading="loginLoading"
|
|
||||||
:title="t('login.login')"
|
|
||||||
class="w-full"
|
|
||||||
type="primary"
|
|
||||||
@click="getCode()"
|
@click="getCode()"
|
||||||
/>
|
>
|
||||||
</el-form-item>
|
{{ loginLoading ? '登录中...' : t('login.login') }}
|
||||||
</el-col>
|
</button>
|
||||||
|
|
||||||
<Verify
|
<Verify
|
||||||
v-if="loginData.captchaEnable === 'true'"
|
v-if="loginData.captchaEnable === 'true'"
|
||||||
ref="verify"
|
ref="verify"
|
||||||
@@ -77,16 +74,14 @@
|
|||||||
mode="pop"
|
mode="pop"
|
||||||
@success="handleLogin"
|
@success="handleLogin"
|
||||||
/>
|
/>
|
||||||
</el-row>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ElLoading } from 'element-plus'
|
import { ElLoading } from 'element-plus'
|
||||||
import LoginFormTitle from './LoginFormTitle.vue'
|
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
|
||||||
import { useIcon } from '@/hooks/web/useIcon'
|
import { required } from '@/utils/formRules'
|
||||||
|
|
||||||
import * as authUtil from '@/utils/auth'
|
import * as authUtil from '@/utils/auth'
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
import * as LoginApi from '@/api/login'
|
import * as LoginApi from '@/api/login'
|
||||||
@@ -96,28 +91,27 @@ defineOptions({ name: 'LoginForm' })
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const iconHouse = useIcon({ icon: 'ep:house' })
|
|
||||||
const iconAvatar = useIcon({ icon: 'ep:avatar' })
|
|
||||||
const iconLock = useIcon({ icon: 'ep:lock' })
|
|
||||||
const formLogin = ref()
|
const formLogin = ref()
|
||||||
const { validForm } = useFormValid(formLogin)
|
const { validForm } = useFormValid(formLogin)
|
||||||
const { setLoginState, getLoginState } = useLoginState()
|
const { getLoginState } = useLoginState()
|
||||||
const { currentRoute, push } = useRouter()
|
const { currentRoute, push } = useRouter()
|
||||||
const permissionStore = usePermissionStore()
|
const permissionStore = usePermissionStore()
|
||||||
const redirect = ref<string>('')
|
const redirect = ref<string>('')
|
||||||
const loginLoading = ref(false)
|
const loginLoading = ref(false)
|
||||||
const verify = ref()
|
const verify = ref()
|
||||||
const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字 pictureWord 文字验证码
|
const captchaType = ref('blockPuzzle')
|
||||||
|
|
||||||
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
|
||||||
|
|
||||||
const LoginRules = {
|
const LoginRules: Record<string, any> = {
|
||||||
tenantName: [required],
|
|
||||||
username: [required],
|
username: [required],
|
||||||
password: [required]
|
password: [required]
|
||||||
}
|
}
|
||||||
|
if (import.meta.env.VITE_APP_TENANT_ENABLE === 'true') {
|
||||||
|
LoginRules.tenantName = [required]
|
||||||
|
}
|
||||||
|
|
||||||
const loginData = reactive({
|
const loginData = reactive({
|
||||||
isShowPassword: false,
|
|
||||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||||
loginForm: {
|
loginForm: {
|
||||||
@@ -125,49 +119,38 @@ const loginData = reactive({
|
|||||||
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
||||||
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
||||||
captchaVerification: '',
|
captchaVerification: '',
|
||||||
rememberMe: true // 默认记录我。如果不需要,可手动修改
|
rememberMe: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const socialList = [
|
|
||||||
{ icon: 'ant-design:wechat-filled', type: 30 },
|
|
||||||
{ icon: 'ant-design:dingtalk-circle-filled', type: 20 },
|
|
||||||
{ icon: 'ant-design:github-filled', type: 0 },
|
|
||||||
{ icon: 'ant-design:alipay-circle-filled', type: 0 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 获取验证码
|
|
||||||
const getCode = async () => {
|
const getCode = async () => {
|
||||||
// 情况一,未开启:则直接登录
|
|
||||||
if (loginData.captchaEnable === 'false') {
|
if (loginData.captchaEnable === 'false') {
|
||||||
await handleLogin({})
|
await handleLogin({})
|
||||||
} else {
|
} else {
|
||||||
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
|
verify.value?.show()
|
||||||
// 弹出验证码
|
|
||||||
verify.value.show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 获取租户 ID
|
|
||||||
const getTenantId = async () => {
|
const getTenantId = async () => {
|
||||||
if (loginData.tenantEnable === 'true') {
|
if (loginData.tenantEnable === 'true') {
|
||||||
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
|
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
|
||||||
authUtil.setTenantId(res)
|
authUtil.setTenantId(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 记住我
|
|
||||||
const getLoginFormCache = () => {
|
const getLoginFormCache = () => {
|
||||||
const loginForm = authUtil.getLoginForm()
|
const loginForm = authUtil.getLoginForm()
|
||||||
if (loginForm) {
|
if (loginForm) {
|
||||||
loginData.loginForm = {
|
loginData.loginForm = {
|
||||||
...loginData.loginForm,
|
...loginData.loginForm,
|
||||||
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
|
username: loginForm.username ?? loginData.loginForm.username,
|
||||||
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
|
password: loginForm.password ?? loginData.loginForm.password,
|
||||||
rememberMe: loginForm.rememberMe,
|
rememberMe: loginForm.rememberMe ?? true,
|
||||||
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
|
tenantName: loginForm.tenantName ?? loginData.loginForm.tenantName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 根据域名,获得租户信息
|
|
||||||
const getTenantByWebsite = async () => {
|
const getTenantByWebsite = async () => {
|
||||||
if (loginData.tenantEnable === 'true') {
|
if (loginData.tenantEnable === 'true') {
|
||||||
const website = location.host
|
const website = location.host
|
||||||
@@ -178,8 +161,9 @@ const getTenantByWebsite = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const loading = ref() // ElLoading.service 返回的实例
|
|
||||||
// 登录
|
let loading: any = null
|
||||||
|
|
||||||
const handleLogin = async (params: any) => {
|
const handleLogin = async (params: any) => {
|
||||||
loginLoading.value = true
|
loginLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -188,19 +172,19 @@ const handleLogin = async (params: any) => {
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const loginDataLoginForm = { ...loginData.loginForm }
|
const loginFormData = { ...loginData.loginForm }
|
||||||
loginDataLoginForm.captchaVerification = params.captchaVerification
|
loginFormData.captchaVerification = params.captchaVerification
|
||||||
const res = await LoginApi.login(loginDataLoginForm)
|
const res = await LoginApi.login(loginFormData)
|
||||||
if (!res) {
|
if (!res) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loading.value = ElLoading.service({
|
loading = ElLoading.service({
|
||||||
lock: true,
|
lock: true,
|
||||||
text: '正在加载系统中...',
|
text: '正在加载系统中...',
|
||||||
background: 'rgba(0, 0, 0, 0.7)'
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
})
|
})
|
||||||
if (loginDataLoginForm.rememberMe) {
|
if (loginFormData.rememberMe) {
|
||||||
authUtil.setLoginForm(loginDataLoginForm)
|
authUtil.setLoginForm(loginFormData)
|
||||||
} else {
|
} else {
|
||||||
authUtil.removeLoginForm()
|
authUtil.removeLoginForm()
|
||||||
}
|
}
|
||||||
@@ -208,7 +192,6 @@ const handleLogin = async (params: any) => {
|
|||||||
if (!redirect.value) {
|
if (!redirect.value) {
|
||||||
redirect.value = '/'
|
redirect.value = '/'
|
||||||
}
|
}
|
||||||
// 判断是否为SSO登录
|
|
||||||
if (redirect.value.indexOf('sso') !== -1) {
|
if (redirect.value.indexOf('sso') !== -1) {
|
||||||
window.location.href = window.location.href.replace('/login?redirect=', '')
|
window.location.href = window.location.href.replace('/login?redirect=', '')
|
||||||
} else {
|
} else {
|
||||||
@@ -216,54 +199,18 @@ const handleLogin = async (params: any) => {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loginLoading.value = false
|
loginLoading.value = false
|
||||||
loading.value.close()
|
loading?.close?.()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 社交登录
|
|
||||||
const doSocialLogin = async (type: number) => {
|
|
||||||
if (type === 0) {
|
|
||||||
message.error('此方式未配置')
|
|
||||||
} else {
|
|
||||||
loginLoading.value = true
|
|
||||||
if (loginData.tenantEnable === 'true') {
|
|
||||||
// 尝试先通过 tenantName 获取租户
|
|
||||||
await getTenantId()
|
|
||||||
// 如果获取不到,则需要弹出提示,进行处理
|
|
||||||
if (!authUtil.getTenantId()) {
|
|
||||||
try {
|
|
||||||
const data = await message.prompt('请输入租户名称', t('common.reminder'))
|
|
||||||
if (data?.action !== 'confirm') throw 'cancel'
|
|
||||||
const res = await LoginApi.getTenantIdByName(data.value)
|
|
||||||
authUtil.setTenantId(res)
|
|
||||||
} catch (error) {
|
|
||||||
if (error === 'cancel') return
|
|
||||||
} finally {
|
|
||||||
loginLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 计算 redirectUri
|
|
||||||
// 注意: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。
|
|
||||||
// 配合 social-login.vue#getUrlValue() 使用
|
|
||||||
const redirectUri =
|
|
||||||
location.origin +
|
|
||||||
'/social-login?' +
|
|
||||||
encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
|
|
||||||
|
|
||||||
// 进行跳转
|
|
||||||
window.location.href = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
watch(
|
watch(
|
||||||
() => currentRoute.value,
|
() => currentRoute.value,
|
||||||
(route: RouteLocationNormalizedLoaded) => {
|
(route: RouteLocationNormalizedLoaded) => {
|
||||||
redirect.value = route?.query?.redirect as string
|
redirect.value = route?.query?.redirect as string
|
||||||
},
|
},
|
||||||
{
|
{ immediate: true }
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getLoginFormCache()
|
getLoginFormCache()
|
||||||
getTenantByWebsite()
|
getTenantByWebsite()
|
||||||
@@ -271,23 +218,119 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.anticon) {
|
.login-form-styled {
|
||||||
&:hover {
|
:deep(.el-form-item) {
|
||||||
color: var(--el-color-primary) !important;
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item__error) {
|
||||||
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-code {
|
.login-header {
|
||||||
float: right;
|
margin-bottom: 40px;
|
||||||
width: 100%;
|
}
|
||||||
height: 38px;
|
|
||||||
|
|
||||||
img {
|
.welcome-text {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
:deep(.styled-input) {
|
||||||
|
.el-input__wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
padding: 18px 22px;
|
||||||
max-width: 100px;
|
border-radius: 15px;
|
||||||
vertical-align: middle;
|
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 16px;
|
||||||
|
transition: 0.3s;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.el-input__wrapper:hover {
|
||||||
|
border-color: rgba(0, 79, 176, 0.3);
|
||||||
|
}
|
||||||
|
.el-input__wrapper.is-focus {
|
||||||
|
border-color: #004fb0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.el-input__inner,
|
||||||
|
input {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333 !important;
|
||||||
|
-webkit-text-fill-color: #333 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.links-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 15px 0 40px;
|
||||||
|
|
||||||
|
:deep(.el-checkbox__label) {
|
||||||
|
color: #6e7c8d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 18px;
|
||||||
|
background: linear-gradient(180deg, #8cbaff 0%, #3e81e0 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 35px;
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-shadow: 0 12px 25px rgba(62, 129, 224, 0.35);
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
.login-btn:hover:not(:disabled) {
|
||||||
|
transform: scale(1.02);
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
.login-btn:disabled {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 暗色主题下登录表单字体颜色跟随反转 -->
|
||||||
|
<style lang="scss">
|
||||||
|
.dark .login-form-styled {
|
||||||
|
.welcome-text {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
.links-row :deep(.el-checkbox__label) {
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
.input-group :deep(.el-input__wrapper) {
|
||||||
|
background: var(--el-fill-color);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
}
|
||||||
|
/* 已输入文字颜色 - 确保暗色下清晰可见 */
|
||||||
|
.input-group :deep(.el-input__inner),
|
||||||
|
.input-group :deep(input) {
|
||||||
|
color: var(--el-text-color-primary) !important;
|
||||||
|
-webkit-text-fill-color: var(--el-text-color-primary) !important;
|
||||||
|
}
|
||||||
|
.input-group :deep(input::placeholder) {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
.input-group :deep(.el-input__wrapper.is-focus) {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.input-group :deep(.el-input__wrapper.is-focus .el-input__inner),
|
||||||
|
.input-group :deep(.el-input__wrapper.is-focus input) {
|
||||||
|
color: var(--el-text-color-primary) !important;
|
||||||
|
-webkit-text-fill-color: var(--el-text-color-primary) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -61,6 +61,9 @@
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="租户提示词" prop="tenantPrompt">
|
||||||
|
<el-input v-model="formData.tenantPrompt" type="textarea" :rows="3" placeholder="请输入租户提示词" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="租户状态" prop="status">
|
<el-form-item label="租户状态" prop="status">
|
||||||
<el-radio-group v-model="formData.status">
|
<el-radio-group v-model="formData.status">
|
||||||
<el-radio
|
<el-radio
|
||||||
@@ -103,13 +106,15 @@ const formData = ref({
|
|||||||
expireTime: undefined,
|
expireTime: undefined,
|
||||||
websites: [],
|
websites: [],
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
|
tenantPrompt: '',
|
||||||
// 新增专属
|
// 新增专属
|
||||||
username: undefined,
|
username: undefined,
|
||||||
password: undefined
|
password: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = reactive({
|
const formRules = reactive({
|
||||||
name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
|
name: [{ required: true, message: '租户名不能为空', trigger: 'blur' }],
|
||||||
packageId: [{ required: true, message: '租户套<EFBFBD><EFBFBD><EFBFBD>不能为空', trigger: 'blur' }],
|
packageId: [{ required: true, message: '租户套餐不能为空', trigger: 'blur' }],
|
||||||
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
|
contactName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
|
||||||
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
|
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
|
||||||
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
|
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
|
||||||
@@ -178,6 +183,7 @@ const resetForm = () => {
|
|||||||
expireTime: undefined,
|
expireTime: undefined,
|
||||||
websites: [],
|
websites: [],
|
||||||
status: CommonStatusEnum.ENABLE,
|
status: CommonStatusEnum.ENABLE,
|
||||||
|
tenantPrompt: '',
|
||||||
username: undefined,
|
username: undefined,
|
||||||
password: undefined
|
password: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="560px">
|
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
@@ -7,9 +7,18 @@
|
|||||||
label-width="110px"
|
label-width="110px"
|
||||||
v-loading="formLoading"
|
v-loading="formLoading"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="标签类别" prop="type">
|
||||||
|
<el-select v-model="formData.type" placeholder="请选择标签类别" class="!w-full" @change="loadInitialSqlByType">
|
||||||
|
<el-option label="产品 (product)" value="product" />
|
||||||
|
<el-option label="门店 (store)" value="store" />
|
||||||
|
<el-option label="供货商 (supplier)" value="supplier" />
|
||||||
|
<el-option label="会员 (member)" value="member" />
|
||||||
|
</el-select>
|
||||||
|
<div class="form-tip">选择与标准标签一致的类别,名称不能与同类型标准标签重复</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="标签名称" prop="name">
|
<el-form-item label="标签名称" prop="name">
|
||||||
<el-input v-model="formData.name" placeholder="请输入标签名称,如:销售区域" maxlength="50" show-word-limit />
|
<el-input v-model="formData.name" placeholder="请输入标签名称,如:销售区域" maxlength="50" show-word-limit @blur="validateNameWithStandardTags" />
|
||||||
<div class="form-tip">用于报表中展示的标签名称</div>
|
<div class="form-tip">用于报表中展示的标签名称,不能与同类型标准标签重名</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="公式描述" prop="expression">
|
<el-form-item label="公式描述" prop="expression">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -45,13 +54,22 @@
|
|||||||
<div class="form-tip">用于报表展示时的颜色标识,可点击预设色或使用取色盘</div>
|
<div class="form-tip">用于报表展示时的颜色标识,可点击预设色或使用取色盘</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="SQL 脚本" prop="sqlScript">
|
<el-form-item label="SQL 脚本" prop="sqlScript">
|
||||||
|
<div class="sql-script-field">
|
||||||
|
<div class="param-ref-row">
|
||||||
|
<el-button size="small" type="success" plain @click="insertInsertTemplate">
|
||||||
|
插入 {{ insertTableName }} 模板
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
<el-input
|
<el-input
|
||||||
|
ref="sqlScriptInputRef"
|
||||||
v-model="formData.sqlScript"
|
v-model="formData.sqlScript"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="8"
|
||||||
placeholder="请输入 SQL 脚本,用于获取标签数据"
|
placeholder="请输入标签同步时执行的 SQL 脚本,支持多行。可点击上方按钮插入 INSERT 模板"
|
||||||
|
class="sql-script-area"
|
||||||
/>
|
/>
|
||||||
<div class="form-tip">执行 SQL 获取标签选项,支持多列,第一列作为显示值</div>
|
<div class="form-tip">该 SQL 将用于标签同步,将结果写入对应标签表。支持多列,第一列作为 code,第二列作为 name</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -62,6 +80,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
||||||
|
import { TagApi } from '@/api/ydoyun/tagconfig'
|
||||||
|
|
||||||
defineOptions({ name: 'CustomTagForm' })
|
defineOptions({ name: 'CustomTagForm' })
|
||||||
|
|
||||||
@@ -75,17 +94,77 @@ const formType = ref('')
|
|||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
|
type: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
expression: undefined,
|
expression: undefined,
|
||||||
color: undefined,
|
color: undefined,
|
||||||
sqlScript: undefined
|
sqlScript: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const validateNameWithStandardTags = async (_rule: any, value: string, callback: (e?: Error) => void) => {
|
||||||
|
if (!value?.trim()) return callback()
|
||||||
|
const type = formData.value.type
|
||||||
|
if (!type) return callback()
|
||||||
|
try {
|
||||||
|
const list = await TagApi.getTagListByType(type)
|
||||||
|
const res = list as any
|
||||||
|
const tags = res?.data ?? res ?? []
|
||||||
|
const exists = Array.isArray(tags) && tags.some((t: any) => (t.name || '').trim() === value.trim())
|
||||||
|
if (exists) {
|
||||||
|
callback(new Error('该名称与同类型标准标签重复,请更换'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formRules = reactive({
|
const formRules = reactive({
|
||||||
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
|
type: [{ required: true, message: '请选择标签类别', trigger: 'change' }],
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '标签名称不能为空', trigger: 'blur' },
|
||||||
|
{ validator: validateNameWithStandardTags, trigger: 'blur' }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
const sqlScriptInputRef = ref()
|
||||||
|
|
||||||
|
const INSERT_TABLE_MAP: Record<string, string> = {
|
||||||
|
product: 'ydoyun_tag_product',
|
||||||
|
supplier: 'ydoyun_tag_supplier',
|
||||||
|
store: 'ydoyun_tag_store',
|
||||||
|
member: 'ydoyun_tag_member'
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertTableName = computed(() => {
|
||||||
|
const type = formData.value.type || 'product'
|
||||||
|
return INSERT_TABLE_MAP[type] || 'ydoyun_tag_product'
|
||||||
|
})
|
||||||
|
|
||||||
|
const getInsertTemplate = () => {
|
||||||
|
const table = insertTableName.value
|
||||||
|
return `INSERT INTO ${table} (code, name, color)
|
||||||
|
SELECT code, name, '\${颜色}' AS color FROM (
|
||||||
|
-- 在此编写您的查询逻辑,将 code、name 替换为实际字段;color 使用 \${颜色} 参数引用,同步时代入标签颜色
|
||||||
|
SELECT '' AS code, '' AS name FROM (SELECT 1) _ LIMIT 0
|
||||||
|
) t
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertInsertTemplate = () => {
|
||||||
|
const template = getInsertTemplate()
|
||||||
|
const oldScript = formData.value.sqlScript || ''
|
||||||
|
formData.value.sqlScript = template + (oldScript ? '\n' + oldScript : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据类型加载初始 SQL(仅新增且当前为空时) */
|
||||||
|
const loadInitialSqlByType = () => {
|
||||||
|
if (formType.value !== 'create' || formData.value.sqlScript?.trim()) return
|
||||||
|
if (!formData.value.type) return
|
||||||
|
formData.value.sqlScript = getInsertTemplate()
|
||||||
|
}
|
||||||
|
|
||||||
/** 预设颜色选项 */
|
/** 预设颜色选项 */
|
||||||
const presetColors = [
|
const presetColors = [
|
||||||
@@ -114,6 +193,8 @@ const open = async (type: string, id?: number) => {
|
|||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false
|
formLoading.value = false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
loadInitialSqlByType()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +205,8 @@ const submitForm = async () => {
|
|||||||
await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = formData.value as unknown as CustomTag
|
const base = { ...formData.value }
|
||||||
|
const data = (formType.value === 'create' ? { ...base, useParams: false } : base) as unknown as CustomTag
|
||||||
if (formType.value === 'create') {
|
if (formType.value === 'create') {
|
||||||
await CustomTagApi.createCustomTag(data)
|
await CustomTagApi.createCustomTag(data)
|
||||||
message.success(t('common.createSuccess'))
|
message.success(t('common.createSuccess'))
|
||||||
@@ -143,6 +225,7 @@ const submitForm = async () => {
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
|
type: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
expression: undefined,
|
expression: undefined,
|
||||||
color: undefined,
|
color: undefined,
|
||||||
@@ -200,4 +283,18 @@ const resetForm = () => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sql-script-field { width: 100%; }
|
||||||
|
.param-ref-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.sql-script-area { width: 100%; }
|
||||||
|
.sql-script-area :deep(textarea) {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="560px">
|
<Dialog :title="dialogTitle" v-model="dialogVisible" width="720px">
|
||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
@@ -7,9 +7,18 @@
|
|||||||
label-width="110px"
|
label-width="110px"
|
||||||
v-loading="formLoading"
|
v-loading="formLoading"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="标签类型" prop="type">
|
||||||
|
<el-select v-model="formData.type" placeholder="请选择标签类型" class="!w-full" :disabled="!!props.tagType">
|
||||||
|
<el-option label="产品 (product)" value="product" />
|
||||||
|
<el-option label="门店 (store)" value="store" />
|
||||||
|
<el-option label="供货商 (supplier)" value="supplier" />
|
||||||
|
<el-option label="会员 (member)" value="member" />
|
||||||
|
</el-select>
|
||||||
|
<div class="form-tip">选择与标准标签一致的类别,名称不能与同类型标准标签重复</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="标签名称" prop="name">
|
<el-form-item label="标签名称" prop="name">
|
||||||
<el-input v-model="formData.name" placeholder="请输入标签名称" maxlength="50" show-word-limit />
|
<el-input v-model="formData.name" placeholder="请输入标签名称" maxlength="50" show-word-limit @blur="validateNameWithStandardTags" />
|
||||||
<div class="form-tip">用于报表中展示的标签名称</div>
|
<div class="form-tip">用于报表中展示的标签名称,不能与同类型标准标签重名</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="公式描述" prop="expression">
|
<el-form-item label="公式描述" prop="expression">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -52,30 +61,23 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="SQL 脚本" prop="sqlScript">
|
<el-form-item label="SQL 脚本" prop="sqlScript">
|
||||||
<div class="sql-script-field">
|
<div class="sql-script-field">
|
||||||
<div v-if="formData.useParams === 1" class="param-insert-bar">
|
<div class="param-ref-row">
|
||||||
<span class="param-label">插入参数变量:</span>
|
<el-button size="small" type="success" plain @click="insertInsertTemplate">
|
||||||
<el-select
|
插入 {{ insertTableName }} 模板
|
||||||
v-model="selectedParamIndex"
|
</el-button>
|
||||||
:placeholder="paramList.length > 0 ? '选择参数后点击插入' : '请先填写上方参数列表'"
|
<template v-if="formData.useParams === 1">
|
||||||
clearable
|
<span class="param-ref-label">引用参数:</span>
|
||||||
class="param-select"
|
<el-button
|
||||||
:disabled="paramList.length === 0"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="(p, idx) in paramList"
|
v-for="(p, idx) in paramList"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
:label="`参数${idx + 1}: ${p}`"
|
|
||||||
:value="idx"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="paramList.length === 0 || selectedParamIndex === null"
|
type="info"
|
||||||
@click="selectedParamIndex != null ? insertParamVar(selectedParamIndex) : null"
|
plain
|
||||||
|
@click="insertParamRef(p, idx)"
|
||||||
>
|
>
|
||||||
插入
|
引用{{ p }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<el-input
|
<el-input
|
||||||
ref="sqlScriptInputRef"
|
ref="sqlScriptInputRef"
|
||||||
@@ -112,6 +114,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CustomTagApi, CustomTag, CustomTagType } from '@/api/ydoyun/customtag'
|
import { CustomTagApi, CustomTag, CustomTagType } from '@/api/ydoyun/customtag'
|
||||||
|
import { TagApi } from '@/api/ydoyun/tagconfig'
|
||||||
|
|
||||||
defineOptions({ name: 'CustomTagFormWithParams' })
|
defineOptions({ name: 'CustomTagFormWithParams' })
|
||||||
|
|
||||||
@@ -138,14 +141,85 @@ const formData = ref<Partial<CustomTag>>({
|
|||||||
params: undefined
|
params: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const validateNameWithStandardTags = async (_rule: any, value: string, callback: (e?: Error) => void) => {
|
||||||
|
if (!value?.trim()) return callback()
|
||||||
|
const type = formData.value.type ?? props.tagType
|
||||||
|
if (!type) return callback()
|
||||||
|
try {
|
||||||
|
const list = await TagApi.getTagListByType(type)
|
||||||
|
const res = list as any
|
||||||
|
const tags = res?.data ?? res ?? []
|
||||||
|
const exists = Array.isArray(tags) && tags.some((t: any) => (t.name || '').trim() === value.trim())
|
||||||
|
if (exists) {
|
||||||
|
callback(new Error('该名称与同类型标准标签重复,请更换'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formRules = reactive({
|
const formRules = reactive({
|
||||||
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
|
type: [{ required: true, message: '请选择标签类型', trigger: 'change' }],
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '标签名称不能为空', trigger: 'blur' },
|
||||||
|
{ validator: validateNameWithStandardTags, trigger: 'blur' }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const sqlScriptInputRef = ref()
|
const sqlScriptInputRef = ref()
|
||||||
const sqlCursorPos = ref({ start: 0, end: 0 })
|
const sqlCursorPos = ref({ start: 0, end: 0 })
|
||||||
const selectedParamIndex = ref<number | null>(null)
|
|
||||||
|
const INSERT_TABLE_MAP: Record<string, string> = {
|
||||||
|
product: 'ydoyun_tag_product',
|
||||||
|
supplier: 'ydoyun_tag_supplier',
|
||||||
|
store: 'ydoyun_tag_store',
|
||||||
|
member: 'ydoyun_tag_member'
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertTableName = computed(() => {
|
||||||
|
const type = formData.value.type ?? props.tagType ?? 'product'
|
||||||
|
return INSERT_TABLE_MAP[type] || 'ydoyun_tag_product'
|
||||||
|
})
|
||||||
|
|
||||||
|
const getInsertTemplate = () => {
|
||||||
|
const table = insertTableName.value
|
||||||
|
return `INSERT INTO ${table} (code, name, color)
|
||||||
|
SELECT code, name, '\${颜色}' AS color FROM (
|
||||||
|
-- 在此编写您的查询逻辑,将 code、name 替换为实际字段;color 使用 \${颜色} 参数引用,同步时代入标签颜色
|
||||||
|
SELECT '' AS code, '' AS name FROM (SELECT 1) _ LIMIT 0
|
||||||
|
) t
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertInsertTemplate = () => {
|
||||||
|
const template = getInsertTemplate()
|
||||||
|
const oldScript = formData.value.sqlScript || ''
|
||||||
|
formData.value.sqlScript = template + (oldScript ? '\n' + oldScript : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertAtCursor = (text: string) => {
|
||||||
|
const textarea = sqlScriptInputRef.value?.$el?.querySelector('textarea')
|
||||||
|
if (textarea) {
|
||||||
|
const start = textarea.selectionStart
|
||||||
|
const end = textarea.selectionEnd
|
||||||
|
const sql = formData.value.sqlScript || ''
|
||||||
|
formData.value.sqlScript = sql.slice(0, start) + text + sql.slice(end)
|
||||||
|
nextTick(() => {
|
||||||
|
textarea.focus()
|
||||||
|
const newPos = start + text.length
|
||||||
|
textarea.setSelectionRange(newPos, newPos)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
formData.value.sqlScript = (formData.value.sqlScript || '') + text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertParamRef = (_p: string, idx: number) => {
|
||||||
|
insertAtCursor(`\${参数${idx + 1}}`)
|
||||||
|
}
|
||||||
|
|
||||||
const paramList = computed(() => {
|
const paramList = computed(() => {
|
||||||
const p = formData.value.params
|
const p = formData.value.params
|
||||||
@@ -160,24 +234,6 @@ const saveSqlCursor = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertParamVar = (index: number) => {
|
|
||||||
const variable = `\${参数${index + 1}}`
|
|
||||||
const current = formData.value.sqlScript || ''
|
|
||||||
const { start, end } = sqlCursorPos.value
|
|
||||||
const before = current.slice(0, start)
|
|
||||||
const after = current.slice(end)
|
|
||||||
formData.value.sqlScript = before + variable + after
|
|
||||||
selectedParamIndex.value = null
|
|
||||||
nextTick(() => {
|
|
||||||
sqlCursorPos.value = { start: start + variable.length, end: start + variable.length }
|
|
||||||
const textarea = sqlScriptInputRef.value?.$el?.querySelector('textarea')
|
|
||||||
if (textarea) {
|
|
||||||
textarea.focus()
|
|
||||||
textarea.setSelectionRange(start + variable.length, start + variable.length)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const sqlSegments = computed(() => {
|
const sqlSegments = computed(() => {
|
||||||
const sql = formData.value.sqlScript || ''
|
const sql = formData.value.sqlScript || ''
|
||||||
const regex = /\$\{([^}]+)\}/g
|
const regex = /\$\{([^}]+)\}/g
|
||||||
@@ -241,9 +297,19 @@ const open = async (type: string, id?: number) => {
|
|||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false
|
formLoading.value = false
|
||||||
}
|
}
|
||||||
|
} else if (type === 'create') {
|
||||||
|
loadInitialSqlByType()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据类型加载初始 SQL(仅新增且当前为空时) */
|
||||||
|
const loadInitialSqlByType = () => {
|
||||||
|
if (formType.value !== 'create' || formData.value.sqlScript?.trim()) return
|
||||||
|
const type = formData.value.type ?? props.tagType
|
||||||
|
if (!type) return
|
||||||
|
formData.value.sqlScript = getInsertTemplate()
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ open })
|
defineExpose({ open })
|
||||||
|
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
@@ -294,12 +360,10 @@ const resetForm = () => {
|
|||||||
.color-picker-wrap { display: flex; align-items: center; gap: 12px; }
|
.color-picker-wrap { display: flex; align-items: center; gap: 12px; }
|
||||||
.color-picker-wrap .color-input { flex: 1; min-width: 120px; }
|
.color-picker-wrap .color-input { flex: 1; min-width: 120px; }
|
||||||
.sql-script-field { width: 100%; }
|
.sql-script-field { width: 100%; }
|
||||||
.param-insert-bar {
|
.param-ref-row {
|
||||||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 8px;
|
display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;
|
||||||
padding: 8px 12px; background: var(--el-fill-color-light); border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
.param-label { font-size: 13px; color: var(--el-text-color-secondary); margin-right: 4px; }
|
.param-ref-label { font-size: 13px; color: var(--el-text-color-regular); margin-right: 4px; }
|
||||||
.param-select { width: 180px; }
|
|
||||||
.sql-preview {
|
.sql-preview {
|
||||||
margin-top: 10px; padding: 10px 12px; background: var(--el-fill-color-lighter);
|
margin-top: 10px; padding: 10px 12px; background: var(--el-fill-color-lighter);
|
||||||
border-radius: 6px; font-family: monospace; font-size: 13px;
|
border-radius: 6px; font-family: monospace; font-size: 13px;
|
||||||
|
|||||||
@@ -8,6 +8,14 @@
|
|||||||
:inline="true"
|
:inline="true"
|
||||||
label-width="90px"
|
label-width="90px"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="标签类型" prop="type">
|
||||||
|
<el-select v-model="queryParams.type" placeholder="全部" clearable class="!w-180px">
|
||||||
|
<el-option label="产品" value="product" />
|
||||||
|
<el-option label="门店" value="store" />
|
||||||
|
<el-option label="供货商" value="supplier" />
|
||||||
|
<el-option label="会员" value="member" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="标签名称" prop="name">
|
<el-form-item label="标签名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.name"
|
v-model="queryParams.name"
|
||||||
@@ -81,16 +89,20 @@
|
|||||||
@selection-change="handleRowCheckboxChange"
|
@selection-change="handleRowCheckboxChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
|
||||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">
|
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||||
{{ scope.row.color }}
|
{{ scope.row.name || '-' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-else>-</span>
|
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="标签类型" align="center" prop="type" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ typeLabelMap[scope.row.type] || scope.row.type || '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="创建时间"
|
label="创建时间"
|
||||||
align="center"
|
align="center"
|
||||||
@@ -146,9 +158,17 @@ const loading = ref(true)
|
|||||||
const list = ref<CustomTag[]>([])
|
const list = ref<CustomTag[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|
||||||
|
const typeLabelMap: Record<string, string> = {
|
||||||
|
product: '产品',
|
||||||
|
store: '门店',
|
||||||
|
supplier: '供货商',
|
||||||
|
member: '会员'
|
||||||
|
}
|
||||||
|
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNo: 1,
|
pageNo: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
|
type: undefined as string | undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
expression: undefined,
|
expression: undefined,
|
||||||
createTime: [] as string[]
|
createTime: [] as string[]
|
||||||
@@ -178,6 +198,7 @@ const handleQuery = () => {
|
|||||||
/** 重置 */
|
/** 重置 */
|
||||||
const resetQuery = () => {
|
const resetQuery = () => {
|
||||||
queryFormRef.value?.resetFields()
|
queryFormRef.value?.resetFields()
|
||||||
|
queryParams.type = undefined
|
||||||
handleQuery()
|
handleQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
:inline="true"
|
:inline="true"
|
||||||
label-width="90px"
|
label-width="90px"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="标签类型" prop="type">
|
||||||
|
<el-select v-model="queryParams.type" placeholder="标签类型" class="!w-140px" disabled>
|
||||||
|
<el-option label="产品" value="product" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="标签名称" prop="name">
|
<el-form-item label="标签名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.name"
|
v-model="queryParams.name"
|
||||||
@@ -62,14 +67,20 @@
|
|||||||
@selection-change="handleRowCheckboxChange"
|
@selection-change="handleRowCheckboxChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
|
||||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||||
<span v-else>-</span>
|
{{ scope.row.name || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="标签类型" align="center" prop="type" width="90">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ typeLabelMap[scope.row.type] || scope.row.type || '产品' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||||
@@ -107,6 +118,13 @@ defineOptions({ name: 'ProductCustomTag' })
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const typeLabelMap: Record<string, string> = {
|
||||||
|
product: '产品',
|
||||||
|
store: '门店',
|
||||||
|
supplier: '供货商',
|
||||||
|
member: '会员'
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const list = ref<CustomTag[]>([])
|
const list = ref<CustomTag[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
<div class="kpi-value">{{ kpi.value }}</div>
|
<div class="kpi-value">{{ kpi.value }}</div>
|
||||||
<div class="kpi-footer">
|
<div class="kpi-footer">
|
||||||
<span class="kpi-trend" :class="kpi.trendClass">{{ kpi.trendText }}</span>
|
<span class="kpi-trend" :class="kpi.trendClass">{{ kpi.trendText }}</span>
|
||||||
|
<el-tooltip :content="kpi.desc" placement="top" :show-after="300" class="kpi-desc-tooltip">
|
||||||
<span class="kpi-desc">{{ kpi.desc }}</span>
|
<span class="kpi-desc">{{ kpi.desc }}</span>
|
||||||
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<!-- 迷你柱状图 -->
|
<!-- 迷你柱状图 -->
|
||||||
<div v-if="kpi.miniBars" class="mini-bars">
|
<div v-if="kpi.miniBars" class="mini-bars">
|
||||||
@@ -77,7 +79,7 @@ defineProps<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kpi-value {
|
.kpi-value {
|
||||||
font-size: 32px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
@@ -90,8 +92,10 @@ defineProps<{
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.kpi-trend {
|
.kpi-trend {
|
||||||
|
flex-shrink: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&.trend-up {
|
&.trend-up {
|
||||||
@@ -103,7 +107,16 @@ defineProps<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kpi-desc-tooltip {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.kpi-desc {
|
.kpi-desc {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,11 +162,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
|
||||||
const REPORT_ID = 6
|
const REPORT_ID = 6
|
||||||
|
|
||||||
@@ -190,7 +191,10 @@ interface MiddleClassItem {
|
|||||||
defineOptions({ name: 'MiddleClassRanking' })
|
defineOptions({ name: 'MiddleClassRanking' })
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { setPageLoading } = useAiAssistant()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
watch(loading, (v) => setPageLoading(v), { immediate: true })
|
||||||
|
onUnmounted(() => setPageLoading(false))
|
||||||
const dateRange = ref<[string, string] | null>([
|
const dateRange = ref<[string, string] | null>([
|
||||||
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||||
dayjs().format('YYYY-MM-DD')
|
dayjs().format('YYYY-MM-DD')
|
||||||
|
|||||||
@@ -520,13 +520,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute, useRouter } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
|
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
|
||||||
const REPORT_ID = 6
|
const REPORT_ID = 6
|
||||||
|
|
||||||
@@ -585,7 +586,10 @@ defineOptions({ name: 'ProductCardsPage' })
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { setPageLoading } = useAiAssistant()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
watch(loading, (v) => setPageLoading(v), { immediate: true })
|
||||||
|
onUnmounted(() => setPageLoading(false))
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const username = computed(() => userStore.user?.username || '')
|
const username = computed(() => userStore.user?.username || '')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="supplier-performance-page">
|
<div ref="pageRootRef" class="supplier-performance-page">
|
||||||
<!-- 查询条件区域:与主页一致 -->
|
<!-- 查询条件区域:与主页一致 -->
|
||||||
<el-card class="query-card" shadow="never">
|
<el-card class="query-card" shadow="never">
|
||||||
<div class="query-form">
|
<div class="query-form">
|
||||||
@@ -212,6 +212,11 @@
|
|||||||
</el-radio-button>
|
</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="view-toolbar-right">
|
||||||
|
<el-button link type="primary" size="small" @click="openPromptEdit">
|
||||||
|
<Icon icon="ep:edit" /> 编辑提示词
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标签分类筛选:与主页供应商表现一致 -->
|
<!-- 标签分类筛选:与主页供应商表现一致 -->
|
||||||
@@ -260,7 +265,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 指标表格:三列网格布局 -->
|
<!-- 指标表格:三列网格布局 -->
|
||||||
<div v-if="getColumnsWithDate(row).length > 0" class="metric-section">
|
<div v-if="getColumnsWithDate().length > 0" class="metric-section">
|
||||||
<div class="grid-layout metric-header">
|
<div class="grid-layout metric-header">
|
||||||
<span>检测指标</span>
|
<span>检测指标</span>
|
||||||
<span>实际结果</span>
|
<span>实际结果</span>
|
||||||
@@ -294,17 +299,24 @@
|
|||||||
|
|
||||||
<!-- 底部 -->
|
<!-- 底部 -->
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<span class="footer-label"> </span>
|
<el-button
|
||||||
<span class="footer-link">详情 →</span>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表形式(不需要悬浮卡片) -->
|
<!-- 列表形式:每个字段独立一列铺开展示 -->
|
||||||
<div v-show="displayMode === 'table'" class="table-wrap">
|
<div v-show="displayMode === 'table'" class="table-wrap">
|
||||||
<el-table :data="visibleRows" v-loading="loading" border stripe style="width: 100%">
|
<el-table :data="visibleRows" v-loading="loading" border stripe style="width: 100%">
|
||||||
<el-table-column label="供应商" min-width="220" show-overflow-tooltip>
|
<el-table-column label="供应商" min-width="180" fixed="left" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="table-title">
|
<div class="table-title">
|
||||||
<div class="table-title-main">{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}</div>
|
<div class="table-title-main">{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}</div>
|
||||||
@@ -321,24 +333,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="核心指标" width="160" align="center">
|
<el-table-column label="核心指标名称" min-width="120" align="center" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">{{ getTitleName(row) || '-' }}</template>
|
||||||
<div class="table-core">
|
|
||||||
<div class="core-name">{{ getTitleName(row) || '-' }}</div>
|
|
||||||
<div class="core-value">{{ getTitleValue(row) || '-' }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="检测指标明细" min-width="420">
|
<el-table-column label="核心指标值" min-width="100" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ getTitleValue(row) || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template v-for="(col, ci) in metricColumns" :key="col.key || ci">
|
||||||
|
<el-table-column :label="col.title + '(实际)'" min-width="110" align="center" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="metric-cell">
|
|
||||||
<div
|
<div
|
||||||
v-for="(item, mi) in getTableData(row)"
|
v-for="(item, mi) in getMetricItemAt(row, ci)"
|
||||||
:key="mi"
|
:key="item.metricName + '-' + mi"
|
||||||
class="metric-cell-row"
|
class="table-metric-actual"
|
||||||
|
:class="item.valueColorClass"
|
||||||
>
|
>
|
||||||
<div class="metric-cell-name">{{ item.metricName }}</div>
|
|
||||||
<div class="metric-cell-actual" :class="item.valueColorClass">
|
|
||||||
{{ item.actualValue }}
|
{{ item.actualValue }}
|
||||||
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
||||||
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
||||||
@@ -352,25 +361,44 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-cell-ref">{{ item.paramValue }}</div>
|
</template>
|
||||||
</div>
|
</el-table-column>
|
||||||
|
<el-table-column :label="col.title + '(基准)'" min-width="100" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-for="(item, mi) in getMetricItemAt(row, ci)" :key="item.metricName + '-' + mi" class="table-metric-ref">
|
||||||
|
{{ item.paramValue }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
</template>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<AiPromptEditDialog
|
||||||
|
v-model="promptEditVisible"
|
||||||
|
:module-key="SUPPLIER_PERFORMANCE_MODULE_KEY"
|
||||||
|
module-name="供应商表现"
|
||||||
|
:initial-prompt="promptMap[SUPPLIER_PERFORMANCE_MODULE_KEY]"
|
||||||
|
@saved="loadPromptMap"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'SupplierPerformance' })
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
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 { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
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 {
|
interface ColumnCfg {
|
||||||
title: string
|
title: string
|
||||||
@@ -380,10 +408,59 @@ interface ColumnCfg {
|
|||||||
colorKey?: string
|
colorKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SUPPLIER_PERFORMANCE_COMPONENT = 'SupplierPerformance'
|
||||||
|
const SUPPLIER_PERFORMANCE_MODULE_KEY = `${SUPPLIER_PERFORMANCE_COMPONENT}:main`
|
||||||
|
|
||||||
const REPORT_ID = 6
|
const REPORT_ID = 6
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const { openWithScreenshot, setPageLoading, setPageModuleName, setPageModuleCode, setScreenshotTarget } =
|
||||||
|
useAiAssistant()
|
||||||
|
const pageRootRef = ref<HTMLElement | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
watch(loading, (v) => setPageLoading(v), { immediate: true })
|
||||||
|
onMounted(() => {
|
||||||
|
setPageModuleName('供应商表现')
|
||||||
|
setPageModuleCode(SUPPLIER_PERFORMANCE_MODULE_KEY)
|
||||||
|
nextTick(() => {
|
||||||
|
if (pageRootRef.value) setScreenshotTarget(pageRootRef.value)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
setPageLoading(false)
|
||||||
|
setPageModuleName(null)
|
||||||
|
setPageModuleCode(null)
|
||||||
|
setScreenshotTarget(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
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: '供应商表现',
|
||||||
|
moduleCode: SUPPLIER_PERFORMANCE_MODULE_KEY,
|
||||||
|
prompt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 展示形式:卡片 | 列表 */
|
/** 展示形式:卡片 | 列表 */
|
||||||
const displayMode = ref<'card' | 'table'>('card')
|
const displayMode = ref<'card' | 'table'>('card')
|
||||||
@@ -392,10 +469,6 @@ const rows = ref<any[]>([])
|
|||||||
/** 标签分类筛选(与主页供应商表现一致) */
|
/** 标签分类筛选(与主页供应商表现一致) */
|
||||||
const labelFilter = ref<string>('all')
|
const labelFilter = ref<string>('all')
|
||||||
|
|
||||||
const username = computed(() => {
|
|
||||||
return userStore.user?.username || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 查询参数(与主页一致)
|
// 查询参数(与主页一致)
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
rq: dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
rq: dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||||
@@ -602,51 +675,35 @@ function mapKehuToOptions(list: any): { label: string; value: string }[] {
|
|||||||
if (!Array.isArray(list)) return []
|
if (!Array.isArray(list)) return []
|
||||||
return list.map((item: any) => {
|
return list.map((item: any) => {
|
||||||
const label = item?.khmc ?? item?.CKMC ?? item?.KHMC ?? item?.DLMC ?? item?.label ?? item?.name ?? ''
|
const label = item?.khmc ?? item?.CKMC ?? item?.KHMC ?? item?.DLMC ?? item?.label ?? item?.name ?? ''
|
||||||
const value = item?.khdm != null ? String(item.khdm) : item?.CKDM != null ? String(item.CKDM) : item?.KHDM != null ? String(item.KHDM) : item?.DLDM != null ? String(item.DLDM) : item?.value != null ? String(item.value) : item?.code != null ? String(item.code) : ''
|
const rawVal = item?.khdm ?? item?.CKDM ?? item?.KHDM ?? item?.DLDM ?? item?.value ?? item?.code
|
||||||
|
const value = rawVal != null ? String(rawVal) : ''
|
||||||
return { label, value }
|
return { label, value }
|
||||||
}).filter((o) => o.label && o.value)
|
}).filter((o) => o.label && o.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拉取下拉选项
|
// 拉取下拉选项(通用)
|
||||||
async function fetchBrandOptions() {
|
const OPTION_TABLE_MAP: Array<{
|
||||||
try {
|
tableName: string
|
||||||
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'pinpai' })
|
target: ReturnType<typeof ref<{ label: string; value: string }[]>>
|
||||||
brandOptions.value = mapPinpaiToOptions(resolveTableList(res))
|
mapper: (list: any) => { label: string; value: string }[]
|
||||||
} catch (_) {
|
}> = [
|
||||||
brandOptions.value = []
|
{ tableName: 'pinpai', target: brandOptions, mapper: mapPinpaiToOptions },
|
||||||
}
|
{ tableName: 'jijie', target: seasonOptions, mapper: mapJijieToOptions },
|
||||||
}
|
{ tableName: 'dalei', target: categoryOptions, mapper: mapDaleiToOptions },
|
||||||
|
{ tableName: 'kehu', target: storeOptions, mapper: mapKehuToOptions }
|
||||||
|
]
|
||||||
|
|
||||||
async function fetchSeasonOptions() {
|
async function fetchAllOptions() {
|
||||||
|
await Promise.all(
|
||||||
|
OPTION_TABLE_MAP.map(async ({ tableName, target, mapper }) => {
|
||||||
try {
|
try {
|
||||||
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'jijie' })
|
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName })
|
||||||
seasonOptions.value = mapJijieToOptions(resolveTableList(res))
|
target.value = mapper(resolveTableList(res))
|
||||||
} catch (_) {
|
} catch {
|
||||||
seasonOptions.value = []
|
target.value = []
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
)
|
||||||
async function fetchCategoryOptions() {
|
|
||||||
try {
|
|
||||||
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'dalei' })
|
|
||||||
categoryOptions.value = mapDaleiToOptions(resolveTableList(res))
|
|
||||||
} catch (_) {
|
|
||||||
categoryOptions.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchStoreOptions() {
|
|
||||||
try {
|
|
||||||
const res = await ReportApi.executeTable({ reportId: REPORT_ID, tableName: 'kehu' })
|
|
||||||
storeOptions.value = mapKehuToOptions(resolveTableList(res))
|
|
||||||
} catch (_) {
|
|
||||||
storeOptions.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 线路选项:暂不查后端,使用空默认值 */
|
|
||||||
function fetchLineOptions() {
|
|
||||||
lineOptions.value = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询条件默认值:门店默认全选,品牌有数据且含 1 则默认 1
|
// 查询条件默认值:门店默认全选,品牌有数据且含 1 则默认 1
|
||||||
@@ -666,6 +723,9 @@ const columnsSorted = computed(() => {
|
|||||||
return cols.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
return cols.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 列表表格:检测指标列(排除第一列 title) */
|
||||||
|
const metricColumns = computed(() => columnsSorted.value.slice(1))
|
||||||
|
|
||||||
/** 从 rows 中收集所有 label/tag(按逗号拆分)去重后作为筛选项,首项为「全部」 */
|
/** 从 rows 中收集所有 label/tag(按逗号拆分)去重后作为筛选项,首项为「全部」 */
|
||||||
const labelOpts = computed(() => {
|
const labelOpts = computed(() => {
|
||||||
const labels = new Set<string>()
|
const labels = new Set<string>()
|
||||||
@@ -715,9 +775,8 @@ function parseLabelColorPairs(labelStr: unknown, colorStr: unknown): { label: st
|
|||||||
return labels.map((label, i) => ({ label, color: colors[i] ?? 'flat' }))
|
return labels.map((label, i) => ({ label, color: colors[i] ?? 'flat' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatVal(val: unknown, key: string): string {
|
function formatVal(val: unknown): string {
|
||||||
if (val == null) return '-'
|
if (val == null) return '-'
|
||||||
// 后端 value1/value2/value3 多为字符串(含 %),直接展示;数值则按千分位
|
|
||||||
if (typeof val === 'number') return val.toLocaleString('zh-CN')
|
if (typeof val === 'number') return val.toLocaleString('zh-CN')
|
||||||
return String(val)
|
return String(val)
|
||||||
}
|
}
|
||||||
@@ -769,16 +828,16 @@ function getTitleValue(row: any): string {
|
|||||||
return v != null ? String(v).trim() : '-'
|
return v != null ? String(v).trim() : '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取数据列(排除第一列 title):按接口列配置全部展示,不要求 date 非空 */
|
/** 获取数据列(排除第一列 title) */
|
||||||
function getColumnsWithDate(row: any): ColumnCfg[] {
|
function getColumnsWithDate(): ColumnCfg[] {
|
||||||
return columnsSorted.value.slice(1)
|
return columnsSorted.value.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成表格数据:按列配置渲染,实际结果取 row[key],基准参考取 row[key+date] 或 '-' */
|
/** 生成表格数据:按列配置渲染,实际结果取 row[key],基准参考取 row[key+date] 或 '-' */
|
||||||
function getTableData(row: any): Array<{ metricName: string; actualValue: string; paramValue: string; valueColorClass?: string; arrow?: string; arrowClass?: string; labelPairs?: Array<{ label: string; color: string }> }> {
|
function getTableData(row: any): Array<{ metricName: string; actualValue: string; paramValue: string; valueColorClass?: string; arrow?: string; arrowClass?: string; labelPairs?: Array<{ label: string; color: string }> }> {
|
||||||
const colsWithDate = getColumnsWithDate(row)
|
const colsWithDate = getColumnsWithDate()
|
||||||
return colsWithDate.map((col) => {
|
return colsWithDate.map((col) => {
|
||||||
const actualVal = formatVal((row as any)?.[col.key], col.key)
|
const actualVal = formatVal((row as any)?.[col.key])
|
||||||
const dateVal = (row as any)?.[col.key + 'date']
|
const dateVal = (row as any)?.[col.key + 'date']
|
||||||
const paramVal = dateVal != null && String(dateVal).trim() !== '' ? String(dateVal).trim() : '-'
|
const paramVal = dateVal != null && String(dateVal).trim() !== '' ? String(dateVal).trim() : '-'
|
||||||
|
|
||||||
@@ -827,6 +886,13 @@ function getTableData(row: any): Array<{ metricName: string; actualValue: string
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取某行第 ci 个指标项(用于列表表格按列展示),返回数组供 v-for 使用 */
|
||||||
|
function getMetricItemAt(row: any, ci: number) {
|
||||||
|
const data = getTableData(row)
|
||||||
|
const item = data[ci]
|
||||||
|
return item ? [item] : []
|
||||||
|
}
|
||||||
|
|
||||||
function parseResponseData(raw: unknown): { columns: ColumnCfg[]; list: any[] } {
|
function parseResponseData(raw: unknown): { columns: ColumnCfg[]; list: any[] } {
|
||||||
if (Array.isArray(raw)) {
|
if (Array.isArray(raw)) {
|
||||||
// 二维数组 [[列], [list]]
|
// 二维数组 [[列], [list]]
|
||||||
@@ -860,8 +926,6 @@ function parseResponseData(raw: unknown): { columns: ColumnCfg[]; list: any[] }
|
|||||||
const handleQuery = async () => {
|
const handleQuery = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 确保 username 有值:优先使用 computed,如果为空则直接从 userStore 获取
|
|
||||||
const currentUsername = username.value || userStore.user?.username || ''
|
|
||||||
const res = await ReportApi.getCategoryPerformance({
|
const res = await ReportApi.getCategoryPerformance({
|
||||||
reportId: REPORT_ID,
|
reportId: REPORT_ID,
|
||||||
name: 'YDY_AI_GET_GHSZX',
|
name: 'YDY_AI_GET_GHSZX',
|
||||||
@@ -872,7 +936,7 @@ const handleQuery = async () => {
|
|||||||
dalei: arrToQuery(queryParams.category),
|
dalei: arrToQuery(queryParams.category),
|
||||||
jj: arrToQuery(queryParams.season),
|
jj: arrToQuery(queryParams.season),
|
||||||
p: '123',
|
p: '123',
|
||||||
username: currentUsername,
|
username: userStore.user?.username || '',
|
||||||
ghsdm: supplierCode.value || undefined
|
ghsdm: supplierCode.value || undefined
|
||||||
})
|
})
|
||||||
const raw = (res as any)?.data ?? res
|
const raw = (res as any)?.data ?? res
|
||||||
@@ -906,27 +970,9 @@ const handleReset = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 等待下一个 tick,确保路由参数完全加载
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// 先初始化路由参数
|
|
||||||
initQueryParamsFromRoute()
|
initQueryParamsFromRoute()
|
||||||
|
await Promise.all([loadPromptMap(), fetchAllOptions()])
|
||||||
// 并行获取下拉选项数据(这些不依赖路由参数,用于填充下拉框)
|
|
||||||
// 注意:这些调用会立即执行,但它们是获取下拉选项数据,不是主查询
|
|
||||||
await Promise.all([
|
|
||||||
fetchBrandOptions(),
|
|
||||||
fetchSeasonOptions(),
|
|
||||||
fetchCategoryOptions(),
|
|
||||||
fetchLineOptions(),
|
|
||||||
fetchStoreOptions()
|
|
||||||
])
|
|
||||||
|
|
||||||
// 设置默认值
|
|
||||||
applyQueryDefaults()
|
applyQueryDefaults()
|
||||||
|
|
||||||
// 最后执行主查询(此时路由参数已经读取完成,包括 code 参数)
|
|
||||||
// 主查询会使用 supplierCode.value(从路由参数中读取的 code)作为 ghsdm 参数
|
|
||||||
handleQuery()
|
handleQuery()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -952,6 +998,10 @@ onMounted(async () => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.view-toolbar-right {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.view-switch {
|
.view-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1148,44 +1198,9 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-core {
|
.table-metric-actual,
|
||||||
display: flex;
|
.table-metric-ref {
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.core-name {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-value {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-cell {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-cell-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 160px 1fr 120px;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-cell-name {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-cell-actual {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1212,21 +1227,18 @@ onMounted(async () => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
&.badge-danger,
|
|
||||||
&.label-trend-up {
|
&.label-trend-up {
|
||||||
background: #fee2e2;
|
background: #fee2e2;
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
border: 1px solid #fecaca;
|
border: 1px solid #fecaca;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.badge-warn,
|
|
||||||
&.label-trend-flat {
|
&.label-trend-flat {
|
||||||
background: #fef3c7;
|
background: #fef3c7;
|
||||||
color: #d97706;
|
color: #d97706;
|
||||||
border: 1px solid #fde68a;
|
border: 1px solid #fde68a;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.badge-success,
|
|
||||||
&.label-trend-down {
|
&.label-trend-down {
|
||||||
background: #dcfce7;
|
background: #dcfce7;
|
||||||
color: #22c55e;
|
color: #22c55e;
|
||||||
@@ -1235,12 +1247,22 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-cell-ref {
|
.table-metric-actual {
|
||||||
font-size: 12px;
|
&.label-trend-up {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-trend-down {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-trend-flat {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-metric-ref {
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
text-align: right;
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
@@ -1469,36 +1491,38 @@ onMounted(async () => {
|
|||||||
|
|
||||||
/* 底部 */
|
/* 底部 */
|
||||||
.card-footer {
|
.card-footer {
|
||||||
margin-top: 16px;
|
margin-top: 20px;
|
||||||
padding-top: 16px;
|
padding: 18px 8px 0;
|
||||||
border-top: 2px solid var(--el-border-color-lighter);
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: flex-end;
|
||||||
padding: 0 8px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-label {
|
.ai-analysis-btn {
|
||||||
font-size: 10px;
|
font-size: 14px !important;
|
||||||
color: var(--el-text-color-placeholder);
|
font-weight: 600 !important;
|
||||||
font-weight: 900;
|
letter-spacing: 0.5px;
|
||||||
text-transform: uppercase;
|
padding: 10px 24px !important;
|
||||||
letter-spacing: 2px;
|
border-radius: 12px !important;
|
||||||
font-style: italic;
|
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 {
|
.ai-analysis-btn:hover {
|
||||||
font-size: 10px;
|
background: linear-gradient(135deg, var(--el-color-primary-light-3) 0%, var(--el-color-primary) 100%) !important;
|
||||||
color: #3b82f6;
|
transform: translateY(-2px);
|
||||||
font-weight: 900;
|
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.32);
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
color: #2563eb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
|||||||
@@ -168,11 +168,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
|
||||||
const REPORT_ID = 6
|
const REPORT_ID = 6
|
||||||
|
|
||||||
@@ -195,7 +196,10 @@ interface SupplierContributionData {
|
|||||||
defineOptions({ name: 'SupplierRanking' })
|
defineOptions({ name: 'SupplierRanking' })
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const { setPageLoading } = useAiAssistant()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
watch(loading, (v) => setPageLoading(v), { immediate: true })
|
||||||
|
onUnmounted(() => setPageLoading(false))
|
||||||
const dateRange = ref<[string, string] | null>([
|
const dateRange = ref<[string, string] | null>([
|
||||||
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
||||||
dayjs().format('YYYY-MM-DD')
|
dayjs().format('YYYY-MM-DD')
|
||||||
|
|||||||
@@ -371,13 +371,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Search, Grid, List } from '@element-plus/icons-vue'
|
import { Search, Grid, List } from '@element-plus/icons-vue'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
|
||||||
defineOptions({ name: 'CategoryCardListComponents' })
|
defineOptions({ name: 'CategoryCardListComponents' })
|
||||||
|
|
||||||
@@ -472,7 +473,10 @@ const getTimeRange = (range: string) => {
|
|||||||
return { rq: start.format('YYYY-MM-DD'), rq2: end.format('YYYY-MM-DD') }
|
return { rq: start.format('YYYY-MM-DD'), rq2: end.format('YYYY-MM-DD') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { setPageLoading } = useAiAssistant()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
watch(loading, (v) => setPageLoading(v), { immediate: true })
|
||||||
|
onUnmounted(() => setPageLoading(false))
|
||||||
/** 展示形式:卡片 | 列表 */
|
/** 展示形式:卡片 | 列表 */
|
||||||
const displayMode = ref<'card' | 'table'>('card')
|
const displayMode = ref<'card' | 'table'>('card')
|
||||||
const columns = ref<{ title: string; key: string; order?: number; labelKey?: string; colorKey?: string }[]>([])
|
const columns = ref<{ title: string; key: string; order?: number; labelKey?: string; colorKey?: string }[]>([])
|
||||||
|
|||||||
@@ -434,13 +434,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
|
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
import type { ProductDetailData, ProductSizeStatus, UiStatus } from './index.vue'
|
import type { ProductDetailData, ProductSizeStatus, UiStatus } from './index.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ProductDetailPage' })
|
defineOptions({ name: 'ProductDetailPage' })
|
||||||
@@ -449,7 +450,10 @@ const REPORT_ID = 6
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const { setPageLoading } = useAiAssistant()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
watch(loading, (v) => setPageLoading(v), { immediate: true })
|
||||||
|
onUnmounted(() => setPageLoading(false))
|
||||||
const productList = ref<ProductDetailData[]>([])
|
const productList = ref<ProductDetailData[]>([])
|
||||||
/** 标签分类筛选 */
|
/** 标签分类筛选 */
|
||||||
const labelFilter = ref<string>('all')
|
const labelFilter = ref<string>('all')
|
||||||
|
|||||||
@@ -196,7 +196,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- KPI 卡片区域(加载中显示特效) -->
|
<!-- KPI 卡片区域 -->
|
||||||
|
<div class="kpi-section">
|
||||||
|
<div class="module-header">
|
||||||
|
<span class="module-title">KPI 指标</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="kpi-grid"
|
class="kpi-grid"
|
||||||
v-loading="loadingKpi"
|
v-loading="loadingKpi"
|
||||||
@@ -211,7 +215,9 @@
|
|||||||
<div v-if="kpi.value != null && kpi.value !== ''" class="kpi-value">{{ kpi.value }}</div>
|
<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">
|
<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="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>
|
||||||
<!-- 迷你柱状图 -->
|
<!-- 迷你柱状图 -->
|
||||||
<div v-if="kpi.miniBars" class="mini-bars">
|
<div v-if="kpi.miniBars" class="mini-bars">
|
||||||
@@ -238,10 +244,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 供应商表现 (Top 10) + 中类销售排名 Top 5 -->
|
<!-- 供应商表现 (Top 10) + 中类销售排名 Top 5 -->
|
||||||
<div class="middle-section">
|
<div class="middle-section">
|
||||||
<!-- 供应商表现 (Top 10):KBStyle 筛选 + 紧凑表格 + 展开更多 -->
|
<!-- 供应商表现 (Top 10) -->
|
||||||
<el-card
|
<el-card
|
||||||
class="kb-cat-card"
|
class="kb-cat-card"
|
||||||
shadow="never"
|
shadow="never"
|
||||||
@@ -253,10 +260,12 @@
|
|||||||
<el-icon class="kb-title-icon"><StarFilled /></el-icon>
|
<el-icon class="kb-title-icon"><StarFilled /></el-icon>
|
||||||
<h3 class="kb-card-title">供应商表现 (Top 10)</h3>
|
<h3 class="kb-card-title">供应商表现 (Top 10)</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
<el-button type="primary" link size="small" @click="handleViewMoreCategories" class="more-button">
|
<el-button type="primary" link size="small" @click="handleViewMoreCategories" class="more-button">
|
||||||
更多 <el-icon><ArrowRight /></el-icon>
|
更多 <el-icon><ArrowRight /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="kb-card-content">
|
<div class="kb-card-content">
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<div
|
<div
|
||||||
@@ -489,6 +498,7 @@
|
|||||||
<el-icon class="category-title-icon"><StarFilled /></el-icon>
|
<el-icon class="category-title-icon"><StarFilled /></el-icon>
|
||||||
<h3 class="category-card-title">中类销售排名 Top 5</h3>
|
<h3 class="category-card-title">中类销售排名 Top 5</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@@ -500,6 +510,7 @@
|
|||||||
<el-icon><ArrowRight /></el-icon>
|
<el-icon><ArrowRight /></el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="category-pie-wrap">
|
<div class="category-pie-wrap">
|
||||||
<Echart
|
<Echart
|
||||||
v-if="categoryTop6.length > 0"
|
v-if="categoryTop6.length > 0"
|
||||||
@@ -542,7 +553,7 @@
|
|||||||
row-class-name="product-list-row-clickable"
|
row-class-name="product-list-row-clickable"
|
||||||
@row-click="handleProductRowClick"
|
@row-click="handleProductRowClick"
|
||||||
>
|
>
|
||||||
<el-table-column prop="productInfo" label="商品信息" width="280">
|
<el-table-column prop="productInfo" label="商品信息" width="200" fixed="left">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-popover
|
<el-popover
|
||||||
placement="right"
|
placement="right"
|
||||||
@@ -553,25 +564,19 @@
|
|||||||
@show="handleProductHoverShow(row)"
|
@show="handleProductHoverShow(row)"
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<div class="product-info">
|
<div class="product-info product-info-compact">
|
||||||
<div class="product-image">
|
<div class="product-image">
|
||||||
<img v-if="row.imageUrl" :src="row.imageUrl" alt="商品图片" class="product-img" />
|
<img v-if="row.imageUrl" :src="row.imageUrl" alt="商品图片" class="product-img" />
|
||||||
<el-icon v-else><Picture /></el-icon>
|
<el-icon v-else><Picture /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-details">
|
<div class="product-details">
|
||||||
<div class="product-code">
|
<div class="product-name">{{ row.name || '-' }}</div>
|
||||||
款号:
|
<div
|
||||||
<span
|
|
||||||
class="product-code-link"
|
class="product-code-link"
|
||||||
@click.stop="handleProductCodeClick(row)"
|
@click.stop="handleProductCodeClick(row)"
|
||||||
>
|
>
|
||||||
{{ getProductCode(row) }}
|
{{ getProductCode(row) }}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="product-code">条码: {{ row.barcode || '-' }}</div>
|
|
||||||
<div class="product-code">颜色: {{ row.color }}</div>
|
|
||||||
<div class="product-code">进价: ¥{{ formatNumber(row.purchasePrice || 0) }}</div>
|
|
||||||
<div class="product-code">售价: ¥{{ formatNumber(row.sellingPrice || 0) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -707,13 +712,37 @@
|
|||||||
</el-popover>
|
</el-popover>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<!-- 第二列:商品属性 -->
|
<!-- 商品属性拆开列展示 -->
|
||||||
<el-table-column prop="category" label="商品属性" width="200" show-overflow-tooltip>
|
<el-table-column prop="code" label="款号" min-width="100" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div>{{ row.category }}</div>
|
<span
|
||||||
|
class="product-code-link"
|
||||||
|
@click.stop="handleProductCodeClick(row)"
|
||||||
|
>
|
||||||
|
{{ getProductCode(row) }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<!-- 第三列:销售数据 -->
|
<el-table-column prop="barcode" label="条码" min-width="120" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.barcode || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="color" label="颜色" min-width="80" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.color || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="category" label="类目" min-width="120" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.category || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="purchasePrice" label="进价" min-width="90" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.purchasePrice != null ? '¥' + formatNumber(row.purchasePrice) : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sellingPrice" label="售价" min-width="90" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.sellingPrice != null ? '¥' + formatNumber(row.sellingPrice) : '-' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- 销售数据 -->
|
||||||
<el-table-column prop="ls" label="销售数据" align="right" width="150">
|
<el-table-column prop="ls" label="销售数据" align="right" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
|
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
|
||||||
@@ -799,7 +828,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 dayjs from 'dayjs'
|
||||||
import type { EChartsOption } from 'echarts'
|
import type { EChartsOption } from 'echarts'
|
||||||
import Echart from '@/components/Echart/src/Echart.vue'
|
import Echart from '@/components/Echart/src/Echart.vue'
|
||||||
@@ -817,6 +846,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { Icon } from '@/components/Icon'
|
import { Icon } from '@/components/Icon'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
|
import type { CategoryDiagnosticData } from './components/categoryCardListComponents.vue'
|
||||||
|
import { useAiAssistant } from '@/components/AiAssistant/useAiAssistant'
|
||||||
|
|
||||||
defineOptions({ name: 'ProductDashboard' })
|
defineOptions({ name: 'ProductDashboard' })
|
||||||
|
|
||||||
@@ -1233,6 +1263,15 @@ const loadingCategory = ref(false)
|
|||||||
const loadingPie = ref(false)
|
const loadingPie = ref(false)
|
||||||
/** 商品明细列表加载状态 */
|
/** 商品明细列表加载状态 */
|
||||||
const loadingProductList = 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 searchKeyword = ref('')
|
||||||
const hoverDetail = ref<ProductDetailData | null>(null)
|
const hoverDetail = ref<ProductDetailData | null>(null)
|
||||||
|
|
||||||
@@ -2861,7 +2900,7 @@ onDeactivated(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.kpi-value {
|
.kpi-value {
|
||||||
font-size: 32px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
@@ -2874,8 +2913,10 @@ onDeactivated(() => {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.kpi-trend {
|
.kpi-trend {
|
||||||
|
flex-shrink: 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&.up {
|
&.up {
|
||||||
@@ -2891,7 +2932,16 @@ onDeactivated(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kpi-desc-tooltip {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.kpi-desc {
|
.kpi-desc {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-text-color-placeholder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3554,6 +3604,17 @@ onDeactivated(() => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&.product-info-compact .product-details {
|
||||||
|
.product-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.product-code-link {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
@@ -4197,4 +4258,5 @@ onDeactivated(() => {
|
|||||||
.kb22-legend-item { display: flex; align-items: center; gap: 4px; font-size: 9px; color: var(--el-text-color-placeholder); }
|
.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%; }
|
.kb22-dot { width: 6px; height: 6px; border-radius: 50%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
:inline="true"
|
:inline="true"
|
||||||
label-width="90px"
|
label-width="90px"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="标签类型" prop="type">
|
||||||
|
<el-select v-model="queryParams.type" placeholder="标签类型" class="!w-140px" disabled>
|
||||||
|
<el-option label="门店" value="store" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="标签名称" prop="name">
|
<el-form-item label="标签名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.name"
|
v-model="queryParams.name"
|
||||||
@@ -62,14 +67,20 @@
|
|||||||
@selection-change="handleRowCheckboxChange"
|
@selection-change="handleRowCheckboxChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
|
||||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||||
<span v-else>-</span>
|
{{ scope.row.name || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="标签类型" align="center" prop="type" width="90">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ typeLabelMap[scope.row.type] || scope.row.type || '门店' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||||
@@ -107,6 +118,13 @@ defineOptions({ name: 'StoreCustomTag' })
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const typeLabelMap: Record<string, string> = {
|
||||||
|
product: '产品',
|
||||||
|
store: '门店',
|
||||||
|
supplier: '供货商',
|
||||||
|
member: '会员'
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const list = ref<CustomTag[]>([])
|
const list = ref<CustomTag[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
:inline="true"
|
:inline="true"
|
||||||
label-width="90px"
|
label-width="90px"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="标签类型" prop="type">
|
||||||
|
<el-select v-model="queryParams.type" placeholder="标签类型" class="!w-140px" disabled>
|
||||||
|
<el-option label="供货商" value="supplier" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="标签名称" prop="name">
|
<el-form-item label="标签名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.name"
|
v-model="queryParams.name"
|
||||||
@@ -62,14 +67,20 @@
|
|||||||
@selection-change="handleRowCheckboxChange"
|
@selection-change="handleRowCheckboxChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" />
|
<el-table-column type="selection" width="55" />
|
||||||
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true">
|
||||||
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
|
||||||
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">{{ scope.row.color }}</el-tag>
|
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small" class="!border-0">
|
||||||
<span v-else>-</span>
|
{{ scope.row.name || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>{{ scope.row.name || '-' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="标签类型" align="center" prop="type" width="90">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ typeLabelMap[scope.row.type] || scope.row.type || '供货商' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||||
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
<el-table-column label="代入参数" align="center" prop="useParams" width="90">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
<el-tag v-if="scope.row.useParams === true || scope.row.useParams === 1" type="success" size="small">是</el-tag>
|
||||||
@@ -107,6 +118,13 @@ defineOptions({ name: 'SupplierCustomTag' })
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const typeLabelMap: Record<string, string> = {
|
||||||
|
product: '产品',
|
||||||
|
store: '门店',
|
||||||
|
supplier: '供货商',
|
||||||
|
member: '会员'
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const list = ref<CustomTag[]>([])
|
const list = ref<CustomTag[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
192
src/views/ydoyun/tagconfig/TagForm.vue
Normal file
192
src/views/ydoyun/tagconfig/TagForm.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="120px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="标签名称" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入标签名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签类型" prop="type">
|
||||||
|
<el-select v-model="formData.type" placeholder="请选择标签类型" class="!w-full">
|
||||||
|
<el-option label="产品基础标签 (product)" value="product" />
|
||||||
|
<el-option label="门店基础标签 (store)" value="store" />
|
||||||
|
<el-option label="供应商基础标签 (supplier)" value="supplier" />
|
||||||
|
<el-option label="会员基础标签 (member)" value="member" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签表达式" prop="expression">
|
||||||
|
<el-input v-model="formData.expression" type="textarea" placeholder="请输入标签表达式" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签颜色" prop="color">
|
||||||
|
<div class="color-selector-row">
|
||||||
|
<div class="dot-group">
|
||||||
|
<span
|
||||||
|
v-for="c in presetColors"
|
||||||
|
:key="c"
|
||||||
|
class="color-dot color-dot-clickable"
|
||||||
|
:class="{ 'color-dot-active': formData.color === c }"
|
||||||
|
:style="{ backgroundColor: c }"
|
||||||
|
:title="'预设色 ' + c"
|
||||||
|
@click="formData.color = c"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<el-color-picker v-model="formData.color" :predefine="presetColors" size="default" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="执行SQL脚本" prop="sqlScript">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.sqlScript"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder="请输入标签同步时执行的SQL脚本,支持多行"
|
||||||
|
class="sql-script-area"
|
||||||
|
/>
|
||||||
|
<div class="form-item-hint">该SQL将用于标签计算与同步,请确保语法正确</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否代入参数" prop="useParams">
|
||||||
|
<el-radio-group v-model="formData.useParams">
|
||||||
|
<el-radio :value="true">是</el-radio>
|
||||||
|
<el-radio :value="false">否</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="参数列表" prop="params">
|
||||||
|
<el-input v-model="formData.params" placeholder="逗号分隔,如:param1,param2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { TagApi } from '@/api/ydoyun/tagconfig'
|
||||||
|
import type { Tag } from '@/api/ydoyun/tagconfig'
|
||||||
|
|
||||||
|
defineOptions({ name: 'TagForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const formLoading = ref(false)
|
||||||
|
const formType = ref('')
|
||||||
|
const formData = ref<Tag>({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
type: undefined,
|
||||||
|
expression: undefined,
|
||||||
|
color: undefined,
|
||||||
|
sqlScript: undefined,
|
||||||
|
useParams: false,
|
||||||
|
params: undefined,
|
||||||
|
})
|
||||||
|
const formRules = reactive({
|
||||||
|
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }],
|
||||||
|
type: [{ required: true, message: '请选择标签类型', trigger: 'change' }],
|
||||||
|
sqlScript: [{ required: true, message: '执行SQL脚本不能为空', trigger: 'blur' }],
|
||||||
|
useParams: [{ required: true, message: '请选择是否代入参数', trigger: 'change' }],
|
||||||
|
})
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
/** 4 个预设颜色(蓝、红、黄、绿) */
|
||||||
|
const presetColors = ['#2f54eb', '#f5222d', '#fa8c16', '#52c41a']
|
||||||
|
|
||||||
|
const open = async (type: string, id?: number, initialType?: string) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = type === 'create' ? '新增标签' : '编辑标签'
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await TagApi.getTag(id)
|
||||||
|
formData.value = (res as any)?.data || {}
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
} else if (type === 'create' && initialType) {
|
||||||
|
formData.value.type = initialType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defineExpose({ open })
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate()
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await TagApi.createTag(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await TagApi.updateTag(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
type: undefined,
|
||||||
|
expression: undefined,
|
||||||
|
color: undefined,
|
||||||
|
sqlScript: undefined,
|
||||||
|
useParams: false,
|
||||||
|
params: undefined,
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.color-selector-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.dot-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.color-dot {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 0 1px #d9d9d9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.color-dot-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
.color-dot-clickable:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
.color-dot-active {
|
||||||
|
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.sql-script-area :deep(textarea) {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.form-item-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
2426
src/views/ydoyun/tagconfig/index.vue
Normal file
2426
src/views/ydoyun/tagconfig/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user