20260224
This commit is contained in:
parent
3eb0641842
commit
84bf956f73
BIN
20260117.zip
BIN
20260117.zip
Binary file not shown.
@ -5,6 +5,7 @@
|
||||
<link rel="icon" type="image/png" href="/src/assets/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>nanafa-哪哪发物流,发哪都方便</title>
|
||||
<link rel="stylesheet" type="text/css" media="print" href="/print-lock.css" />
|
||||
<!-- DNS 预解析,加速资源加载 -->
|
||||
<link rel="dns-prefetch" href="//43.139.210.35" />
|
||||
<!-- 预连接,提前建立连接 -->
|
||||
|
||||
@ -27,8 +27,16 @@ server {
|
||||
# 静态文件根目录
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# ============ API 代理(同源请求,避免跨域 405)============
|
||||
location ^~ /api/ {
|
||||
# ============ API 前端路由(必须先于 /api/ 代理)============
|
||||
# 直接访问 /api/downstreamInterface 等页面路由时返回 index.html,由 SPA 接管,避免 404
|
||||
# 使用正则且放在 /api/ 代理之前:^~ 优先级高于正则,故不能再用 ^~ /api/,下面用正则代理其余 /api/
|
||||
location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest)(/|$) {
|
||||
root /usr/share/nginx/html;
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
# ============ API 代理(仅真实后端接口,同源请求避免跨域 405)============
|
||||
location ~ ^/api/ {
|
||||
rewrite ^/api/(.*)$ /itfx/$1 break;
|
||||
proxy_pass http://api.nanafa.com;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"scripts": {
|
||||
"preinstall": "node -e \"if (!process.env.npm_config_user_agent || !process.env.npm_config_user_agent.includes('pnpm')) { console.error('Please use pnpm to install dependencies.'); process.exit(1); }\"",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "node --max-old-space-size=4096 node_modules/vite/bin/vite.js build",
|
||||
"report": "cross-env REPORT=true npm run build",
|
||||
"preview": "npm run build && vite preview --host",
|
||||
"type:check": "vue-tsc --noEmit --skipLibCheck",
|
||||
@ -33,23 +33,22 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/g2plot": "^2.4.35",
|
||||
"@arco-design/web-vue": "^2.44.7",
|
||||
"@handsontable/vue3": "^16.2.0",
|
||||
"@monaco-editor/loader": "^1.5.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@vueuse/core": "^9.3.0",
|
||||
"ag-grid-community": "^35.0.0",
|
||||
"ag-grid-vue3": "^35.0.0",
|
||||
"arco-design-pro-vue": "^2.7.3",
|
||||
"axios": "^0.24.0",
|
||||
"dayjs": "^1.11.5",
|
||||
"diff": "^8.0.2",
|
||||
"echarts": "^5.4.0",
|
||||
"element-plus": "^2.13.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"form-data": "^4.0.5",
|
||||
"handsontable": "^16.2.0",
|
||||
"hyperformula": "^3.1.1",
|
||||
"interactjs": "^1.10.27",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.0",
|
||||
@ -59,6 +58,7 @@
|
||||
"query-string": "^8.0.3",
|
||||
"sm-crypto": "^0.3.13",
|
||||
"sortablejs": "^1.15.0",
|
||||
"vg-print": "^1.0.420",
|
||||
"vue": "^3.2.40",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-echarts": "^6.2.3",
|
||||
|
||||
510
pnpm-lock.yaml
generated
510
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
297
public/print-lock.css
Normal file
297
public/print-lock.css
Normal file
@ -0,0 +1,297 @@
|
||||
|
||||
@media print {
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hiprint-printPaper * {
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box; /* Firefox */
|
||||
-webkit-box-sizing: border-box; /* Safari */
|
||||
image-rendering: -webkit-optimize-contrast; /* 让图片/标尺稍微清楚一点 */
|
||||
}
|
||||
|
||||
.hiprint-printPaper *:focus {
|
||||
outline: -webkit-focus-ring-color auto 0px;
|
||||
}
|
||||
|
||||
|
||||
.hiprint-page-break-avoid {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
.hiprint-printPaper {
|
||||
position: relative;
|
||||
padding: 0 0 0 0;
|
||||
page-break-after: always;
|
||||
-webkit-user-select: none; /* Chrome/Safari/Opera */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
user-select: none;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hiprint-printPaper .hiprint-printPaper-content {
|
||||
position: relative;
|
||||
}
|
||||
.hiprint-printPaper.design {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.hiprint-printTemplate .hiprint-printPanel {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.hiprint-printPaper, hiprint-printPanel {
|
||||
box-sizing: border-box;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.hiprint-printPanel .hiprint-printPaper:last-child {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.hiprint-printTemplate .hiprint-printPanel:last-child {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
.hiprint-printPaper .hideheaderLinetarget {
|
||||
border-top: 0px dashed rgb(201, 190, 190) !important;
|
||||
}
|
||||
|
||||
.hiprint-printPaper .hidefooterLinetarget {
|
||||
border-top: 0px dashed rgb(201, 190, 190) !important;
|
||||
}
|
||||
|
||||
.hiprint-printPaper.design {
|
||||
border: 1px dashed rgba(170,170,170,0.7);
|
||||
}
|
||||
|
||||
.design .hiprint-printElement-table-content, .design .hiprint-printElement-longText-content {
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.design .resize-panel {
|
||||
box-sizing: border-box;
|
||||
border: 1px dotted;
|
||||
}
|
||||
|
||||
.hiprint-printElement-text {
|
||||
background-color: transparent;
|
||||
background-repeat: repeat;
|
||||
padding: 0 0 0 0;
|
||||
border: 0.75pt none rgb(0,0,0);
|
||||
direction: ltr;
|
||||
font-family: 'SimSun';
|
||||
font-size: 9pt;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
padding-bottom: 0pt;
|
||||
padding-left: 0pt;
|
||||
padding-right: 0pt;
|
||||
padding-top: 0pt;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
line-height: 9.75pt;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.design .hiprint-printElement-text-content {
|
||||
border: 1px dashed rgb(206, 188, 188);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hiprint-printElement-longText {
|
||||
background-color: transparent;
|
||||
background-repeat: repeat;
|
||||
border: 0.75pt none rgb(0,0,0);
|
||||
direction: ltr;
|
||||
font-family: 'SimSun';
|
||||
font-size: 9pt;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
padding-bottom: 0pt;
|
||||
padding-left: 0pt;
|
||||
padding-right: 0pt;
|
||||
padding-top: 0pt;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
line-height: 9.75pt;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
/*white-space: pre-wrap*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
.hiprint-printElement-table {
|
||||
background-color: transparent;
|
||||
background-repeat: repeat;
|
||||
color: rgb(0,0,0);
|
||||
border-color: rgb(0,0,0);
|
||||
border-style: none;
|
||||
direction: ltr;
|
||||
font-family: 'SimSun';
|
||||
font-size: 9pt;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
padding-bottom: 0pt;
|
||||
padding-left: 0pt;
|
||||
padding-right: 0pt;
|
||||
padding-top: 0pt;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
padding: 0 0 0 0;
|
||||
box-sizing: border-box;
|
||||
line-height: 9.75pt;
|
||||
}
|
||||
|
||||
.hiprint-printElement-table thead {
|
||||
background: #e8e8e8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hiprint-printElement-tableTarget, .hiprint-printElement-tableTarget tr, .hiprint-printElement-tableTarget td {
|
||||
border-color: rgb(0,0,0);
|
||||
/*border-style: none;*/
|
||||
/*border: 1px solid rgb(0,0,0);*/
|
||||
font-weight: normal;
|
||||
direction: ltr;
|
||||
padding-bottom: 0pt;
|
||||
padding-left: 4pt;
|
||||
padding-right: 4pt;
|
||||
padding-top: 0pt;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
/*line-height: 9.75pt;
|
||||
font-size: 9pt;*/
|
||||
}
|
||||
|
||||
/*.hiprint-printElement-tableTarget tr,*/
|
||||
.hiprint-printElement-tableTarget td {
|
||||
height: 18pt;
|
||||
}
|
||||
|
||||
/* 边框样式(用于打印时保持边框) */
|
||||
table.hiprint-printElement-tableTarget { width: 100%; }
|
||||
.hiprint-printElement-tableTarget-border-all { border: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-none { border: 0px solid; }
|
||||
.hiprint-printElement-tableTarget-border-lr { border-left: 1px solid; border-right: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-left { border-left: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-right { border-right: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-tb { border-top: 1px solid; border-bottom: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-top { border-top: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-bottom { border-bottom: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-td-none td { border: 0px solid; }
|
||||
.hiprint-printElement-tableTarget-border-td-all td:not(:nth-last-child(-n+2)) { border-right: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-td-all td { border-left: 1px solid; }
|
||||
.hiprint-printElement-tableTarget-border-td-all td:last-child:first-child { border-left: none; }
|
||||
|
||||
.hiprint-printPaper .hiprint-paperNumber {
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.design .hiprint-printElement-table-handle {
|
||||
position: absolute;
|
||||
height: 12pt;
|
||||
width: 12pt;
|
||||
background: red;
|
||||
z-index:1;
|
||||
}
|
||||
|
||||
.hiprint-printPaper .hiprint-paperNumber-disabled {
|
||||
float: right !important;
|
||||
right: 0 !important;
|
||||
color: gainsboro !important;
|
||||
}
|
||||
|
||||
.hiprint-printElement-vline, .hiprint-printElement-hline {
|
||||
border: 0px none rgb(0,0,0);
|
||||
|
||||
}
|
||||
.hiprint-printElement-vline {
|
||||
border-left: 0.75pt solid #000;
|
||||
border-right: 0px none rgb(0,0,0) !important;
|
||||
border-bottom: 0px none rgb(0,0,0) !important;
|
||||
border-top: 0px none rgb(0,0,0) !important;
|
||||
}
|
||||
|
||||
.hiprint-printElement-hline {
|
||||
border-top: 0.75pt solid #000;
|
||||
border-right: 0px none rgb(0,0,0) !important;
|
||||
border-bottom: 0px none rgb(0,0,0) !important;
|
||||
border-left: 0px none rgb(0,0,0) !important;
|
||||
}
|
||||
|
||||
.hiprint-printElement-oval, .hiprint-printElement-rect {
|
||||
border: 0.75pt solid #000;
|
||||
}
|
||||
|
||||
.hiprint-text-content-middle {
|
||||
display:table;
|
||||
}
|
||||
.hiprint-text-content-middle>div {
|
||||
display: table-cell;
|
||||
vertical-align:middle
|
||||
}
|
||||
|
||||
.hiprint-text-content-bottom {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.hiprint-text-content-bottom > div {
|
||||
display: table-cell;
|
||||
vertical-align: bottom
|
||||
}
|
||||
|
||||
/*hi-grid-row */
|
||||
.hi-grid-row {
|
||||
position: relative;
|
||||
height: auto;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
zoom: 1;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hi-grid-row::after, .hi-grid-row::before {
|
||||
display: table;
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hi-grid-col {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
float: left;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.table-grid-row {
|
||||
margin-left: -0pt;
|
||||
margin-right: -0pt;
|
||||
}
|
||||
|
||||
.tableGridColumnsGutterRow {
|
||||
padding-left: 0pt;
|
||||
padding-right: 0pt;
|
||||
}
|
||||
.hiprint-gridColumnsFooter {
|
||||
text-align: left;
|
||||
clear: both;
|
||||
}
|
||||
@ -38,5 +38,9 @@ export type MenuUpdateDto = {
|
||||
* 描述
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
order?: number | null;
|
||||
};
|
||||
|
||||
|
||||
@ -129,26 +129,13 @@
|
||||
return findNode(props.treeData);
|
||||
}
|
||||
|
||||
/** 二级菜单序号:按父级分组,每组从 1 重新开始 */
|
||||
function getNodeIndex(key: string): number | '' {
|
||||
if (!key || !props.treeData?.length) return '';
|
||||
let index = 0;
|
||||
function findIndex(nodes: CustomLeftMenuTreeNode[]): number | null {
|
||||
for (const node of nodes) {
|
||||
if (node.children?.length) {
|
||||
for (const child of node.children) {
|
||||
index++;
|
||||
if (child.key === key) return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const category of props.treeData) {
|
||||
if (category.children?.length) {
|
||||
for (const child of category.children) {
|
||||
index++;
|
||||
if (child.key === key) return index;
|
||||
}
|
||||
const children = category.children || [];
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (children[i].key === key) return i + 1;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
@ -305,10 +292,13 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="getNodeData(key)?.isLeaf && (showNodeCode && getNodeData(key)?.code || $slots['node-second-row-extra'])">
|
||||
<td class="node-code-cell">
|
||||
<span v-if="showNodeCode && getNodeData(key)?.code" class="interface-code">{{ getNodeData(key)?.code }}</span>
|
||||
<td v-if="showNodeCode && getNodeData(key)?.code" class="node-code-cell">
|
||||
<span class="interface-code">{{ getNodeData(key)?.code }}</span>
|
||||
</td>
|
||||
<td class="node-actions-cell node-second-row-extra-cell">
|
||||
<td
|
||||
:class="['node-actions-cell', 'node-second-row-extra-cell']"
|
||||
:colspan="(showNodeCode && getNodeData(key)?.code) ? 1 : 2"
|
||||
>
|
||||
<span class="node-second-row-extra-slot">
|
||||
<slot name="node-second-row-extra" :node-data="getNodeData(key)" :node-key="key"></slot>
|
||||
</span>
|
||||
@ -559,6 +549,15 @@
|
||||
.map-count .count-num {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* 选中时第二行插槽(如创建时间)保持深色,不变白 */
|
||||
.node-second-row-extra-slot {
|
||||
color: #666 !important;
|
||||
|
||||
.create-time {
|
||||
color: #666 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-table {
|
||||
|
||||
@ -13,6 +13,11 @@
|
||||
<Block :options="contentOpts" :title="$t('settings.content')" />
|
||||
<Block :options="othersOpts" :title="$t('settings.otherSettings')" />
|
||||
<a-alert>{{ $t('settings.alertContent') }}</a-alert>
|
||||
<div class="restore-default-wrap">
|
||||
<a-button type="outline" long @click="restoreDefault">
|
||||
{{ $t('settings.restoreDefault') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
@ -72,6 +77,8 @@
|
||||
},
|
||||
]);
|
||||
|
||||
const DEFAULT_THEME_COLOR = '#132c44';
|
||||
|
||||
const cancel = () => {
|
||||
appStore.updateSettings({ globalSettings: false });
|
||||
emit('cancel');
|
||||
@ -81,5 +88,15 @@
|
||||
await copy(text);
|
||||
Message.success(t('settings.copySettings.message'));
|
||||
};
|
||||
const restoreDefault = () => {
|
||||
appStore.updateSettings({ themeColor: DEFAULT_THEME_COLOR });
|
||||
Message.success(t('settings.restoreDefault.message'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.restore-default-wrap {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<div class="tab-bar-scroll">
|
||||
<div class="tags-wrap">
|
||||
<div ref="tagsWrapRef" class="tags-wrap">
|
||||
<tab-item
|
||||
v-for="(tag, index) in tagList"
|
||||
:key="tag.fullPath"
|
||||
@ -14,8 +14,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
import Sortable from 'sortablejs';
|
||||
import {
|
||||
listenerRouteChange,
|
||||
removeRouteListener,
|
||||
@ -27,6 +28,9 @@
|
||||
const tabBarStore = useTabBarStore();
|
||||
|
||||
const affixRef = ref();
|
||||
const tagsWrapRef = ref<HTMLElement | null>(null);
|
||||
let sortableInstance: Sortable | null = null;
|
||||
|
||||
const tagList = computed(() => {
|
||||
return tabBarStore.getTabList;
|
||||
});
|
||||
@ -37,7 +41,7 @@
|
||||
watch(
|
||||
() => appStore.navbar,
|
||||
() => {
|
||||
affixRef.value.updatePosition();
|
||||
affixRef.value?.updatePosition();
|
||||
}
|
||||
);
|
||||
listenerRouteChange((route: RouteLocationNormalized) => {
|
||||
@ -53,7 +57,25 @@
|
||||
}
|
||||
}, true);
|
||||
|
||||
onMounted(() => {
|
||||
if (tagsWrapRef.value) {
|
||||
sortableInstance = new Sortable(tagsWrapRef.value, {
|
||||
animation: 150,
|
||||
filter: '.arco-tag-close-btn, .arco-tag-refresh-btn', // 点击关闭/刷新不触发拖拽
|
||||
preventOnFilter: true,
|
||||
onEnd(evt) {
|
||||
const { oldIndex, newIndex } = evt;
|
||||
if (oldIndex != null && newIndex != null && oldIndex !== newIndex) {
|
||||
tabBarStore.reorderTabs(oldIndex, newIndex);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
sortableInstance?.destroy();
|
||||
sortableInstance = null;
|
||||
removeRouteListener();
|
||||
});
|
||||
</script>
|
||||
@ -89,6 +111,16 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.tag-link {
|
||||
cursor: grab;
|
||||
}
|
||||
&.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
&.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.arco-tag-close-btn {
|
||||
display: none;
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"hideMenu": false,
|
||||
"menuCollapse": false,
|
||||
"footer": false,
|
||||
"themeColor": "#2D8C8C",
|
||||
"themeColor": "#132c44",
|
||||
"menuWidth": 220,
|
||||
"globalSettings": false,
|
||||
"device": "desktop",
|
||||
|
||||
15
src/env.d.ts
vendored
15
src/env.d.ts
vendored
@ -1,3 +1,18 @@
|
||||
declare module 'vg-print' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
export const FullDesigner: DefineComponent<Record<string, unknown>, {}, any>;
|
||||
export const hiprint: { register(options: { authKey: string }): void };
|
||||
export const createTemplate: (json: unknown) => unknown;
|
||||
export const printBrowser: (tpl: unknown, data: unknown) => void;
|
||||
const plugin: { install: (app: unknown) => void };
|
||||
export default plugin;
|
||||
}
|
||||
|
||||
declare module 'vg-print/style.css' {
|
||||
const css: string;
|
||||
export default css;
|
||||
}
|
||||
|
||||
declare module 'vue-request-editor' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
export const RequestEditor: DefineComponent<Record<string, unknown>, {}, any>;
|
||||
|
||||
@ -74,6 +74,7 @@ export default {
|
||||
'menu.channel.weightRules': 'Weight Rules',
|
||||
'menu.channel.weightRounding': 'Weight Rounding',
|
||||
'menu.channel.autoTrigger': 'Auto Trigger',
|
||||
'menu.channel.labelDesign': 'Label Design',
|
||||
'menu.track': 'Track',
|
||||
'menu.track.trackOrder': 'Track Order',
|
||||
'menu.track.warningOrder': 'Warning Order',
|
||||
@ -1296,6 +1297,25 @@ export default {
|
||||
'system.interface.import.success': 'Imported successfully',
|
||||
'system.interface.import.fail': 'Import failed',
|
||||
|
||||
// Channel - Label Design
|
||||
'channel.labelDesign.pageTitle': 'Label Template Config',
|
||||
'channel.labelDesign.configTitle': 'Template Config',
|
||||
'channel.labelDesign.sizeLabel': 'Label Size (W×H)',
|
||||
'channel.labelDesign.widthPlaceholder': 'Width',
|
||||
'channel.labelDesign.heightPlaceholder': 'Height',
|
||||
'channel.labelDesign.titleLabel': 'Label Title',
|
||||
'channel.labelDesign.titlePlaceholder': 'Enter label title',
|
||||
'channel.labelDesign.contentLabel': 'Label Content',
|
||||
'channel.labelDesign.contentPlaceholder': 'Enter label content',
|
||||
'channel.labelDesign.apply': 'Apply',
|
||||
'channel.labelDesign.reset': 'Reset',
|
||||
'channel.labelDesign.previewTitle': 'Preview (drag to move, resize from corner)',
|
||||
'channel.labelDesign.sizeMinError': 'Min size is 50px',
|
||||
'channel.labelDesign.noTitle': 'No title',
|
||||
'channel.labelDesign.noContent': 'No content',
|
||||
'channel.labelDesign.applySuccess': 'Config applied',
|
||||
'channel.labelDesign.resetSuccess': 'Config reset',
|
||||
|
||||
// Channel - Sales Products
|
||||
'channel.salesProducts.search.placeholder': 'Enter product name',
|
||||
'channel.salesProducts.search.refresh': 'Refresh list',
|
||||
@ -1455,6 +1475,7 @@ export default {
|
||||
'channel.serviceChannels.createTime': 'Created: {time}',
|
||||
|
||||
// Channel - Blacklist/Remote Zones (shared)
|
||||
'channel.area.listTitle': 'List',
|
||||
'channel.area.search.placeholder': 'Enter keyword',
|
||||
'channel.area.search.refresh': 'Refresh list',
|
||||
'channel.area.search.addArea': 'Add area',
|
||||
@ -1506,6 +1527,7 @@ export default {
|
||||
'channel.area.search.button.reset': 'Reset',
|
||||
|
||||
// Channel - Weight Rules
|
||||
'channel.weightRules.listTitle': 'List',
|
||||
'channel.weightRules.search.placeholder': 'Enter keyword',
|
||||
'channel.weightRules.search.refresh': 'Refresh list',
|
||||
'channel.weightRules.search.addRule': 'Add rule',
|
||||
@ -1556,6 +1578,7 @@ export default {
|
||||
'channel.weightRules.updateTime': 'Updated: {time}',
|
||||
|
||||
// Channel - Weight Rounding
|
||||
'channel.weightRounding.listTitle': 'List',
|
||||
'channel.weightRounding.search.placeholder': 'Enter keyword',
|
||||
'channel.weightRounding.search.refresh': 'Refresh list',
|
||||
'channel.weightRounding.search.addRounding': 'Add rounding',
|
||||
|
||||
@ -22,6 +22,8 @@ export default {
|
||||
'settings.copySettings': 'Copy Settings',
|
||||
'settings.copySettings.message':
|
||||
'Copy succeeded, please paste to file src/settings.json.',
|
||||
'settings.restoreDefault': 'Restore Default',
|
||||
'settings.restoreDefault.message': 'Theme color has been restored to default',
|
||||
'settings.close': 'Close',
|
||||
'settings.color.tooltip':
|
||||
'10 gradient colors generated according to the theme color',
|
||||
|
||||
@ -74,6 +74,7 @@ export default {
|
||||
'menu.channel.weightRules': '计重规则',
|
||||
'menu.channel.weightRounding': '计重进位',
|
||||
'menu.channel.autoTrigger': '自动触发',
|
||||
'menu.channel.labelDesign': '标签设计',
|
||||
'menu.track': '轨迹',
|
||||
'menu.track.trackOrder': '轨迹订单',
|
||||
'menu.track.warningOrder': '预警订单',
|
||||
@ -1297,6 +1298,25 @@ export default {
|
||||
'system.interface.import.success': '导入成功',
|
||||
'system.interface.import.fail': '导入失败',
|
||||
|
||||
// 渠道 - 标签设计
|
||||
'channel.labelDesign.pageTitle': '标签模板配置',
|
||||
'channel.labelDesign.configTitle': '模板配置',
|
||||
'channel.labelDesign.sizeLabel': '标签尺寸 (宽×高)',
|
||||
'channel.labelDesign.widthPlaceholder': '宽度',
|
||||
'channel.labelDesign.heightPlaceholder': '高度',
|
||||
'channel.labelDesign.titleLabel': '标签标题',
|
||||
'channel.labelDesign.titlePlaceholder': '请输入标签标题',
|
||||
'channel.labelDesign.contentLabel': '标签内容',
|
||||
'channel.labelDesign.contentPlaceholder': '请输入标签内容',
|
||||
'channel.labelDesign.apply': '应用配置',
|
||||
'channel.labelDesign.reset': '重置',
|
||||
'channel.labelDesign.previewTitle': '模板预览 (可拖拽调整位置、右下角缩放调整尺寸)',
|
||||
'channel.labelDesign.sizeMinError': '标签尺寸最小值为 50px,请重新输入',
|
||||
'channel.labelDesign.noTitle': '无标题',
|
||||
'channel.labelDesign.noContent': '无内容',
|
||||
'channel.labelDesign.applySuccess': '配置已应用',
|
||||
'channel.labelDesign.resetSuccess': '配置已重置',
|
||||
|
||||
// 渠道 - 销售产品
|
||||
'channel.salesProducts.search.placeholder': '请输入产品名称',
|
||||
'channel.salesProducts.search.refresh': '刷新列表',
|
||||
@ -1457,6 +1477,7 @@ export default {
|
||||
'channel.serviceChannels.createTime': '创建: {time}',
|
||||
|
||||
// 渠道 - 黑名单区/偏远分区(共用)
|
||||
'channel.area.listTitle': '列表',
|
||||
'channel.area.search.placeholder': '请输入关键字',
|
||||
'channel.area.search.refresh': '刷新列表',
|
||||
'channel.area.search.addArea': '新增分区',
|
||||
@ -1508,6 +1529,7 @@ export default {
|
||||
'channel.area.search.button.reset': '重置',
|
||||
|
||||
// 渠道 - 计重规则
|
||||
'channel.weightRules.listTitle': '列表',
|
||||
'channel.weightRules.search.placeholder': '请输入关键字',
|
||||
'channel.weightRules.search.refresh': '刷新列表',
|
||||
'channel.weightRules.search.addRule': '新增规则',
|
||||
@ -1558,6 +1580,7 @@ export default {
|
||||
'channel.weightRules.updateTime': '更新: {time}',
|
||||
|
||||
// 渠道 - 计重进位
|
||||
'channel.weightRounding.listTitle': '列表',
|
||||
'channel.weightRounding.search.placeholder': '请输入关键字',
|
||||
'channel.weightRounding.search.refresh': '刷新列表',
|
||||
'channel.weightRounding.search.addRounding': '新增进位',
|
||||
|
||||
@ -22,6 +22,8 @@ export default {
|
||||
'settings.copySettings': '复制配置',
|
||||
'settings.copySettings.message':
|
||||
'复制成功,请粘贴到 src/settings.json 文件中',
|
||||
'settings.restoreDefault': '恢复默认',
|
||||
'settings.restoreDefault.message': '主题色已恢复为默认',
|
||||
'settings.close': '关闭',
|
||||
'settings.color.tooltip':
|
||||
'根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)',
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { createApp, watch } from 'vue';
|
||||
import ArcoVue from '@arco-design/web-vue';
|
||||
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
|
||||
import ElementPlus from 'element-plus';
|
||||
import 'element-plus/dist/index.css';
|
||||
import VxeUITable from 'vxe-table';
|
||||
import 'vxe-table/lib/style.css';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
@ -19,6 +21,7 @@ import globalProperties from './plugins/globalProperties';
|
||||
import './mock';
|
||||
// 导入 ECharts 配置,确保组件在应用启动时注册
|
||||
import './utils/echarts';
|
||||
|
||||
// 初始化 RBAC token(从localStorage恢复)
|
||||
import { setAuthToken } from './api/rbac';
|
||||
import { applyThemeColor } from '@/utils/themeColor';
|
||||
@ -119,6 +122,8 @@ const app = createApp(App);
|
||||
|
||||
app.use(ArcoVue, {});
|
||||
app.use(ArcoVueIcon);
|
||||
// vg-print 内部使用 Element Plus 组件(el-row、el-col 等),需全局注册
|
||||
app.use(ElementPlus);
|
||||
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
|
||||
@ -75,6 +75,15 @@ const CHANNEL: AppRouteRecordRaw = {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'labelDesign',
|
||||
name: 'LabelDesign',
|
||||
component: () => import('@/views/channel/labelDesign/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.channel.labelDesign',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -75,6 +75,14 @@ const useAppStore = defineStore('tabBar', {
|
||||
this.deleteTag(idx, tag);
|
||||
}
|
||||
},
|
||||
/** 拖动排序标签:将 fromIndex 位置的标签移动到 toIndex */
|
||||
reorderTabs(fromIndex: number, toIndex: number) {
|
||||
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return;
|
||||
const list = [...this.tagList];
|
||||
const [item] = list.splice(fromIndex, 1);
|
||||
list.splice(toIndex, 0, item);
|
||||
this.freshTabList(list);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -4,84 +4,38 @@
|
||||
<BasicLayoutLeftRight :left-width="280">
|
||||
<template #left>
|
||||
<div class="left-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model="searchModel.keyword"
|
||||
:placeholder="t('channel.area.search.placeholder')"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="onFormSubmitClick"
|
||||
@clear="onFormSubmitClick"
|
||||
@keyup.enter="onFormSubmitClick"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="search-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn refresh-btn"
|
||||
@click="onFormSubmitClick"
|
||||
:title="t('channel.area.search.refresh')"
|
||||
>
|
||||
↻
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn add-btn"
|
||||
@click="handleOpenModal('add')"
|
||||
:title="t('channel.area.search.addArea')"
|
||||
>
|
||||
+
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<div
|
||||
v-for="(item, index) in leftMenuData"
|
||||
:key="item.id"
|
||||
class="menu-item"
|
||||
:class="{ 'menu-item-selected': currentId?.id === item.id }"
|
||||
@click="cellClick(item)"
|
||||
>
|
||||
<div class="menu-item-content">
|
||||
<span class="menu-index">{{ index + 1 }}</span>
|
||||
<span class="menu-text">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="menu-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="edit-btn"
|
||||
@click.stop="handleOpenModal('edit', item)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconEdit />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="delete-btn"
|
||||
@click.stop="handleDelData(item.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconDelete />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!leftMenuData.length" class="empty-menu">
|
||||
<a-empty :description="t('channel.area.empty')" />
|
||||
</div>
|
||||
</div>
|
||||
<CustomLeftMenu
|
||||
:tree-data="leftTreeData"
|
||||
:selected-keys="leftSelectedKeys"
|
||||
v-model:expanded-keys="leftExpandedKeys"
|
||||
:search-keyword="searchModel.keyword"
|
||||
@update:search-keyword="(v) => (searchModel.keyword = v)"
|
||||
:search-placeholder="t('channel.area.search.placeholder')"
|
||||
:refresh-title="t('channel.area.search.refresh')"
|
||||
:add-title="t('channel.area.search.addArea')"
|
||||
show-node-index
|
||||
:show-method="false"
|
||||
:show-map-count="false"
|
||||
:show-node-code="false"
|
||||
show-node-actions
|
||||
:delete-label="t('channel.area.actions.delete')"
|
||||
@search="onFormSubmitClick"
|
||||
@refresh="onFormSubmitClick"
|
||||
@add="handleOpenModal('add')"
|
||||
@select="handleSelectFromTree"
|
||||
@delete-node="handleDeleteLeftNode"
|
||||
>
|
||||
<template #node-menu="{ nodeData }">
|
||||
<a-doption value="edit" @click="handleOpenModal('edit', nodeData)">
|
||||
<IconEdit />
|
||||
{{ t('channel.area.actions.edit') }}
|
||||
</a-doption>
|
||||
<a-doption value="delete" @click="handleDelData(nodeData?.id)">
|
||||
<IconDelete />
|
||||
{{ t('channel.area.actions.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
|
||||
<ModalForm
|
||||
ref="modalFormLeftRef"
|
||||
@ -216,6 +170,8 @@
|
||||
|
||||
import { $baseMessage } from '@/utils';
|
||||
import { IconEdit, IconDelete, IconPlus, IconSearch } from '@arco-design/web-vue/es/icon';
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types';
|
||||
|
||||
import TemplateScroll from './components/TemplateScroll.vue';
|
||||
|
||||
@ -486,6 +442,38 @@
|
||||
|
||||
// 左侧菜单数据
|
||||
const leftMenuData = ref<any[]>([]);
|
||||
const leftExpandedKeys = ref<string[]>(['category-list']);
|
||||
|
||||
const leftTreeData = computed<CustomLeftMenuTreeNode[]>(() => [
|
||||
{
|
||||
key: 'category-list',
|
||||
title: t('channel.area.listTitle'),
|
||||
children: leftMenuData.value.map((item) => ({
|
||||
key: `item-${item.id}`,
|
||||
title: item.name,
|
||||
isLeaf: true,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
const leftSelectedKeys = computed(() =>
|
||||
currentId.value?.id ? [`item-${currentId.value.id}`] : []
|
||||
);
|
||||
|
||||
function handleSelectFromTree(keys: string[], info: any) {
|
||||
const key = info.node?.key;
|
||||
if (!key || !key.startsWith('item-')) return;
|
||||
const id = Number(key.replace('item-', ''));
|
||||
const item = leftMenuData.value.find((i) => i.id === id);
|
||||
if (item) cellClick(item);
|
||||
}
|
||||
|
||||
function handleDeleteLeftNode(nodeData: any) {
|
||||
if (nodeData?.id != null) handleDelData(nodeData.id);
|
||||
}
|
||||
|
||||
const tableRef = ref<any>(null);
|
||||
const tableProps = reactive<any>({
|
||||
@ -797,42 +785,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧使用 CustomLeftMenu,仅保留布局(与销售产品一致) */
|
||||
.left-sidebar {
|
||||
width: 280px;
|
||||
background: var(--app-sidebar-bg);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.left-sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
|
||||
77
src/views/channel/labelDesign/index.vue
Normal file
77
src/views/channel/labelDesign/index.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="label-design-page">
|
||||
<FullDesigner
|
||||
ref="designerRef"
|
||||
default-lang="cn"
|
||||
:hi-auto-connect="false"
|
||||
@save="onSave"
|
||||
>
|
||||
<!-- 覆盖默认左侧区域,隐藏 logo + 标题(插槽可能不生效,配合 CSS 隐藏 .header-left) -->
|
||||
<template #headerLeft></template>
|
||||
<!-- 覆盖默认右侧按钮区域,彻底隐藏“反馈”等按钮 -->
|
||||
<template #headerRight></template>
|
||||
</FullDesigner>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { FullDesigner, hiprint } from 'vg-print';
|
||||
import 'vg-print/style.css';
|
||||
|
||||
defineOptions({ name: 'LabelDesign' });
|
||||
|
||||
// 仅在显式开启本地打印客户端时,才注册 hiprint(避免全站无意义的本地 WebSocket 连接)
|
||||
const enableHiprintClient = import.meta.env.VITE_ENABLE_HIPRINT_CLIENT === 'true';
|
||||
|
||||
// 模块级防重复注册
|
||||
let hiprintRegistered = false;
|
||||
|
||||
if (enableHiprintClient && !hiprintRegistered) {
|
||||
hiprint.register({
|
||||
authKey: 'eyJrIjoiZ21jNTc2MDMzNyJ9',
|
||||
});
|
||||
hiprintRegistered = true;
|
||||
}
|
||||
|
||||
const designerRef = ref<InstanceType<typeof FullDesigner> | null>(null);
|
||||
|
||||
function onSave(payload: { template: unknown; data: unknown }) {
|
||||
// 模板 JSON + 打印数据,后续可持久化到后端
|
||||
// console.log('保存模板:', payload);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.label-design-page {
|
||||
padding: 16px;
|
||||
background-color: var(--color-bg-1);
|
||||
min-height: calc(100vh - 86px);
|
||||
}
|
||||
|
||||
/* 顶部栏背景透明(第一行 .primary-toolbar + 第二行 .secondary-toolbar) */
|
||||
.label-design-page :deep(.primary-toolbar),
|
||||
.label-design-page :deep(.primary-toolbar .toolbar-content),
|
||||
.label-design-page :deep(.secondary-toolbar),
|
||||
.label-design-page :deep(.secondary-toolbar .toolbar-content) {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 顶部中间/右侧区域也透明 */
|
||||
.label-design-page :deep(.header-center),
|
||||
.label-design-page :deep(.header-right.toolbar-right) {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* 隐藏 vg-print 顶部左侧 logo + 文本(打包代码中为 .header-left / .toolbar-logo / .toolbar-title) */
|
||||
.label-design-page :deep(.header-left) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 隐藏 vg-print 内置的反馈/帮助按钮(class: btn-feedback) */
|
||||
.label-design-page :deep(.btn-feedback) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
@ -4,84 +4,38 @@
|
||||
<BasicLayoutLeftRight :left-width="280">
|
||||
<template #left>
|
||||
<div class="left-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model="searchModel.keyword"
|
||||
:placeholder="t('channel.area.search.placeholder')"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="onFormSubmitClick"
|
||||
@clear="onFormSubmitClick"
|
||||
@keyup.enter="onFormSubmitClick"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="search-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn refresh-btn"
|
||||
@click="onFormSubmitClick"
|
||||
:title="t('channel.area.search.refresh')"
|
||||
>
|
||||
↻
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn add-btn"
|
||||
@click="handleOpenModal('add')"
|
||||
:title="t('channel.area.search.addArea')"
|
||||
>
|
||||
+
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<div
|
||||
v-for="(item, index) in leftMenuData"
|
||||
:key="item.id"
|
||||
class="menu-item"
|
||||
:class="{ 'menu-item-selected': currentId?.id === item.id }"
|
||||
@click="cellClick(item)"
|
||||
>
|
||||
<div class="menu-item-content">
|
||||
<span class="menu-index">{{ index + 1 }}</span>
|
||||
<span class="menu-text">{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="menu-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="edit-btn"
|
||||
@click.stop="handleOpenModal('edit', item)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconEdit />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="delete-btn"
|
||||
@click.stop="handleDelData(item.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconDelete />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!leftMenuData.length" class="empty-menu">
|
||||
<a-empty :description="t('channel.area.empty')" />
|
||||
</div>
|
||||
</div>
|
||||
<CustomLeftMenu
|
||||
:tree-data="leftTreeData"
|
||||
:selected-keys="leftSelectedKeys"
|
||||
v-model:expanded-keys="leftExpandedKeys"
|
||||
:search-keyword="searchModel.keyword"
|
||||
@update:search-keyword="(v) => (searchModel.keyword = v)"
|
||||
:search-placeholder="t('channel.area.search.placeholder')"
|
||||
:refresh-title="t('channel.area.search.refresh')"
|
||||
:add-title="t('channel.area.search.addArea')"
|
||||
show-node-index
|
||||
:show-method="false"
|
||||
:show-map-count="false"
|
||||
:show-node-code="false"
|
||||
show-node-actions
|
||||
:delete-label="t('channel.area.actions.delete')"
|
||||
@search="onFormSubmitClick"
|
||||
@refresh="onFormSubmitClick"
|
||||
@add="handleOpenModal('add')"
|
||||
@select="handleSelectFromTree"
|
||||
@delete-node="handleDeleteLeftNode"
|
||||
>
|
||||
<template #node-menu="{ nodeData }">
|
||||
<a-doption value="edit" @click="handleOpenModal('edit', nodeData)">
|
||||
<IconEdit />
|
||||
{{ t('channel.area.actions.edit') }}
|
||||
</a-doption>
|
||||
<a-doption value="delete" @click="handleDelData(nodeData?.id)">
|
||||
<IconDelete />
|
||||
{{ t('channel.area.actions.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
|
||||
<ModalForm
|
||||
ref="modalFormLeftRef"
|
||||
@ -216,6 +170,8 @@
|
||||
|
||||
import { $baseMessage } from '@/utils';
|
||||
import { IconEdit, IconDelete, IconPlus, IconSearch } from '@arco-design/web-vue/es/icon';
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types';
|
||||
|
||||
import TemplateScroll from './components/TemplateScroll.vue';
|
||||
|
||||
@ -486,6 +442,38 @@
|
||||
|
||||
// 左侧菜单数据
|
||||
const leftMenuData = ref<any[]>([]);
|
||||
const leftExpandedKeys = ref<string[]>(['category-list']);
|
||||
|
||||
const leftTreeData = computed<CustomLeftMenuTreeNode[]>(() => [
|
||||
{
|
||||
key: 'category-list',
|
||||
title: t('channel.area.listTitle'),
|
||||
children: leftMenuData.value.map((item) => ({
|
||||
key: `item-${item.id}`,
|
||||
title: item.name,
|
||||
isLeaf: true,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
const leftSelectedKeys = computed(() =>
|
||||
currentId.value?.id ? [`item-${currentId.value.id}`] : []
|
||||
);
|
||||
|
||||
function handleSelectFromTree(keys: string[], info: any) {
|
||||
const key = info.node?.key;
|
||||
if (!key || !key.startsWith('item-')) return;
|
||||
const id = Number(key.replace('item-', ''));
|
||||
const item = leftMenuData.value.find((i) => i.id === id);
|
||||
if (item) cellClick(item);
|
||||
}
|
||||
|
||||
function handleDeleteLeftNode(nodeData: any) {
|
||||
if (nodeData?.id != null) handleDelData(nodeData.id);
|
||||
}
|
||||
|
||||
const tableRef = ref<any>(null);
|
||||
const tableProps = reactive<any>({
|
||||
@ -797,42 +785,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧使用 CustomLeftMenu,仅保留布局(与销售产品一致) */
|
||||
.left-sidebar {
|
||||
width: 280px;
|
||||
background: var(--app-sidebar-bg);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.left-sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
import { Message, Modal, Input } from '@arco-design/web-vue';
|
||||
import ModalPro from '@/components/ModalPro/index.vue';
|
||||
import useModal from '@/components/ModalPro/useModal';
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { diffJson } from 'diff';
|
||||
import dayjs from 'dayjs';
|
||||
@ -164,6 +166,50 @@
|
||||
addProductModalVisible.value = false;
|
||||
};
|
||||
|
||||
// 左侧菜单:筛选后的产品列表(按名称搜索)
|
||||
const filteredProductList = computed(() => {
|
||||
if (!searchKeyword.value) return productList.value;
|
||||
return productList.value.filter((p: any) =>
|
||||
p.name?.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
// 左侧菜单:产品树数据(单分类 + 子节点,供 CustomLeftMenu 使用)
|
||||
const productTreeData = computed<CustomLeftMenuTreeNode[]>(() => {
|
||||
const list = filteredProductList.value || [];
|
||||
if (list.length === 0) return [];
|
||||
return [
|
||||
{
|
||||
key: 'category-products',
|
||||
title: '产品',
|
||||
children: list.map((p: any) => ({
|
||||
key: `item-${p.id}`,
|
||||
title: p.name,
|
||||
isLeaf: true,
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
...p,
|
||||
})),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const productSelectedKeys = computed(() =>
|
||||
selectedProduct.value !== null ? [`item-${selectedProduct.value}`] : []
|
||||
);
|
||||
|
||||
const productExpandedKeys = ref(['category-products']);
|
||||
|
||||
function handleProductSelectFromTree(keys: string[], info: any) {
|
||||
const node = info?.node;
|
||||
if (!node || !node.isLeaf) return;
|
||||
const key = node.key;
|
||||
if (!key.startsWith('item-')) return;
|
||||
const id = Number(key.slice(5));
|
||||
const product = filteredProductList.value.find((p: any) => p.id === id);
|
||||
if (product) handleSelectProduct(product);
|
||||
}
|
||||
|
||||
// 选择产品
|
||||
const handleSelectProduct = (product: any) => {
|
||||
selectedProduct.value = product.id;
|
||||
@ -225,6 +271,30 @@
|
||||
activeTab.value = key;
|
||||
};
|
||||
|
||||
// 状态开关滑动切换:记录触摸起点
|
||||
const statusTouchStartX = ref(0)
|
||||
const STATUS_SWIPE_THRESHOLD = 40
|
||||
function onStatusSwipeStart(e: TouchEvent) {
|
||||
statusTouchStartX.value = e.touches[0].clientX
|
||||
}
|
||||
function onStatusSwipeEnd(record: any, e: TouchEvent) {
|
||||
const endX = e.changedTouches[0].clientX
|
||||
if (Math.abs(endX - statusTouchStartX.value) >= STATUS_SWIPE_THRESHOLD) {
|
||||
record.status = !record.status
|
||||
handleTogglePriceStatus(record)
|
||||
}
|
||||
}
|
||||
function onProductSwitchSwipeEnd(nodeData: any, e: TouchEvent) {
|
||||
const endX = e.changedTouches[0].clientX
|
||||
if (Math.abs(endX - statusTouchStartX.value) >= STATUS_SWIPE_THRESHOLD) {
|
||||
const product = productList.value.find((p: any) => p.id === nodeData?.id)
|
||||
if (product) {
|
||||
product.isActive = !product.isActive
|
||||
handleToggleProduct(product)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存选择弹窗
|
||||
const saveConfirmModalVisible = ref(false);
|
||||
const currentSaveType = ref<'config' | 'price' | 'rules'>('config');
|
||||
@ -1569,75 +1639,61 @@
|
||||
|
||||
<template>
|
||||
<div class="sales-product-layout">
|
||||
<!-- 左侧产品列表 -->
|
||||
<!-- 左侧产品列表:使用公共组件 CustomLeftMenu -->
|
||||
<div class="left-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model="searchKeyword"
|
||||
:placeholder="t('channel.salesProducts.search.placeholder')"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="search-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn refresh-btn"
|
||||
@click="handleSearch"
|
||||
:title="t('channel.salesProducts.search.refresh')"
|
||||
>
|
||||
↻
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn add-btn"
|
||||
@click="handleAddProduct"
|
||||
:title="t('channel.salesProducts.search.addProduct')"
|
||||
>
|
||||
+
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-list">
|
||||
<div
|
||||
v-for="product in productList"
|
||||
:key="product.id"
|
||||
:class="['product-item', { active: selectedProduct === product.id }]"
|
||||
@click="handleSelectProduct(product)"
|
||||
>
|
||||
<div class="product-info">
|
||||
<div class="product-header">
|
||||
<div class="product-name">{{ product.name }}</div>
|
||||
<a-switch
|
||||
v-model="product.isActive"
|
||||
size="small"
|
||||
@change="handleToggleProduct(product)"
|
||||
/>
|
||||
</div>
|
||||
<div class="product-meta">
|
||||
<span class="create-time">创建: {{ product.createTime }}</span>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="delete-btn"
|
||||
@click.stop="handleDeleteProduct(product)"
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CustomLeftMenu
|
||||
:tree-data="productTreeData"
|
||||
:selected-keys="productSelectedKeys"
|
||||
v-model:expanded-keys="productExpandedKeys"
|
||||
v-model:search-keyword="searchKeyword"
|
||||
:search-placeholder="t('channel.salesProducts.search.placeholder')"
|
||||
:refresh-title="t('channel.salesProducts.search.refresh')"
|
||||
:add-title="t('channel.salesProducts.search.addProduct')"
|
||||
:show-node-index="true"
|
||||
:show-method="false"
|
||||
:show-map-count="false"
|
||||
:show-node-code="false"
|
||||
:show-node-actions="true"
|
||||
:delete-label="t('channel.salesProducts.actions.delete')"
|
||||
@search="handleSearch"
|
||||
@refresh="handleSearch"
|
||||
@add="handleAddProduct"
|
||||
@select="handleProductSelectFromTree"
|
||||
@delete-node="(nodeData) => {
|
||||
const product = productList.find((p: any) => p.id === nodeData?.id);
|
||||
if (product) handleDeleteProduct(product);
|
||||
}"
|
||||
>
|
||||
<template #node-second-row-extra="{ nodeData }">
|
||||
<table class="product-second-row-table">
|
||||
<tr>
|
||||
<td class="product-row-left">
|
||||
<span class="create-time">创建: {{ nodeData?.createTime }}</span>
|
||||
</td>
|
||||
<td class="product-row-right">
|
||||
<span
|
||||
class="switch-swipe-wrapper"
|
||||
@click.capture.stop="() => {
|
||||
const product = productList.find((p: any) => p.id === nodeData?.id);
|
||||
if (product) { product.isActive = !product.isActive; handleToggleProduct(product); }
|
||||
}"
|
||||
@touchstart.passive="onStatusSwipeStart"
|
||||
@touchend="(e) => onProductSwitchSwipeEnd(nodeData, e)"
|
||||
>
|
||||
<a-switch
|
||||
:model-value="nodeData?.isActive"
|
||||
size="small"
|
||||
type="circle"
|
||||
class="product-active-switch"
|
||||
checked-color="#00b42a"
|
||||
unchecked-color="#f53f3f"
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
</div>
|
||||
|
||||
<!-- 右侧产品配置 -->
|
||||
@ -1664,6 +1720,14 @@
|
||||
<span style="margin-left: 16px; color: #8c8c8c;">生效时间:{{ effectiveTime }}</span>
|
||||
</div>
|
||||
<div class="price-header-actions">
|
||||
<a-button
|
||||
type="secondary"
|
||||
class="add-price-btn"
|
||||
@click="handleAddPrice"
|
||||
>
|
||||
<icon-plus class="btn-icon" />
|
||||
{{ t('channel.salesProducts.actions.add') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="save-btn"
|
||||
@ -1671,14 +1735,6 @@
|
||||
>
|
||||
{{ t('channel.salesProducts.actions.save') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="secondary"
|
||||
class="add-price-btn"
|
||||
@click="handleAddPrice"
|
||||
>
|
||||
{{ t('channel.salesProducts.actions.add') }}
|
||||
<icon-plus class="btn-icon" />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-table-container">
|
||||
@ -1690,11 +1746,21 @@
|
||||
class="price-table"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-switch
|
||||
v-model="record.status"
|
||||
size="small"
|
||||
@change="handleTogglePriceStatus(record)"
|
||||
/>
|
||||
<div
|
||||
class="switch-swipe-wrapper"
|
||||
@click.capture.stop="() => { record.status = !record.status; handleTogglePriceStatus(record); }"
|
||||
@touchstart.passive="onStatusSwipeStart"
|
||||
@touchend="(e) => onStatusSwipeEnd(record, e)"
|
||||
>
|
||||
<a-switch
|
||||
:model-value="record.status"
|
||||
type="circle"
|
||||
size="small"
|
||||
class="product-active-switch"
|
||||
checked-color="#00b42a"
|
||||
unchecked-color="#f53f3f"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
@ -2870,204 +2936,65 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 左侧使用 CustomLeftMenu,仅保留布局(参考应用列表) */
|
||||
.left-sidebar {
|
||||
width: 280px;
|
||||
background: var(--app-sidebar-bg);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.left-sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 10px 12px !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
min-height: 48px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 第二行表格:创建时间对齐 node-title,开关对齐 node-actions-cell */
|
||||
.product-second-row-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-icon-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
.product-row-left {
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.add-btn {
|
||||
color: var(--app-primary-color) !important;
|
||||
background-color: var(--app-primary-color-light) !important;
|
||||
border: 1px solid var(--app-primary-color) !important;
|
||||
border-radius: 50% !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-width: 28px !important;
|
||||
max-width: 28px !important;
|
||||
min-height: 28px !important;
|
||||
max-height: 28px !important;
|
||||
padding: 0 !important;
|
||||
line-height: 28px !important;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--app-primary-color) !important;
|
||||
border-color: var(--app-primary-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.refresh-btn {
|
||||
color: #fff;
|
||||
&:hover {
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
padding-left: 24px; /* 与 node-title 左侧对齐:node-number(min-width 16px + margin-right 8px) */
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
.create-time {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.product-list {
|
||||
flex: 1;
|
||||
padding: 8px 0 0;
|
||||
overflow-y: auto;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
border-radius: 16px;
|
||||
padding-right: 4px;
|
||||
.product-row-right {
|
||||
text-align: right;
|
||||
padding: 0;
|
||||
width: 30px; /* 与 CustomLeftMenu 的 node-actions-cell 同宽 */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.product-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
/* 开关:启用-绿色,停用-红色(与应用列表一致) */
|
||||
.left-sidebar :deep(.product-active-switch.arco-switch-checked) {
|
||||
background-color: #00b42a !important;
|
||||
}
|
||||
.left-sidebar :deep(.product-active-switch:not(.arco-switch-checked)) {
|
||||
background-color: #f53f3f !important;
|
||||
}
|
||||
|
||||
.product-list::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.product-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(86, 144, 236, 0.65);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.product-list::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(59, 97, 148, 0.85);
|
||||
}
|
||||
|
||||
.product-item {
|
||||
:deep(.node-second-row-extra-slot) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 10px 12px;
|
||||
margin: 8px 0;
|
||||
border-radius: 18px;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(13, 29, 61, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #FFF394;
|
||||
box-shadow: 0 10px 20px rgba(255, 243, 148, 0.4);
|
||||
border-color: #FFF394 !important;
|
||||
color: #000 !important;
|
||||
|
||||
.product-name,
|
||||
.create-time {
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.product-info {
|
||||
width: 100%;
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.product-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
.create-time {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&:hover {
|
||||
color: #f53f3f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.node-second-row-extra-cell) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 选中时创建时间保持深色,不变白 */
|
||||
:deep(.tree-node.selected .create-time) {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.right-content.content-panel {
|
||||
@ -3079,6 +3006,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.switch-swipe-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tabs-section {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 0 24px;
|
||||
@ -3229,41 +3160,28 @@
|
||||
|
||||
.action-btn {
|
||||
color: var(--app-primary-color);
|
||||
|
||||
:deep(svg) {
|
||||
color: var(--app-primary-color);
|
||||
fill: var(--app-primary-color);
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
color: #1a6b6b;
|
||||
background-color: #f0f9f9;
|
||||
|
||||
:deep(svg) {
|
||||
color: #1a6b6b;
|
||||
fill: #1a6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.delete-btn {
|
||||
color: #f53f3f;
|
||||
|
||||
:deep(svg) {
|
||||
color: #f53f3f;
|
||||
fill: #f53f3f;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
color: #d32f2f;
|
||||
background-color: #fff1f0;
|
||||
|
||||
:deep(svg) {
|
||||
color: #d32f2f;
|
||||
fill: #d32f2f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.product-active-switch.arco-switch-checked) {
|
||||
background-color: #00b42a !important;
|
||||
}
|
||||
:deep(.product-active-switch:not(.arco-switch-checked)) {
|
||||
background-color: #f53f3f !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3706,38 +3624,18 @@
|
||||
|
||||
.action-btn {
|
||||
color: var(--app-primary-color);
|
||||
|
||||
:deep(svg) {
|
||||
color: var(--app-primary-color);
|
||||
fill: var(--app-primary-color);
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
color: #1a6b6b;
|
||||
background-color: #f0f9f9;
|
||||
|
||||
:deep(svg) {
|
||||
color: #1a6b6b;
|
||||
fill: #1a6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.delete-btn {
|
||||
color: #f53f3f;
|
||||
|
||||
:deep(svg) {
|
||||
color: #f53f3f;
|
||||
fill: #f53f3f;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
color: #d32f2f;
|
||||
background-color: #fff1f0;
|
||||
|
||||
:deep(svg) {
|
||||
color: #d32f2f;
|
||||
fill: #d32f2f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,111 +1,60 @@
|
||||
<template>
|
||||
<div class="service-channel-layout">
|
||||
<!-- 左侧渠道列表 -->
|
||||
<!-- 左侧渠道列表:使用公共组件 CustomLeftMenu(参考销售产品样式) -->
|
||||
<div class="channel-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model="searchKeyword"
|
||||
:placeholder="t('channel.serviceChannels.search.placeholder')"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="search-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn refresh-btn"
|
||||
@click="handleSearch"
|
||||
:title="t('channel.serviceChannels.search.refresh')"
|
||||
>
|
||||
↻
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn add-btn"
|
||||
@click="handleAddProvider"
|
||||
:title="t('channel.serviceChannels.search.addProvider')"
|
||||
>
|
||||
+
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="channel-list">
|
||||
<div class="channel-group" v-for="group in channelGroups" :key="group.id">
|
||||
<div class="group-header">
|
||||
<div class="group-info">
|
||||
<icon-folder />
|
||||
<span class="group-name">{{ group.name }}</span>
|
||||
</div>
|
||||
<div class="group-actions">
|
||||
<a-dropdown trigger="click" position="br">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="group-action-btn more-btn"
|
||||
<CustomLeftMenu
|
||||
:tree-data="channelTreeData"
|
||||
:selected-keys="channelSelectedKeys"
|
||||
v-model:expanded-keys="channelExpandedKeys"
|
||||
v-model:search-keyword="searchKeyword"
|
||||
:search-placeholder="t('channel.serviceChannels.search.placeholder')"
|
||||
:refresh-title="t('channel.serviceChannels.search.refresh')"
|
||||
:add-title="t('channel.serviceChannels.search.addProvider')"
|
||||
:show-node-index="true"
|
||||
:show-method="false"
|
||||
:show-map-count="false"
|
||||
:show-node-code="false"
|
||||
:show-node-actions="true"
|
||||
:delete-label="t('channel.serviceChannels.actions.delete')"
|
||||
@search="handleSearch"
|
||||
@refresh="handleSearch"
|
||||
@add="handleAddProvider"
|
||||
@select="handleChannelSelectFromTree"
|
||||
@delete-node="(nodeData) => {
|
||||
const channel = findChannelById(nodeData?.id)
|
||||
if (channel) handleDeleteChannel(channel)
|
||||
}"
|
||||
>
|
||||
<template #node-second-row-extra="{ nodeData }">
|
||||
<table class="channel-second-row-table">
|
||||
<tr>
|
||||
<td class="channel-row-left">
|
||||
<span class="create-time">{{ t('channel.serviceChannels.createTime', { time: nodeData?.createTime }) }}</span>
|
||||
</td>
|
||||
<td class="channel-row-right">
|
||||
<span
|
||||
class="switch-swipe-wrapper"
|
||||
@click.capture.stop="() => {
|
||||
const channel = findChannelById(nodeData?.id)
|
||||
if (channel) { channel.isActive = !channel.isActive; handleToggleChannel(channel); }
|
||||
}"
|
||||
@touchstart.passive="onStatusSwipeStart"
|
||||
@touchend="(e) => onChannelSwitchSwipeEnd(nodeData, e)"
|
||||
>
|
||||
<icon-more />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleAddChannelToGroup(group)">
|
||||
<icon-plus style="margin-right: 8px;" />
|
||||
新增服务渠道
|
||||
</a-doption>
|
||||
<a-doption @click="handleEditGroup(group)">
|
||||
<icon-edit style="margin-right: 8px;" />
|
||||
编辑服务商
|
||||
</a-doption>
|
||||
<a-doption @click="handleDeleteGroup(group)" class="delete-option">
|
||||
<icon-delete style="margin-right: 8px;" />
|
||||
删除服务商
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group-channels">
|
||||
<div
|
||||
v-for="channel in group.channels"
|
||||
:key="channel.id"
|
||||
class="channel-item"
|
||||
:class="{ active: selectedChannel?.id === channel.id }"
|
||||
@click="selectChannel(channel)"
|
||||
>
|
||||
<div class="channel-info">
|
||||
<div class="channel-header">
|
||||
<div class="channel-name">{{ channel.name }}</div>
|
||||
<a-switch
|
||||
v-model="channel.isActive"
|
||||
size="small"
|
||||
@change="handleToggleChannel(channel)"
|
||||
<a-switch
|
||||
:model-value="nodeData?.isActive"
|
||||
size="small"
|
||||
type="circle"
|
||||
class="channel-active-switch"
|
||||
checked-color="#00b42a"
|
||||
unchecked-color="#f53f3f"
|
||||
/>
|
||||
</div>
|
||||
<div class="channel-meta">
|
||||
<span class="create-time">{{ t('channel.serviceChannels.createTime', { time: channel.createTime }) }}</span>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="delete-btn"
|
||||
@click.stop="handleDeleteChannel(channel)"
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
</div>
|
||||
|
||||
<!-- 右侧配置区域 -->
|
||||
@ -217,6 +166,14 @@
|
||||
<span style="margin-left: 16px; color: #8c8c8c;">生效时间:{{ effectiveTime }}</span>
|
||||
</div>
|
||||
<div class="price-header-actions">
|
||||
<a-button
|
||||
type="secondary"
|
||||
class="add-price-btn"
|
||||
@click="handleAddPrice"
|
||||
>
|
||||
<icon-plus class="btn-icon" />
|
||||
{{ t('channel.serviceChannels.actions.add') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="save-btn"
|
||||
@ -224,14 +181,6 @@
|
||||
>
|
||||
{{ t('channel.serviceChannels.actions.save') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="secondary"
|
||||
class="add-price-btn"
|
||||
@click="handleAddPrice"
|
||||
>
|
||||
{{ t('channel.serviceChannels.actions.add') }}
|
||||
<icon-plus class="btn-icon" />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-table-container">
|
||||
@ -243,11 +192,21 @@
|
||||
class="price-table"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-switch
|
||||
v-model="record.status"
|
||||
size="small"
|
||||
@change="handleTogglePriceStatus(record)"
|
||||
/>
|
||||
<div
|
||||
class="switch-swipe-wrapper"
|
||||
@click.capture.stop="() => { record.status = !record.status; handleTogglePriceStatus(record); }"
|
||||
@touchstart.passive="onStatusSwipeStart"
|
||||
@touchend="(e) => onStatusSwipeEnd(record, e)"
|
||||
>
|
||||
<a-switch
|
||||
:model-value="record.status"
|
||||
type="circle"
|
||||
size="small"
|
||||
class="product-active-switch"
|
||||
checked-color="#00b42a"
|
||||
unchecked-color="#f53f3f"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
@ -1118,7 +1077,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, watch, nextTick, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { IconPlus, IconDelete, IconSearch, IconEdit, IconDragArrow, IconQuestionCircleFill, IconFolder, IconMore } from '@arco-design/web-vue/es/icon'
|
||||
import { Message, Modal, Input } from '@arco-design/web-vue'
|
||||
@ -1130,6 +1089,8 @@ import * as XLSX from 'xlsx'
|
||||
import Sortable from 'sortablejs'
|
||||
import { getAppGetList } from '@/api/ApplicationServices/apps'
|
||||
import { getServiceProviderGetServiceProviderDropdownList } from '@/api/interface-serviceProvider/serviceProvider'
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue'
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types'
|
||||
|
||||
defineOptions({
|
||||
name: 'ServiceChannel'
|
||||
@ -1187,6 +1148,74 @@ const channelGroups = ref([
|
||||
}
|
||||
])
|
||||
|
||||
// 左侧菜单:按关键词筛选渠道组(组名或渠道名)
|
||||
const filteredChannelGroups = computed(() => {
|
||||
const kw = searchKeyword.value?.toLowerCase() || ''
|
||||
if (!kw) return channelGroups.value
|
||||
return channelGroups.value
|
||||
.map((group: any) => {
|
||||
const nameMatch = group.name?.toLowerCase().includes(kw)
|
||||
const channels = (group.channels || []).filter((ch: any) =>
|
||||
ch.name?.toLowerCase().includes(kw)
|
||||
)
|
||||
if (nameMatch || channels.length > 0) {
|
||||
return { ...group, channels: nameMatch ? (group.channels || []) : channels }
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
// 左侧菜单:渠道树数据(组为父节点,渠道为子节点)
|
||||
const channelTreeData = computed<CustomLeftMenuTreeNode[]>(() => {
|
||||
const groups = filteredChannelGroups.value || []
|
||||
return groups.map((group: any) => ({
|
||||
key: `group-${group.id}`,
|
||||
title: group.name,
|
||||
children: (group.channels || []).map((ch: any) => ({
|
||||
key: `item-${ch.id}`,
|
||||
title: ch.name,
|
||||
isLeaf: true,
|
||||
id: ch.id,
|
||||
name: ch.name,
|
||||
...ch
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
const channelSelectedKeys = computed(() =>
|
||||
selectedChannel.value ? [`item-${(selectedChannel.value as any).id}`] : []
|
||||
)
|
||||
|
||||
const channelExpandedKeys = ref<string[]>(
|
||||
channelGroups.value.map((g: any) => `group-${g.id}`)
|
||||
)
|
||||
watch(
|
||||
() => channelGroups.value.map((g: any) => `group-${g.id}`),
|
||||
(keys) => {
|
||||
channelExpandedKeys.value = keys
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
function findChannelById(id: number | string) {
|
||||
for (const group of channelGroups.value) {
|
||||
const ch = (group.channels || []).find((c: any) => c.id === id)
|
||||
if (ch) return ch
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function handleChannelSelectFromTree(keys: string[], info: any) {
|
||||
const node = info?.node
|
||||
if (!node || !node.isLeaf) return
|
||||
const key = node.key
|
||||
if (!key.startsWith('item-')) return
|
||||
const id = Number(key.slice(5))
|
||||
const channel = findChannelById(id)
|
||||
if (channel) selectChannel(channel)
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
channelCode: '',
|
||||
@ -2190,6 +2219,30 @@ const handleTogglePriceStatus = (record: any) => {
|
||||
record.status = !record.status
|
||||
}
|
||||
|
||||
// 状态开关滑动切换
|
||||
const statusTouchStartX = ref(0)
|
||||
const STATUS_SWIPE_THRESHOLD = 40
|
||||
function onStatusSwipeStart(e: TouchEvent) {
|
||||
statusTouchStartX.value = e.touches[0].clientX
|
||||
}
|
||||
function onStatusSwipeEnd(record: any, e: TouchEvent) {
|
||||
const endX = e.changedTouches[0].clientX
|
||||
if (Math.abs(endX - statusTouchStartX.value) >= STATUS_SWIPE_THRESHOLD) {
|
||||
record.status = !record.status
|
||||
handleTogglePriceStatus(record)
|
||||
}
|
||||
}
|
||||
function onChannelSwitchSwipeEnd(nodeData: any, e: TouchEvent) {
|
||||
const endX = e.changedTouches[0].clientX
|
||||
if (Math.abs(endX - statusTouchStartX.value) >= STATUS_SWIPE_THRESHOLD) {
|
||||
const channel = findChannelById(nodeData?.id)
|
||||
if (channel) {
|
||||
channel.isActive = !channel.isActive
|
||||
handleToggleChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增服务商
|
||||
const handleAddProvider = () => {
|
||||
editProviderForm.providerName = ''
|
||||
@ -2822,275 +2875,64 @@ const handleCancelSave = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 左侧使用 CustomLeftMenu,仅保留布局(参考销售产品) */
|
||||
.channel-sidebar {
|
||||
width: 280px;
|
||||
background: var(--app-sidebar-bg);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channel-sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 10px 12px !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
min-height: 48px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 第二行表格:创建时间对齐 node-title,开关对齐 node-actions-cell */
|
||||
.channel-second-row-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action-icon-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
.channel-row-left {
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.add-btn {
|
||||
color: var(--app-primary-color) !important;
|
||||
background-color: var(--app-primary-color-light) !important;
|
||||
border: 1px solid var(--app-primary-color) !important;
|
||||
border-radius: 50% !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-width: 28px !important;
|
||||
max-width: 28px !important;
|
||||
min-height: 28px !important;
|
||||
max-height: 28px !important;
|
||||
padding: 0 !important;
|
||||
line-height: 28px !important;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--app-primary-color) !important;
|
||||
border-color: var(--app-primary-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.refresh-btn {
|
||||
color: #fff;
|
||||
&:hover {
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
padding-left: 24px;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
.create-time {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-list {
|
||||
flex: 1;
|
||||
padding: 8px 0 0;
|
||||
overflow-y: auto;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
border-radius: 16px;
|
||||
padding-right: 4px;
|
||||
.channel-row-right {
|
||||
text-align: right;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.channel-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
.channel-sidebar :deep(.channel-active-switch.arco-switch-checked) {
|
||||
background-color: #00b42a !important;
|
||||
}
|
||||
.channel-sidebar :deep(.channel-active-switch:not(.arco-switch-checked)) {
|
||||
background-color: #f53f3f !important;
|
||||
}
|
||||
|
||||
.channel-list::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.channel-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(86, 144, 236, 0.65);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.channel-list::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(59, 97, 148, 0.85);
|
||||
}
|
||||
|
||||
.channel-group {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 8px;
|
||||
|
||||
.group-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.group-name {
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
gap: 0px;
|
||||
align-items: center;
|
||||
|
||||
.group-action-btn {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&.more-btn {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉菜单选项样式
|
||||
:deep(.arco-dropdown-option) {
|
||||
&.delete-option {
|
||||
color: #f53f3f;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff1f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-channels {
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.channel-item {
|
||||
:deep(.node-second-row-extra-slot) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding: 10px 12px;
|
||||
margin: 8px 0;
|
||||
border-radius: 18px;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(13, 29, 61, 0.8);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #FFF394;
|
||||
box-shadow: 0 10px 20px rgba(255, 243, 148, 0.4);
|
||||
border-color: #FFF394 !important;
|
||||
color: #000 !important;
|
||||
|
||||
.channel-name,
|
||||
.create-time {
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-info {
|
||||
width: 100%;
|
||||
|
||||
.channel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.channel-name {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
.create-time {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
&:hover {
|
||||
color: #f53f3f;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.node-second-row-extra-cell) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 选中时创建时间保持深色,不变白 */
|
||||
:deep(.tree-node.selected .create-time) {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.channel-content.content-panel {
|
||||
@ -3102,6 +2944,10 @@ const handleCancelSave = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.switch-swipe-wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tabs-section {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
padding: 0 24px;
|
||||
@ -3320,6 +3166,13 @@ const handleCancelSave = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.product-active-switch.arco-switch-checked) {
|
||||
background-color: #00b42a !important;
|
||||
}
|
||||
:deep(.product-active-switch:not(.arco-switch-checked)) {
|
||||
background-color: #f53f3f !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,95 +4,48 @@
|
||||
<BasicLayoutLeftRight :left-width="280">
|
||||
<template #left>
|
||||
<div class="left-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model="searchModel.keyword"
|
||||
:placeholder="t('channel.weightRounding.search.placeholder')"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="onFormSubmitClick"
|
||||
@clear="onFormSubmitClick"
|
||||
@keyup.enter="onFormSubmitClick"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="search-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn refresh-btn"
|
||||
@click="onFormSubmitClick"
|
||||
:title="t('channel.weightRounding.search.refresh')"
|
||||
>
|
||||
↻
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn add-btn"
|
||||
@click="handleOpenModal('add')"
|
||||
:title="t('channel.weightRounding.search.addRounding')"
|
||||
>
|
||||
+
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<div
|
||||
v-for="(item, index) in leftMenuData"
|
||||
:key="item.id"
|
||||
class="menu-item"
|
||||
:class="{ 'menu-item-selected': currentId?.id === item.id }"
|
||||
@click="cellClick(item)"
|
||||
>
|
||||
<div class="menu-item-content">
|
||||
<span class="menu-text">{{ item.name }}</span>
|
||||
<div class="menu-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="edit-btn"
|
||||
@click.stop="handleOpenModal('edit', item)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconEdit />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="delete-btn"
|
||||
@click.stop="handleDelData(item.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconDelete />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-tags">
|
||||
<span
|
||||
v-for="tag in item.tags"
|
||||
:key="tag"
|
||||
class="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="menu-update">
|
||||
{{ t('channel.weightRounding.updateTime', { time: item.updateTime }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!leftMenuData.length" class="empty-menu">
|
||||
<a-empty :description="t('channel.weightRounding.empty')" />
|
||||
</div>
|
||||
</div>
|
||||
<CustomLeftMenu
|
||||
:tree-data="leftTreeData"
|
||||
:selected-keys="leftSelectedKeys"
|
||||
v-model:expanded-keys="leftExpandedKeys"
|
||||
:search-keyword="searchModel.keyword"
|
||||
@update:search-keyword="(v) => (searchModel.keyword = v)"
|
||||
:search-placeholder="t('channel.weightRounding.search.placeholder')"
|
||||
:refresh-title="t('channel.weightRounding.search.refresh')"
|
||||
:add-title="t('channel.weightRounding.search.addRounding')"
|
||||
show-node-index
|
||||
:show-method="false"
|
||||
:show-map-count="false"
|
||||
:show-node-code="false"
|
||||
show-node-actions
|
||||
:delete-label="t('channel.weightRounding.actions.delete')"
|
||||
@search="onFormSubmitClick"
|
||||
@refresh="onFormSubmitClick"
|
||||
@add="handleOpenModal('add')"
|
||||
@select="handleSelectFromTree"
|
||||
@delete-node="handleDeleteLeftNode"
|
||||
>
|
||||
<template #node-menu="{ nodeData }">
|
||||
<a-doption value="edit" @click="handleOpenModal('edit', nodeData)">
|
||||
<IconEdit />
|
||||
{{ t('channel.area.actions.edit') }}
|
||||
</a-doption>
|
||||
<a-doption value="delete" @click="handleDelData(nodeData?.id)">
|
||||
<IconDelete />
|
||||
{{ t('channel.weightRounding.actions.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
<template #node-second-row-extra="{ nodeData }">
|
||||
<table class="product-second-row-table">
|
||||
<tr>
|
||||
<td class="product-row-left">
|
||||
<span class="update-time">{{ t('channel.weightRounding.updateTime', { time: nodeData?.updateTime ?? '' }) }}</span>
|
||||
</td>
|
||||
<td class="product-row-right"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
|
||||
<ModalForm
|
||||
ref="modalFormLeftRef"
|
||||
@ -217,6 +170,8 @@
|
||||
|
||||
import { $baseMessage } from '@/utils';
|
||||
import { IconEdit, IconDelete, IconPlus, IconSearch } from '@arco-design/web-vue/es/icon';
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
@ -420,6 +375,38 @@
|
||||
updateTime: '2023.6.6 12:45'
|
||||
}
|
||||
]);
|
||||
const leftExpandedKeys = ref<string[]>(['category-list']);
|
||||
|
||||
const leftTreeData = computed<CustomLeftMenuTreeNode[]>(() => [
|
||||
{
|
||||
key: 'category-list',
|
||||
title: t('channel.weightRounding.listTitle'),
|
||||
children: leftMenuData.value.map((item) => ({
|
||||
key: `item-${item.id}`,
|
||||
title: item.name,
|
||||
isLeaf: true,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
const leftSelectedKeys = computed(() =>
|
||||
currentId.value?.id ? [`item-${currentId.value.id}`] : []
|
||||
);
|
||||
|
||||
function handleSelectFromTree(keys: string[], info: any) {
|
||||
const key = info.node?.key;
|
||||
if (!key || !key.startsWith('item-')) return;
|
||||
const id = Number(key.replace('item-', ''));
|
||||
const item = leftMenuData.value.find((i) => i.id === id);
|
||||
if (item) cellClick(item);
|
||||
}
|
||||
|
||||
function handleDeleteLeftNode(nodeData: any) {
|
||||
if (nodeData?.id != null) handleDelData(nodeData.id);
|
||||
}
|
||||
|
||||
// 表格数据
|
||||
const roundingData = ref<any[]>([
|
||||
@ -763,42 +750,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧使用 CustomLeftMenu,仅保留布局(与销售产品一致) */
|
||||
.left-sidebar {
|
||||
width: 280px;
|
||||
background: var(--app-sidebar-bg);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
/* 第二行表格:更新时间布局与样式对齐销售产品的创建时间 */
|
||||
.left-sidebar .product-second-row-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.left-sidebar .product-row-left {
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
padding-left: 24px;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
.update-time {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.left-sidebar .product-row-right {
|
||||
text-align: right;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.left-sidebar :deep(.node-second-row-extra-slot) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-sidebar :deep(.node-second-row-extra-cell) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.left-sidebar :deep(.tree-node.selected .update-time) {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
|
||||
@ -4,95 +4,48 @@
|
||||
<BasicLayoutLeftRight :left-width="280">
|
||||
<template #left>
|
||||
<div class="left-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="search-box">
|
||||
<a-input
|
||||
v-model="searchModel.keyword"
|
||||
:placeholder="t('channel.weightRules.search.placeholder')"
|
||||
allow-clear
|
||||
size="small"
|
||||
@change="onFormSubmitClick"
|
||||
@clear="onFormSubmitClick"
|
||||
@keyup.enter="onFormSubmitClick"
|
||||
>
|
||||
<template #prefix>
|
||||
<icon-search />
|
||||
</template>
|
||||
</a-input>
|
||||
<div class="search-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn refresh-btn"
|
||||
@click="onFormSubmitClick"
|
||||
:title="t('channel.weightRules.search.refresh')"
|
||||
>
|
||||
↻
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="action-icon-btn add-btn"
|
||||
@click="handleOpenModal('add')"
|
||||
:title="t('channel.weightRules.search.addRule')"
|
||||
>
|
||||
+
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-list">
|
||||
<div
|
||||
v-for="(item, index) in leftMenuData"
|
||||
:key="item.id"
|
||||
class="menu-item"
|
||||
:class="{ 'menu-item-selected': currentId?.id === item.id }"
|
||||
@click="cellClick(item)"
|
||||
>
|
||||
<div class="menu-item-content">
|
||||
<span class="menu-text">{{ item.name }}</span>
|
||||
<div class="menu-actions">
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="edit-btn"
|
||||
@click.stop="handleOpenModal('edit', item)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconEdit />
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="delete-btn"
|
||||
@click.stop="handleDelData(item.id)"
|
||||
>
|
||||
<template #icon>
|
||||
<IconDelete />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-tags">
|
||||
<span
|
||||
v-for="tag in item.tags"
|
||||
:key="tag"
|
||||
class="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="menu-update">
|
||||
{{ t('channel.weightRules.updateTime', { time: item.updateTime }) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="!leftMenuData.length" class="empty-menu">
|
||||
<a-empty :description="t('channel.weightRules.empty')" />
|
||||
</div>
|
||||
</div>
|
||||
<CustomLeftMenu
|
||||
:tree-data="leftTreeData"
|
||||
:selected-keys="leftSelectedKeys"
|
||||
v-model:expanded-keys="leftExpandedKeys"
|
||||
:search-keyword="searchModel.keyword"
|
||||
@update:search-keyword="(v) => (searchModel.keyword = v)"
|
||||
:search-placeholder="t('channel.weightRules.search.placeholder')"
|
||||
:refresh-title="t('channel.weightRules.search.refresh')"
|
||||
:add-title="t('channel.weightRules.search.addRule')"
|
||||
show-node-index
|
||||
:show-method="false"
|
||||
:show-map-count="false"
|
||||
:show-node-code="false"
|
||||
show-node-actions
|
||||
:delete-label="t('channel.weightRules.actions.delete')"
|
||||
@search="onFormSubmitClick"
|
||||
@refresh="onFormSubmitClick"
|
||||
@add="handleOpenModal('add')"
|
||||
@select="handleSelectFromTree"
|
||||
@delete-node="handleDeleteLeftNode"
|
||||
>
|
||||
<template #node-menu="{ nodeData }">
|
||||
<a-doption value="edit" @click="handleOpenModal('edit', nodeData)">
|
||||
<IconEdit />
|
||||
{{ t('channel.area.actions.edit') }}
|
||||
</a-doption>
|
||||
<a-doption value="delete" @click="handleDelData(nodeData?.id)">
|
||||
<IconDelete />
|
||||
{{ t('channel.weightRules.actions.delete') }}
|
||||
</a-doption>
|
||||
</template>
|
||||
<template #node-second-row-extra="{ nodeData }">
|
||||
<table class="product-second-row-table">
|
||||
<tr>
|
||||
<td class="product-row-left">
|
||||
<span class="update-time">{{ t('channel.weightRules.updateTime', { time: nodeData?.updateTime ?? '' }) }}</span>
|
||||
</td>
|
||||
<td class="product-row-right"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
|
||||
<ModalForm
|
||||
ref="modalFormLeftRef"
|
||||
@ -217,6 +170,8 @@
|
||||
|
||||
import { $baseMessage } from '@/utils';
|
||||
import { IconEdit, IconDelete, IconPlus, IconSearch } from '@arco-design/web-vue/es/icon';
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue';
|
||||
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
@ -421,6 +376,38 @@
|
||||
updateTime: '2023.6.6 12:45'
|
||||
}
|
||||
]);
|
||||
const leftExpandedKeys = ref<string[]>(['category-list']);
|
||||
|
||||
const leftTreeData = computed<CustomLeftMenuTreeNode[]>(() => [
|
||||
{
|
||||
key: 'category-list',
|
||||
title: t('channel.weightRules.listTitle'),
|
||||
children: leftMenuData.value.map((item) => ({
|
||||
key: `item-${item.id}`,
|
||||
title: item.name,
|
||||
isLeaf: true,
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
...item,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
const leftSelectedKeys = computed(() =>
|
||||
currentId.value?.id ? [`item-${currentId.value.id}`] : []
|
||||
);
|
||||
|
||||
function handleSelectFromTree(keys: string[], info: any) {
|
||||
const key = info.node?.key;
|
||||
if (!key || !key.startsWith('item-')) return;
|
||||
const id = Number(key.replace('item-', ''));
|
||||
const item = leftMenuData.value.find((i) => i.id === id);
|
||||
if (item) cellClick(item);
|
||||
}
|
||||
|
||||
function handleDeleteLeftNode(nodeData: any) {
|
||||
if (nodeData?.id != null) handleDelData(nodeData.id);
|
||||
}
|
||||
|
||||
// 表格数据
|
||||
const rulesData = ref<any[]>([
|
||||
@ -756,42 +743,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧使用 CustomLeftMenu,仅保留布局(与销售产品一致) */
|
||||
.left-sidebar {
|
||||
width: 280px;
|
||||
background: var(--app-sidebar-bg);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
padding: 12px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-sidebar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
/* 第二行表格:更新时间布局与样式对齐销售产品的创建时间 */
|
||||
.left-sidebar .product-second-row-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.left-sidebar .product-row-left {
|
||||
text-align: left;
|
||||
padding: 0;
|
||||
padding-left: 24px;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
|
||||
.update-time {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.left-sidebar .product-row-right {
|
||||
text-align: right;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.left-sidebar :deep(.node-second-row-extra-slot) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-sidebar :deep(.node-second-row-extra-cell) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.left-sidebar :deep(.tree-node.selected .update-time) {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
|
||||
@ -345,6 +345,7 @@ async function handleModalOk() {
|
||||
icon: formState.icon || undefined,
|
||||
route: formState.route || undefined,
|
||||
component: formState.component || undefined,
|
||||
order: formState.order ?? undefined,
|
||||
isHidden: formState.isHidden,
|
||||
isCached: formState.isCached,
|
||||
description: formState.description || undefined,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { resolve } from 'path';
|
||||
import type { Plugin, UserConfig } from 'vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
@ -15,7 +16,6 @@ export default defineConfig({
|
||||
fs: {
|
||||
strict: true,
|
||||
},
|
||||
historyApiFallback: true,
|
||||
// 添加中间件,在代理之前拦截前端路由
|
||||
middlewareMode: false,
|
||||
proxy: {
|
||||
@ -176,9 +176,10 @@ export default defineConfig({
|
||||
console.log('[Vite Middleware] Intercepting frontend route:', url);
|
||||
// 标记请求,让代理中间件知道不要代理
|
||||
(req as any).__skipProxy = true;
|
||||
// 标记为前端路由
|
||||
(req as any).__isFrontendRoute = true;
|
||||
// 继续处理,让 Vite 的 SPA 路由处理(通过 historyApiFallback)
|
||||
// 关键:将 URL 重写为 /index.html,否则以 /api 开头的路径在 Vite 中不会走 SPA 回退,会直接 404
|
||||
// /track/* 等非 /api 路径会正常走 SPA 回退,所以只有 /api/* 前端路由需要显式重写
|
||||
req.url = '/index.html';
|
||||
next();
|
||||
return;
|
||||
}
|
||||
@ -212,13 +213,11 @@ export default defineConfig({
|
||||
// 注意:如果服务器不支持压缩,可以禁用此插件
|
||||
// 只保留 brotli 压缩(压缩率更高,文件数量减少一半)
|
||||
// 如果需要同时支持 gzip 和 brotli,取消下面的注释
|
||||
...(process.env.NODE_ENV === 'production'
|
||||
? [
|
||||
// 只启用 brotli 压缩(减少文件数量)
|
||||
...configCompressPlugin('brotli'),
|
||||
// 如果需要同时支持 gzip,取消下面的注释
|
||||
// ...configCompressPlugin('gzip'),
|
||||
]
|
||||
...(process.env.NODE_ENV === 'production'
|
||||
? ((): Plugin[] => {
|
||||
const p = configCompressPlugin('brotli');
|
||||
return Array.isArray(p) ? p : [p];
|
||||
})()
|
||||
: []),
|
||||
],
|
||||
resolve: {
|
||||
@ -278,75 +277,31 @@ export default defineConfig({
|
||||
build: {
|
||||
// 启用压缩(使用esbuild,Vite默认,性能更好)
|
||||
minify: 'esbuild',
|
||||
// 生产环境移除console和debugger
|
||||
// 生产环境移除 console 和 debugger
|
||||
esbuild: {
|
||||
drop: process.env.NODE_ENV === 'production' ? ['console', 'debugger'] : [],
|
||||
},
|
||||
// 启用CSS代码分割
|
||||
cssCodeSplit: true,
|
||||
// 生成source map(可选,生产环境可以关闭以减小体积)
|
||||
// 生成 source map(默认关闭,避免线上包体积变大;需要排查问题时可临时打开)
|
||||
sourcemap: false,
|
||||
// 构建输出配置
|
||||
// 构建输出配置:使用 Vite 默认的代码分割策略,避免过度自定义导致库初始化顺序问题
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 手动代码分割,优化缓存
|
||||
manualChunks: (id) => {
|
||||
// node_modules中的包单独打包
|
||||
if (id.includes('node_modules')) {
|
||||
// vue核心库和vue相关库打包在一起,确保加载顺序
|
||||
if (id.includes('vue') || id.includes('vue-router') || id.includes('pinia') || id.includes('@vue')) {
|
||||
return 'vue-vendor';
|
||||
}
|
||||
// monaco-editor单独打包(按需加载,不阻塞初始加载)
|
||||
if (id.includes('monaco-editor')) {
|
||||
return 'monaco-editor';
|
||||
}
|
||||
// echarts 相关库单独打包
|
||||
if (id.includes('echarts') || id.includes('zrender')) {
|
||||
return 'echarts-vendor';
|
||||
}
|
||||
// arco-design相关
|
||||
if (id.includes('@arco-design')) {
|
||||
return 'arco-vendor';
|
||||
}
|
||||
// vxe-table相关
|
||||
if (id.includes('vxe-table') || id.includes('xe-utils')) {
|
||||
return 'vxe-vendor';
|
||||
}
|
||||
// axios 和请求相关库
|
||||
if (id.includes('axios') || id.includes('query-string')) {
|
||||
return 'http-vendor';
|
||||
}
|
||||
// lodash 单独打包(如果使用较多)
|
||||
if (id.includes('lodash')) {
|
||||
return 'lodash-vendor';
|
||||
}
|
||||
// dayjs 单独打包
|
||||
if (id.includes('dayjs')) {
|
||||
return 'dayjs-vendor';
|
||||
}
|
||||
// handsontable 单独打包(如果使用)
|
||||
if (id.includes('handsontable')) {
|
||||
return 'handsontable-vendor';
|
||||
}
|
||||
// 其他第三方库进一步拆分
|
||||
// 按大小拆分,避免单个vendor包过大
|
||||
return 'vendor';
|
||||
}
|
||||
},
|
||||
// 确保文件名包含hash,便于浏览器缓存
|
||||
// 确保文件名包含 hash,便于浏览器缓存
|
||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||
assetFileNames: (assetInfo) => {
|
||||
const info = assetInfo.name.split('.');
|
||||
const ext = info[info.length - 1];
|
||||
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
|
||||
const name = assetInfo.name ?? 'asset';
|
||||
const info = name.split('.');
|
||||
const ext = info[info.length - 1] ?? 'bin';
|
||||
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(name)) {
|
||||
return `assets/media/[name]-[hash].${ext}`;
|
||||
}
|
||||
if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/.test(assetInfo.name)) {
|
||||
if (/\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/.test(name)) {
|
||||
return `assets/img/[name]-[hash].${ext}`;
|
||||
}
|
||||
if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
|
||||
if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(name)) {
|
||||
return `assets/fonts/[name]-[hash].${ext}`;
|
||||
}
|
||||
return `assets/[name]-[hash].${ext}`;
|
||||
@ -357,10 +312,6 @@ export default defineConfig({
|
||||
chunkSizeWarningLimit: 2000,
|
||||
// 启用 terser 压缩以获得更好的压缩率(可选,esbuild更快但压缩率稍低)
|
||||
// minify: 'terser', // 如果需要更好的压缩率,可以启用,但构建会慢一些
|
||||
// 启用 rollup 的 tree-shaking
|
||||
treeshake: {
|
||||
moduleSideEffects: 'no-external',
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
@ -374,4 +325,4 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} as UserConfig);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user