提交 0f44358c authored 作者: 王鹏飞's avatar 王鹏飞

feat: 优化页面样式

上级 55352a26
...@@ -3,7 +3,10 @@ ...@@ -3,7 +3,10 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="https://zws-imgs-pub.ezijing.com/pc/base/favicon.ico" /> <link rel="icon" href="https://zws-imgs-pub.ezijing.com/pc/base/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>数字营销实验室DML</title> <title>数字营销实验室DML</title>
<script src="https://webapp-pub.ezijing.com/screenshot.js" async></script> <script src="https://webapp-pub.ezijing.com/screenshot.js" async></script>
</head> </head>
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
"@dagrejs/dagre": "^1.1.3", "@dagrejs/dagre": "^1.1.3",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@ezijing/ai-core": "^1.0.0", "@ezijing/ai-core": "^1.0.0",
"@ezijing/ai-vue": "^1.0.37", "@ezijing/ai-vue": "^1.0.44",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@remixicon/vue": "^4.7.0", "@remixicon/vue": "^4.7.0",
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
...@@ -754,9 +754,9 @@ ...@@ -754,9 +754,9 @@
} }
}, },
"node_modules/@ezijing/ai-core": { "node_modules/@ezijing/ai-core": {
"version": "1.0.37", "version": "1.0.44",
"resolved": "https://registry.npmjs.org/@ezijing/ai-core/-/ai-core-1.0.37.tgz", "resolved": "https://registry.npmjs.org/@ezijing/ai-core/-/ai-core-1.0.44.tgz",
"integrity": "sha512-LyT97TuYIZmgnRcHChJ06PND4d282hwSH5yCr/3DXPjcZPitogirvfC+I/5/dYsj2LpwpmEK65ips59KGtXt8A==", "integrity": "sha512-Uf65IwIrjLrSwP/SEOpm6LnP6R8C0wOUXdmlaWIZBkKMsx3uz5V8IoL5qVy166xQ+qTD9t66QNTZUchkGC9drQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.11.0", "axios": "^1.11.0",
...@@ -764,12 +764,12 @@ ...@@ -764,12 +764,12 @@
} }
}, },
"node_modules/@ezijing/ai-vue": { "node_modules/@ezijing/ai-vue": {
"version": "1.0.37", "version": "1.0.44",
"resolved": "https://registry.npmjs.org/@ezijing/ai-vue/-/ai-vue-1.0.37.tgz", "resolved": "https://registry.npmjs.org/@ezijing/ai-vue/-/ai-vue-1.0.44.tgz",
"integrity": "sha512-r+Us9aKq932CQ3lzc/uTVSpVbYFTy5VHmaVG8Xz0qeUBTqP11/0Ef/r5U9f3vaSkO+lazPulcv7y7XQNXfjiUw==", "integrity": "sha512-NU3gaSoKB/Rm+nYD16l/NNo2FREqd72RqF6wpmSKFzt+FtZDzL2c0YUwrTYzA0B9+xXwxwD34s4eXRObIFSbbQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ezijing/ai-core": "1.0.37" "@ezijing/ai-core": "1.0.44"
}, },
"peerDependencies": { "peerDependencies": {
"vue": ">=3.0.0" "vue": ">=3.0.0"
...@@ -2617,6 +2617,18 @@ ...@@ -2617,6 +2617,18 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agentkeepalive": { "node_modules/agentkeepalive": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.3.tgz", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.3.tgz",
...@@ -2890,14 +2902,15 @@ ...@@ -2890,14 +2902,15 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.1", "version": "1.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.16.0",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
} }
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
...@@ -4620,12 +4633,12 @@ ...@@ -4620,12 +4633,12 @@
} }
}, },
"node_modules/eventsource-parser": { "node_modules/eventsource-parser": {
"version": "3.0.3", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
"integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/evp_bytestokey": { "node_modules/evp_bytestokey": {
...@@ -4933,15 +4946,16 @@ ...@@ -4933,15 +4946,16 @@
} }
}, },
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.6", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh" "url": "https://github.com/sponsors/RubenVerborgh"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
}, },
...@@ -4961,16 +4975,16 @@ ...@@ -4961,16 +4975,16 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.4", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2", "hasown": "^2.0.4",
"mime-types": "^2.1.12" "mime-types": "^2.1.35"
}, },
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
...@@ -5288,9 +5302,10 @@ ...@@ -5288,9 +5302,10 @@
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
...@@ -5343,6 +5358,19 @@ ...@@ -5343,6 +5358,19 @@
"integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==",
"peer": true "peer": true
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/humanize-ms": { "node_modules/humanize-ms": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
...@@ -7086,9 +7114,13 @@ ...@@ -7086,9 +7114,13 @@
"integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="
}, },
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "2.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
}, },
"node_modules/prr": { "node_modules/prr": {
"version": "1.0.1", "version": "1.0.1",
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
"@dagrejs/dagre": "^1.1.3", "@dagrejs/dagre": "^1.1.3",
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
"@ezijing/ai-core": "^1.0.0", "@ezijing/ai-core": "^1.0.0",
"@ezijing/ai-vue": "^1.0.37", "@ezijing/ai-vue": "^1.0.44",
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@remixicon/vue": "^4.7.0", "@remixicon/vue": "^4.7.0",
"@tinymce/tinymce-vue": "^5.0.1", "@tinymce/tinymce-vue": "^5.0.1",
......
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
body, body,
h1, h1,
h2, h2,
...@@ -158,7 +167,6 @@ textarea:focus { ...@@ -158,7 +167,6 @@ textarea:focus {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
/* el-pagination 移动端简化 */ /* el-pagination 移动端简化 */
.el-pagination { .el-pagination {
flex-wrap: wrap; flex-wrap: wrap;
...@@ -203,12 +211,12 @@ textarea:focus { ...@@ -203,12 +211,12 @@ textarea:focus {
} }
/* 固定宽度容器防溢出 */ /* 固定宽度容器防溢出 */
[style*="width: 200px"], [style*='width: 200px'],
[style*="width: 220px"], [style*='width: 220px'],
[style*="width: 300px"], [style*='width: 300px'],
[style*="width: 340px"], [style*='width: 340px'],
[style*="width: 400px"], [style*='width: 400px'],
[style*="width: 150px"] { [style*='width: 150px'] {
max-width: 100% !important; max-width: 100% !important;
} }
...@@ -218,8 +226,8 @@ textarea:focus { ...@@ -218,8 +226,8 @@ textarea:focus {
.hover-show { .hover-show {
display: block !important; display: block !important;
} }
[class*="remove"]:hover, [class*='remove']:hover,
[class*="delete"]:hover { [class*='delete']:hover {
opacity: 1; opacity: 1;
} }
} }
...@@ -72,9 +72,9 @@ const fetchList = (isReset = false) => { ...@@ -72,9 +72,9 @@ const fetchList = (isReset = false) => {
if (beforeRequest) { if (beforeRequest) {
requestParams = beforeRequest(requestParams, isReset) requestParams = beforeRequest(requestParams, isReset)
} }
for (const key in params) { for (const key in requestParams) {
if (params[key] === '' || params[key] === undefined || params[key] === undefined) { if (requestParams[key] === '' || requestParams[key] === undefined || requestParams[key] === null) {
delete params[key] delete requestParams[key]
} }
} }
loading.value = true loading.value = true
...@@ -166,7 +166,7 @@ defineExpose({ refetch, tableRef }) ...@@ -166,7 +166,7 @@ defineExpose({ refetch, tableRef })
</template> </template>
</el-form-item> </el-form-item>
</template> </template>
<el-form-item class="filter-buttons"> <el-form-item class="filter-buttons" v-if="!isMobile">
<el-button type="primary" :icon="Search" @click="search">搜索</el-button> <el-button type="primary" :icon="Search" @click="search">搜索</el-button>
<el-button :icon="RefreshLeft" @click="reset">重置</el-button> <el-button :icon="RefreshLeft" @click="reset">重置</el-button>
</el-form-item> </el-form-item>
...@@ -178,7 +178,7 @@ defineExpose({ refetch, tableRef }) ...@@ -178,7 +178,7 @@ defineExpose({ refetch, tableRef })
<slot></slot> <slot></slot>
<!-- 主体 --> <!-- 主体 -->
<div class="table-list-bd"> <div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }"> <slot name="body" v-bind="{ data: dataList, loading }">
<!-- 桌面端 / 禁用移动端卡片时:表格 --> <!-- 桌面端 / 禁用移动端卡片时:表格 -->
<el-table <el-table
v-if="!isMobile || !mobileCards" v-if="!isMobile || !mobileCards"
...@@ -202,7 +202,10 @@ defineExpose({ refetch, tableRef }) ...@@ -202,7 +202,10 @@ defineExpose({ refetch, tableRef })
<span class="mobile-card-label">{{ col.label }}</span> <span class="mobile-card-label">{{ col.label }}</span>
<span class="mobile-card-colon">:</span> <span class="mobile-card-colon">:</span>
<span class="mobile-card-value"> <span class="mobile-card-value">
<slot v-if="col.slots && $slots[col.slots]" :name="col.slots" v-bind="{ row, $index: rowIndex }"></slot> <slot
v-if="col.slots && $slots[col.slots]"
:name="col.slots"
v-bind="{ row, $index: rowIndex }"></slot>
<div v-html="col.computed({ row, $index: rowIndex })" v-else-if="col.computed"></div> <div v-html="col.computed({ row, $index: rowIndex })" v-else-if="col.computed"></div>
<template v-else>{{ row[col.prop] ?? '-' }}</template> <template v-else>{{ row[col.prop] ?? '-' }}</template>
</span> </span>
...@@ -292,27 +295,68 @@ defineExpose({ refetch, tableRef }) ...@@ -292,27 +295,68 @@ defineExpose({ refetch, tableRef })
min-height: 120px; min-height: 120px;
} }
.mobile-card { .mobile-card {
background: #fff; border-radius: 8px; border: 1px solid #eee; background: #fff;
margin-bottom: 12px; overflow: hidden; display: flex; flex-direction: column; border-radius: 12px;
border: 1px solid #e5e5e5;
margin-bottom: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mobile-card-body {
flex: 1;
padding: 14px;
} }
.mobile-card-body { flex: 1; padding: 14px 16px; }
.mobile-card-row { .mobile-card-row {
display: flex; padding: 5px 0; border-bottom: 1px solid #f5f5f5; line-height: 1.6; display: flex;
&:last-child { border-bottom: none; } align-items: flex-start;
padding: 8px 0;
border-bottom: 1px solid #f2f2f2;
line-height: 1.6;
&:last-child {
border-bottom: none;
}
}
.mobile-card-label {
flex-shrink: 0;
color: #999;
font-size: 13px;
}
.mobile-card-colon {
color: #999;
font-size: 13px;
margin-right: 6px;
}
.mobile-card-value {
flex: 1;
font-size: 14px;
color: #333;
min-width: 0;
word-break: break-all;
}
.mobile-card-row:first-child {
.mobile-card-value {
color: var(--main-color);
font-size: 16px;
font-weight: 600;
line-height: 1.5;
}
} }
.mobile-card-label { flex-shrink: 0; color: #999; font-size: 14px; }
.mobile-card-colon { color: #999; font-size: 14px; margin-right: 6px; }
.mobile-card-value { flex: 1; font-size: 14px; color: #333; min-width: 0; word-break: break-all; }
.mobile-card-ft { .mobile-card-ft {
padding: 6px 12px; display: flex; flex-wrap: wrap; padding: 10px 12px;
gap: 6px; row-gap: 4px; display: flex;
background: #fafafa; border-top: 1px solid #f0f0f0; flex-wrap: wrap;
gap: 4px 8px;
background: #fafafa;
border-top: 1px solid #f0f0f0;
.el-button { .el-button {
padding: 5px 10px; padding: 5px 8px;
font-size: 12px; font-size: 12px;
min-width: 44px; min-width: 44px;
text-align: center; text-align: center;
+ .el-button { margin-left: 0; } + .el-button {
margin-left: 0;
}
} }
} }
...@@ -341,7 +385,7 @@ defineExpose({ refetch, tableRef }) ...@@ -341,7 +385,7 @@ defineExpose({ refetch, tableRef })
width: 100% !important; width: 100% !important;
} }
.filter-buttons { .filter-buttons {
width: auto; display: none;
} }
} }
......
...@@ -5,13 +5,16 @@ export default { name: 'AppAside' } ...@@ -5,13 +5,16 @@ export default { name: 'AppAside' }
<script setup lang="ts"> <script setup lang="ts">
import type { IMenuItem } from '@/types' import type { IMenuItem } from '@/types'
import { useMenuStore } from '@/stores/menu' import { useMenuStore } from '@/stores/menu'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue' import { ArrowLeft, ArrowRight, Menu as MenuIcon } from '@element-plus/icons-vue'
const menus = useMenuStore().menus const menus = useMenuStore().menus
const route = useRoute() const route = useRoute()
const isMobile = useMediaQuery('(max-width: 767px)')
const collapsed = ref(false) const collapsed = ref(false)
const mobileMenuVisible = ref(false)
const mobileActiveGroup = ref('')
const currentMenu = computed(() => { const currentMenu = computed(() => {
return findMenu(route.fullPath, menus) return findMenu(route.fullPath, menus)
...@@ -21,6 +24,17 @@ const currentSubmenu = computed(() => { ...@@ -21,6 +24,17 @@ const currentSubmenu = computed(() => {
return currentMenu.value?.children ? findMenu(route.fullPath, currentMenu.value.children) : null return currentMenu.value?.children ? findMenu(route.fullPath, currentMenu.value.children) : null
}) })
watchEffect(() => {
mobileActiveGroup.value = currentMenu.value?.children ? currentMenu.value.path : ''
})
watch(
() => route.fullPath,
() => {
mobileMenuVisible.value = false
}
)
function findMenu(path: string, menus: IMenuItem[]) { function findMenu(path: string, menus: IMenuItem[]) {
return menus.find((item) => { return menus.find((item) => {
if ( if (
...@@ -46,6 +60,10 @@ function toggleCollapse() { ...@@ -46,6 +60,10 @@ function toggleCollapse() {
collapsed.value = !collapsed.value collapsed.value = !collapsed.value
} }
function toggleMobileMenu() {
mobileMenuVisible.value = !mobileMenuVisible.value
}
// 处理菜单路径中的 query 参数(如 /material?type=1),与 experiment_id 合并 // 处理菜单路径中的 query 参数(如 /material?type=1),与 experiment_id 合并
function menuLinkTo(item: { path: string }) { function menuLinkTo(item: { path: string }) {
const baseQuery: Record<string, any> = { experiment_id: route.query.experiment_id } const baseQuery: Record<string, any> = { experiment_id: route.query.experiment_id }
...@@ -61,7 +79,66 @@ function menuLinkTo(item: { path: string }) { ...@@ -61,7 +79,66 @@ function menuLinkTo(item: { path: string }) {
</script> </script>
<template> <template>
<aside class="app-aside" :class="{ 'is-collapsed': collapsed }"> <template v-if="isMobile">
<el-drawer
v-model="mobileMenuVisible"
direction="ltr"
size="84%"
:with-header="false"
class="app-aside-drawer">
<div class="mobile-menu">
<div class="mobile-menu__header">
<el-button circle plain :icon="MenuIcon" @click="toggleMobileMenu" />
</div>
<div class="mobile-menu__body">
<template v-for="item in menus" :key="item.path">
<RouterLink
v-if="!item.children"
:to="menuLinkTo(item)"
class="mobile-menu__link"
:class="{ 'is-active': item.path === currentMenu?.path }"
v-permission="item.tag"
@click="toggleMobileMenu">
<component :is="item.icon" class="mobile-menu__link-icon" v-if="item.icon" />
<span>{{ item.name }}</span>
</RouterLink>
<el-collapse v-else v-model="mobileActiveGroup" accordion class="mobile-menu__group" v-permission="item.tag">
<el-collapse-item :name="item.path">
<template #title>
<div class="mobile-menu__group-title">
<div class="mobile-menu__group-main">
<component :is="item.icon" class="mobile-menu__link-icon" v-if="item.icon" />
<span>{{ item.name }}</span>
</div>
<span class="mobile-menu__group-count">{{ item.children.length }} 项</span>
</div>
</template>
<div class="mobile-menu__children">
<RouterLink
v-for="submenu in item.children"
:key="submenu.path"
:to="menuLinkTo(submenu)"
class="mobile-menu__sublink"
:class="{ 'is-active': submenu.path === currentSubmenu?.path }"
v-permission="submenu.tag"
@click="toggleMobileMenu">
<component :is="submenu.icon" class="mobile-menu__sublink-icon" v-if="submenu.icon"></component>
<span>{{ submenu.name }}</span>
</RouterLink>
</div>
</el-collapse-item>
</el-collapse>
</template>
</div>
</div>
</el-drawer>
<button class="mobile-menu-float-btn" type="button" @click="toggleMobileMenu">
<el-icon><ArrowRight /></el-icon>
</button>
</template>
<aside v-else class="app-aside" :class="{ 'is-collapsed': collapsed }">
<nav class="menu"> <nav class="menu">
<ul> <ul>
<li <li
...@@ -206,21 +283,21 @@ function menuLinkTo(item: { path: string }) { ...@@ -206,21 +283,21 @@ function menuLinkTo(item: { path: string }) {
// 底部收起按钮 // 底部收起按钮
.menu-toggle { .menu-toggle {
position: absolute; position: absolute;
bottom: 20px; bottom: 24px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 28px; width: 32px;
height: 56px; height: 52px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
color: #999; color: #777;
font-size: 14px; font-size: 14px;
background: #fff; background: #fff;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
border-radius: 28px; border-radius: 18px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
z-index: 10; z-index: 10;
&:hover { &:hover {
background: var(--main-color); background: var(--main-color);
...@@ -243,20 +320,20 @@ function menuLinkTo(item: { path: string }) { ...@@ -243,20 +320,20 @@ function menuLinkTo(item: { path: string }) {
.aside-float-btn { .aside-float-btn {
position: fixed; position: fixed;
left: 0; left: 0;
bottom: 20px; bottom: 24px;
width: 28px; width: 32px;
height: 56px; height: 52px;
background: #fff; background: #fff;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
border-left: none; border-left: none;
border-radius: 0 28px 28px 0; border-radius: 0 18px 18px 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
z-index: 2000; z-index: 2000;
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.08); box-shadow: 2px 0 10px rgba(0, 0, 0, 0.08);
color: #666; color: #777;
font-size: 14px; font-size: 14px;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
...@@ -265,4 +342,152 @@ function menuLinkTo(item: { path: string }) { ...@@ -265,4 +342,152 @@ function menuLinkTo(item: { path: string }) {
border-color: var(--main-color); border-color: var(--main-color);
} }
} }
.app-aside-drawer {
.el-drawer {
background: #ecf2f7;
}
.el-drawer__body {
padding: 0;
}
}
.mobile-menu {
height: 100%;
display: flex;
flex-direction: column;
}
.mobile-menu__header {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 14px 18px 10px;
background: #fff;
border-bottom: 1px solid #e5e5e5;
}
.mobile-menu__body {
flex: 1;
overflow-y: auto;
padding: 14px;
}
.mobile-menu__link,
.mobile-menu__group {
margin-bottom: 10px;
}
.mobile-menu__link {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-radius: 12px;
background: #fff;
color: #333;
border: 1px solid #e5e5e5;
&.is-active {
background: var(--main-color);
color: #fff;
border-color: var(--main-color);
.mobile-menu__link-icon {
fill: currentColor;
color: currentColor;
}
}
}
.mobile-menu__link-icon,
.mobile-menu__sublink-icon {
width: 18px;
flex-shrink: 0;
}
.mobile-menu__group {
border: none;
background: transparent;
.el-collapse-item__wrap,
.el-collapse-item__content {
background: transparent;
}
.el-collapse-item__header {
height: auto;
line-height: normal;
padding: 14px 16px;
border: 1px solid #e5e5e5;
border-radius: 12px;
background: #fff;
}
.el-collapse-item__arrow {
margin-left: 10px;
color: #999;
}
.el-collapse-item__content {
padding-bottom: 0;
}
}
.mobile-menu__group-title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.mobile-menu__group-main {
display: flex;
align-items: center;
gap: 10px;
color: #333;
font-weight: 600;
}
.mobile-menu__group-count {
font-size: 12px;
color: #999;
}
.mobile-menu__children {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 6px 2px;
}
.mobile-menu__sublink {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: 12px;
color: #666;
background: #fff;
border: 1px solid #e5e5e5;
&.is-active {
color: var(--main-color);
border-color: var(--main-color);
background: rgba(186, 20, 62, 0.06);
font-weight: 600;
}
}
.mobile-menu-float-btn {
position: fixed;
left: 0;
bottom: 24px;
width: 32px;
height: 52px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #e0e0e0;
border-left: none;
border-radius: 0 18px 18px 0;
background: #fff;
color: var(--main-color);
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.08);
z-index: 2000;
}
</style> </style>
...@@ -68,7 +68,7 @@ async function handleUpdate() { ...@@ -68,7 +68,7 @@ async function handleUpdate() {
<LiveProductCategory v-model="form.live_commodity_type_id" style="width: 100%" placeholder="请选择" /> <LiveProductCategory v-model="form.live_commodity_type_id" style="width: 100%" placeholder="请选择" />
</el-form-item> </el-form-item>
<el-form-item label="重要性" prop="is_importance"> <el-form-item label="重要性" prop="is_importance">
<el-radio-group v-model="form.is_importance" :disabled="isUpdate"> <el-radio-group v-model="form.is_importance">
<el-radio v-for="item in importType" v-bind="item" :key="item.value"></el-radio> <el-radio v-for="item in importType" v-bind="item" :key="item.value"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
......
<script setup> <script setup>
import { ElMessageBox, ElMessage } from 'element-plus' import { ElMessageBox, ElMessage } from 'element-plus'
import { useMediaQuery } from '@vueuse/core'
import { getCategoryList, deleteCategory } from '../api' import { getCategoryList, deleteCategory } from '../api'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
import { getNameByValue } from '@/utils/dictionary' import { getNameByValue } from '@/utils/dictionary'
...@@ -7,6 +8,7 @@ import { getNameByValue } from '@/utils/dictionary' ...@@ -7,6 +8,7 @@ import { getNameByValue } from '@/utils/dictionary'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue')) const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
const statusList = useMapStore().getMapValuesByKey('system_status') const statusList = useMapStore().getMapValuesByKey('system_status')
const isMobile = useMediaQuery('(max-width: 767px)')
const appList = ref(null) const appList = ref(null)
// 刷新 // 刷新
...@@ -44,6 +46,23 @@ const listOptions = { ...@@ -44,6 +46,23 @@ const listOptions = {
], ],
} }
const getStatusText = (status) => getNameByValue(status, statusList)
const getLevelText = (level) => parseInt(level) + 1
const flattenCategories = (list = [], parents = []) => {
return list.flatMap((item) => {
const currentPath = [...parents, item.name]
const currentItem = {
...item,
pathText: currentPath.join(' / '),
levelText: getLevelText(item.level),
statusText: getStatusText(item.status),
}
const children = flattenCategories(item.children || [], currentPath)
return [currentItem, ...children]
})
}
const formVisible = ref(false) const formVisible = ref(false)
const currentRow = ref(null) const currentRow = ref(null)
...@@ -76,8 +95,129 @@ const handleRemove = async (row) => { ...@@ -76,8 +95,129 @@ const handleRemove = async (row) => {
<el-button text type="primary" @click="handleUpdate(row)">编辑</el-button> <el-button text type="primary" @click="handleUpdate(row)">编辑</el-button>
<el-button text type="primary" @click="handleRemove(row)">删除</el-button> <el-button text type="primary" @click="handleRemove(row)">删除</el-button>
</template> </template>
<template #body="{ data, loading }">
<div v-if="isMobile" class="category-mobile-list" v-loading="loading">
<div v-for="row in flattenCategories(data)" :key="row.id" class="category-mobile-card">
<div class="category-mobile-card__head">
<div class="category-mobile-card__title">{{ row.name }}</div>
<el-tag size="small" :type="row.status === '1' ? 'success' : 'danger'" effect="light">
{{ row.statusText }}
</el-tag>
</div>
<div class="category-mobile-card__meta">
<span class="category-mobile-card__meta-label">层级</span>
<span class="category-mobile-card__meta-value">第 {{ row.levelText }} 级</span>
</div>
<div class="category-mobile-card__path">
<span class="category-mobile-card__meta-label">层级路径</span>
<span class="category-mobile-card__path-text">{{ row.pathText }}</span>
</div>
<div class="category-mobile-card__actions">
<el-button text type="primary" @click="handleAdd({ pid: row.id })" v-if="row.level != 4">新增子类</el-button>
<el-button text type="primary" @click="handleUpdate(row)">编辑</el-button>
<el-button text type="danger" @click="handleRemove(row)">删除</el-button>
</div>
</div>
<el-empty v-if="!data.length" description="暂无数据" />
</div>
<el-table v-else v-loading="loading" :header-cell-style="{ background: '#ededed' }" :data="data" row-key="id">
<el-table-column label="商品品类名称" prop="name" align="left" />
<el-table-column label="层级" prop="level" align="center">
<template #default="{ row }">
{{ getLevelText(row.level) }}
</template>
</el-table-column>
<el-table-column label="状态" prop="status" align="center">
<template #default="{ row }">
<span :style="{ color: row.status === '1' ? 'var(--main-success-color)' : 'var(--main-color)' }">
{{ getStatusText(row.status) }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="160" align="center">
<template #default="{ row }">
<el-button text type="primary" @click="handleAdd({ pid: row.id })" v-if="row.level != 4">新增</el-button>
<el-button text type="primary" @click="handleUpdate(row)">编辑</el-button>
<el-button text type="primary" @click="handleRemove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</template>
</AppList> </AppList>
</AppCard> </AppCard>
<!-- 新建/修改 --> <!-- 新建/修改 -->
<FormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible" /> <FormDialog v-model="formVisible" :data="currentRow" @update="handleRefresh" v-if="formVisible" />
</template> </template>
<style scoped lang="scss">
.category-mobile-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.category-mobile-card {
padding: 14px;
border: 1px solid #e5e5e5;
border-radius: 12px;
background: #fff;
}
.category-mobile-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.category-mobile-card__title {
font-size: 16px;
font-weight: 600;
color: var(--main-color);
line-height: 1.5;
word-break: break-word;
}
.category-mobile-card__meta,
.category-mobile-card__path {
display: flex;
gap: 8px;
align-items: flex-start;
font-size: 13px;
line-height: 1.6;
}
.category-mobile-card__path {
margin-top: 6px;
}
.category-mobile-card__meta-label {
flex-shrink: 0;
color: #999;
}
.category-mobile-card__meta-value,
.category-mobile-card__path-text {
color: #333;
min-width: 0;
word-break: break-word;
}
.category-mobile-card__path-text {
font-family: ui-monospace, 'SFMono-Regular', 'SF Mono', Menlo, Consolas, monospace;
font-size: 12px;
padding: 2px 8px;
border-radius: 999px;
background: #f5f5f5;
}
.category-mobile-card__actions {
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
</style>
...@@ -107,7 +107,7 @@ watch( ...@@ -107,7 +107,7 @@ watch(
async (id) => { async (id) => {
const res = await getProduct({ id }) const res = await getProduct({ id })
product.value = res.data.detail product.value = res.data.detail
} },
) )
// 提交 // 提交
...@@ -156,11 +156,11 @@ watch( ...@@ -156,11 +156,11 @@ watch(
.replace(/(商品引入|商品讲解|引导转化)/g, wrapWithStyle) .replace(/(商品引入|商品讲解|引导转化)/g, wrapWithStyle)
.replace( .replace(
/(开场互动|引出话题|引出商品|目标人群锁定|产品卖点讲解|观众互动|价格福利|催单话术|强调购买方式|结尾互动)/g, /(开场互动|引出话题|引出商品|目标人群锁定|产品卖点讲解|观众互动|价格福利|催单话术|强调购买方式|结尾互动)/g,
wrapWithStyle2 wrapWithStyle2,
) )
} }
}, },
{ deep: true } { deep: true },
) )
const aiActive = ref(1) const aiActive = ref(1)
...@@ -275,7 +275,7 @@ function handleAIGenerate(index) { ...@@ -275,7 +275,7 @@ function handleAIGenerate(index) {
@click="handleAIGenerate(3)" @click="handleAIGenerate(3)"
:disabled="isLoading" :disabled="isLoading"
:loading="isLoading && aiActive === 3"> :loading="isLoading && aiActive === 3">
{{ messages.length ? '再次生成直播话术' : 'AI生成直播话术' }} AI生成直播话术
</el-button> </el-button>
</div> </div>
<Editor <Editor
......
...@@ -38,7 +38,7 @@ const listOptions = computed(() => { ...@@ -38,7 +38,7 @@ const listOptions = computed(() => {
], ],
columns: [ columns: [
{ label: '序号', type: 'index', width: 60 }, { label: '序号', type: 'index', width: 60 },
{ label: '直播ID', prop: 'id' }, // { label: '直播ID', prop: 'id' },
{ label: '直播主题标题', prop: 'live_commodity_title' }, { label: '直播主题标题', prop: 'live_commodity_title' },
{ label: '直播话术', prop: 'live_commodity_speeches_name' }, { label: '直播话术', prop: 'live_commodity_speeches_name' },
{ label: '所属直播主题品类', prop: 'live_commodity_type_full_name' }, { label: '所属直播主题品类', prop: 'live_commodity_type_full_name' },
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论