This commit is contained in:
zlwu committed 2026-08-09 21:07:28 +08:00
1 parent b45ce2c32c
commit a5210a6a9e
30 files changed
+11989 -5917

No files matched your search

+1 -1
View File
@@ -1 +1 @@
18897
85846
-3
View File
@@ -17,8 +17,6 @@ declare module 'vue' {
VanActionSheet: typeof import('vant/es')['ActionSheet']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
VanCheckboxGroup: typeof import('vant/es')['CheckboxGroup']
VanDialog: typeof import('vant/es')['Dialog']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
@@ -28,7 +26,6 @@ declare module 'vue' {
VanPicker: typeof import('vant/es')['Picker']
VanPopup: typeof import('vant/es')['Popup']
VanPullRefresh: typeof import('vant/es')['PullRefresh']
VanSearch: typeof import('vant/es')['Search']
VanSwitch: typeof import('vant/es')['Switch']
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
-1
View File
@@ -2,7 +2,6 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<title>报销系统</title>
</head>
@@ -166,6 +166,7 @@
v-for="pick in selectedInvoicePicksSorted"
:key="pick.key"
class="invoice-picked-card"
:style="{ '--invoice-group-color': invoiceGroupColor(pick.invoiceId) }"
>
<button
type="button"
@@ -187,16 +188,21 @@
</div>
<div class="invoice-picked-card__meta">
<span class="invoice-picked-card__name">{{ pick.fileName }}</span>
<div class="invoice-picked-card__amount">¥{{ money(pick.amount) }}</div>
<div class="invoice-picked-card__amount">整票 ¥{{ money(pick.amount) }}</div>
</div>
</div>
</div>
<p class="form-tip">请先在「我的发票夹」上传发票;提交时按合计扣减余额。选择对象为附件文件。</p>
<p class="form-tip">
请先在「我的发票夹」上传发票;提交时按合计扣减余额。选择对象为附件;金额为整票金额;相同边框色为同一张发票。
</p>
</div>
<van-popup v-model:show="showInvoicePicker" position="bottom" round :style="{ height: '72%' }">
<div class="invoice-picker">
<div class="invoice-picker__title">选择发票附件</div>
<p class="invoice-picker__hint">
金额为「整票」金额:同一次上传的多张附件共用该金额。相同边框色表示同一张发票;不同颜色区分不同发票。
</p>
<div class="invoice-picker__body">
<p v-if="!pickerMonthGroups.length" class="invoice-picker__empty">暂无发票附件</p>
<section
@@ -212,6 +218,7 @@
type="button"
class="invoice-attach-card"
:class="{ 'is-selected': pickerDraftKeys.includes(item.key) }"
:style="{ '--invoice-group-color': invoiceGroupColor(item.invoiceId) }"
@click="togglePickerKey(item.key)"
>
<span class="invoice-attach-card__index">{{ item.serial }}</span>
@@ -231,7 +238,7 @@
</div>
<div class="invoice-attach-card__meta">
<span class="invoice-attach-card__name">{{ item.fileName }}</span>
<div class="invoice-attach-card__amount">¥{{ money(item.amount) }}</div>
<div class="invoice-attach-card__amount">整票 ¥{{ money(item.amount) }}</div>
</div>
</button>
</div>
@@ -503,6 +510,25 @@ function isImageFileType(ft: string) {
return String(ft || '').startsWith('image/')
}
/** 同一发票(同一次上传)共用边框色,不同发票互相区分 */
const INVOICE_GROUP_COLORS = [
'#0891b2', // cyan
'#7c3aed', // violet
'#d97706', // amber
'#db2777', // pink
'#2563eb', // blue
'#059669', // emerald
'#ea580c', // orange
'#4f46e5', // indigo
]
function invoiceGroupColor(invoiceId: string | number) {
const s = String(invoiceId || '')
let hash = 0
for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) >>> 0
return INVOICE_GROUP_COLORS[hash % INVOICE_GROUP_COLORS.length]
}
function monthKey(raw: string) {
const text = String(raw || '').trim()
const m = text.match(/^(\d{4})[-/]?(\d{2})/)
@@ -977,7 +1003,7 @@ onMounted(async () => {
position: relative;
flex: 0 0 120px;
width: 120px;
border: 1px solid var(--border);
border: 2px solid var(--invoice-group-color, var(--border));
border-radius: 8px;
background: #fff;
overflow: hidden;
@@ -1041,9 +1067,18 @@ onMounted(async () => {
padding-bottom: env(safe-area-inset-bottom);
}
.invoice-picker__title {
padding: 14px 16px;
padding: 14px 16px 8px;
font-weight: 600;
text-align: center;
flex-shrink: 0;
}
.invoice-picker__hint {
margin: 0;
padding: 0 16px 12px;
font-size: 12px;
line-height: 1.45;
color: var(--text-muted);
text-align: left;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
@@ -1075,7 +1110,7 @@ onMounted(async () => {
.invoice-attach-card {
position: relative;
padding: 0;
border: 1.5px solid var(--border);
border: 2px solid var(--invoice-group-color, var(--border));
border-radius: 8px;
background: #fff;
text-align: left;
@@ -1084,20 +1119,16 @@ onMounted(async () => {
color: inherit;
cursor: pointer;
}
.invoice-attach-card.is-selected {
border-color: var(--primary);
box-shadow: 0 0 0 1px var(--primary);
}
.invoice-attach-card__index {
position: absolute;
top: 4px;
left: 4px;
top: 0;
left: 0;
z-index: 1;
min-width: 18px;
height: 18px;
padding: 0 4px;
border-radius: 9px;
background: rgba(15, 23, 42, 0.5);
padding: 0 5px;
border-radius: 8px 0 8px 0;
background: #ef4444;
color: #fff;
font-size: 10px;
display: flex;
@@ -1148,7 +1179,7 @@ onMounted(async () => {
}
.invoice-attach-card__amount {
margin-top: 2px;
font-size: 12px;
font-size: 11px;
font-weight: 600;
color: var(--primary-dark);
}
@@ -0,0 +1,159 @@
-- 系统管理:新增二级目录「系统信息」「临时存放」,并迁移原子菜单 parent_id
-- 幂等可重复执行;与 web-admin/src/router/modules/sqxp.ts 对齐
-- 目录 id:SYSM...010 / SYSM...011
-- 1) 插入二级目录
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'SYSM000000000000000000000000000010', '系统信息', 'SYSM000000000000000000000000000001',
'M', 'info', NULL, NULL, 1, '1', '1', 'shanxia2.0', 1, '系统管理-系统信息分组',
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'SYSM000000000000000000000000000010' AND `del_flag` = 0
);
INSERT INTO `sys_menu` (
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
'SYSM000000000000000000000000000011', '临时存放', 'SYSM000000000000000000000000000001',
'M', 'temp', NULL, NULL, 1, '1', '1', 'shanxia2.0', 2, '系统管理-临时存放分组',
'system', '', NOW(), NULL, NOW(), 0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `id` = 'SYSM000000000000000000000000000011' AND `del_flag` = 0
);
-- 2) 「系统信息」下菜单
UPDATE `sys_menu`
SET `parent_id` = 'SYSM000000000000000000000000000010',
`update_time` = NOW()
WHERE `del_flag` = 0
AND `id` IN (
'101', -- 租户管理
'102', -- 字典管理
'106', -- 菜单管理
'103', -- 组织管理
'105', -- 角色管理
'104', -- 用户管理
'108', -- 职位管理
'SYS_OPERLOG' -- 操作日志
);
-- 3) 「临时存放」下菜单(含隐藏附属页)
UPDATE `sys_menu`
SET `parent_id` = 'SYSM000000000000000000000000000011',
`update_time` = NOW()
WHERE `del_flag` = 0
AND `id` IN (
'SYS_APPROVER_SETTING', -- 报价、合同审批人设置
'CRM3000000000000000000000000040', -- 主体危化品能力
'CRM1000000000000000000000000010', -- 线索管理
'CRM1000000000000000000000000011', -- 线索详情
'CRM1000000000000000000000000012', -- 线索统计(若存在)
'CRM2000000000000000000000000010', -- 商机管理
'CRM2000000000000000000000000011', -- 商机详情
'CRM2000000000000000000000000012', -- 商机看板
'CRM2000000000000000000000000013', -- 销售漏斗
'CRM2000000000000000000000000014', -- 商机统计
'CRM1000000000000000000000000003', -- 公海池
'CRM1000000000000000000000000004' -- 公海池规则
);
-- 排序:系统信息内
UPDATE `sys_menu` SET `order_num` = 1, `update_time` = NOW() WHERE `id` = '101' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 2, `update_time` = NOW() WHERE `id` = '102' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 3, `update_time` = NOW() WHERE `id` = '106' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 4, `update_time` = NOW() WHERE `id` = '103' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 5, `update_time` = NOW() WHERE `id` = '105' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 6, `update_time` = NOW() WHERE `id` = '104' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 7, `update_time` = NOW() WHERE `id` = '108' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 8, `update_time` = NOW() WHERE `id` = 'SYS_OPERLOG' AND `del_flag` = 0;
-- 排序:临时存放内(可见菜单)
UPDATE `sys_menu` SET `order_num` = 1, `update_time` = NOW() WHERE `id` = 'SYS_APPROVER_SETTING' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 2, `update_time` = NOW() WHERE `id` = 'CRM3000000000000000000000000040' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 3, `update_time` = NOW() WHERE `id` = 'CRM1000000000000000000000000010' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 4, `update_time` = NOW() WHERE `id` = 'CRM2000000000000000000000000010' AND `del_flag` = 0;
UPDATE `sys_menu` SET `order_num` = 5, `update_time` = NOW() WHERE `id` = 'CRM1000000000000000000000000003' AND `del_flag` = 0;
-- 4) 角色菜单:曾拥有子菜单的角色,补挂二级目录(避免孤儿叶子)
INSERT INTO `sys_role_menu` (
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
CONCAT('RM_SYS_INFO_', UPPER(MD5(CONCAT(rm.role_id, ':SYSM000000000000000000000000000010')))),
rm.role_id,
'SYSM000000000000000000000000000010',
COALESCE(NULLIF(rm.tenant_no, ''), 'shanxia2.0'),
0,
'auto: system info group',
'system',
'',
NOW(),
NULL,
NOW(),
0
FROM `sys_role_menu` rm
WHERE rm.del_flag = 0
AND rm.menu_id IN (
'101', '102', '106', '103', '105', '104', '108', 'SYS_OPERLOG'
)
AND NOT EXISTS (
SELECT 1
FROM `sys_role_menu` x
WHERE x.role_id = rm.role_id
AND x.menu_id = 'SYSM000000000000000000000000000010'
AND x.del_flag = 0
)
GROUP BY rm.role_id, rm.tenant_no;
INSERT INTO `sys_role_menu` (
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
)
SELECT
CONCAT('RM_SYS_TEMP_', UPPER(MD5(CONCAT(rm.role_id, ':SYSM000000000000000000000000000011')))),
rm.role_id,
'SYSM000000000000000000000000000011',
COALESCE(NULLIF(rm.tenant_no, ''), 'shanxia2.0'),
0,
'auto: system temp group',
'system',
'',
NOW(),
NULL,
NOW(),
0
FROM `sys_role_menu` rm
WHERE rm.del_flag = 0
AND rm.menu_id IN (
'SYS_APPROVER_SETTING',
'CRM3000000000000000000000000040',
'CRM1000000000000000000000000010',
'CRM1000000000000000000000000011',
'CRM1000000000000000000000000012',
'CRM2000000000000000000000000010',
'CRM2000000000000000000000000011',
'CRM2000000000000000000000000012',
'CRM2000000000000000000000000013',
'CRM2000000000000000000000000014',
'CRM1000000000000000000000000003',
'CRM1000000000000000000000000004'
)
AND NOT EXISTS (
SELECT 1
FROM `sys_role_menu` x
WHERE x.role_id = rm.role_id
AND x.menu_id = 'SYSM000000000000000000000000000011'
AND x.del_flag = 0
)
GROUP BY rm.role_id, rm.tenant_no;
+1 -1
View File
@@ -1 +1 @@
{"/Users/wuzilong/work/00code/shanxia2.0/.agents/skills/brainstorming/scripts/start-server.sh":{"size":4606,"mtime_ns":1780108930000000000,"hash":"7c3c6c5cce454f755159758334ffb434c970a29960bbff6669efe0cb22d1f2fb"},"/Users/wuzilong/work/00code/shanxia2.0/.agents/skills/brainstorming/scripts/stop-server.sh":{"size":1314,"mtime_ns":1780108930000000000,"hash":"9581ff879388aaa10463aea1fc1e9f9c00d69f2106f055ac976f97513ab84aba"},"/Users/wuzilong/work/00code/shanxia2.0/.agents/skills/systematic-debugging/find-polluter.sh":{"size":1528,"mtime_ns":1780108930000000000,"hash":"8340aac47e1279874e46d7ecb4954b5faae531a883abf05b118951687d8ca044"},"/Users/wuzilong/work/00code/shanxia2.0/.cursor/mcp.json":{"size":468,"mtime_ns":1780120483000000000,"hash":"ce4b438767a095c6a830e4612c221303ea16ab46af23f438bfb033eace904c36"},"/Users/wuzilong/work/00code/shanxia2.0/.opencode/opencode.json":{"size":105,"mtime_ns":1780120095000000000,"hash":"936122822138b12f363fb9b7a8567edaa1f834c835137a4c72d81c17116ce11e"},"/Users/wuzilong/work/00code/shanxia2.0/.opencode/package.json":{"size":65,"mtime_ns":1780109751000000000,"hash":"cf97d417f1ebce0ba81ad4629a5345e13cc95cb799c6eb2632caead5364880de"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_17f66cf13ffeo6JpSbBDM7Jx4t.json":{"size":214,"mtime_ns":1780274139000000000,"hash":"26182a523926188400188b8cc668585f8e81586416fa2399fd3585b0c07ff86c"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_1818ffbf9ffezXCeRFVV3Jj01O.json":{"size":263,"mtime_ns":1780238536000000000,"hash":"bf2e860eaa6fa5051d4cb0f70a53cf419b558c57759154e559712efef636dcb5"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_181f43997ffeO8mkoWfZl8v2Ri.json":{"size":214,"mtime_ns":1780236423000000000,"hash":"8252a79ead22a73fe0e229ef4eb6e1aac159547cdc04607d4102c75b077501a2"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_1823f3f66ffesCD5qohVVcWtbf.json":{"size":214,"mtime_ns":1780230242000000000,"hash":"27ba5fb0606f8ddda44c4f01ce5fbce77d7429f890317c6a1a6f9456058b1f70"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_183d2d323ffeBMoLOLwwPmAXym.json":{"size":214,"mtime_ns":1780210339000000000,"hash":"26814a22a2f0ee80b65a7306c4f52becca5edb09358cc12b588efa896546f571"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_183f72fc4ffeCZI7jMs3P9gB5n.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"cd6c278503c190ec5dae59675957e9a963cbd94ce09cf974b838a0e717b42817"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_184f894aeffej3cvErXgdYFhY8.json":{"size":263,"mtime_ns":1780201410000000000,"hash":"d3bb6c88a4c332c09d2684c3a34f10e311620956a8777a894cb1492ab15cce86"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_186a13679ffenULQIK102x2NfS.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"eb2fe454abea5ef36854bc2858ba6e9ac5e8f4d18e0121901f007816e0e40ebe"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_187ac729affeBH1jDFb5X7rWz0.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"b571d2be1c871201f1d572c536b57c89a277b0b3eb3f3a3e4125db5008b37a3f"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_18880f06fffeJSijqM7oWZ6JYI.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"efb86e46a8d0c2af314450407bfacd3f422ec8f68025291a440760cffe4ca092"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_1892f6be2ffe1K80MWGl0oc2Fo.json":{"size":214,"mtime_ns":1780114611000000000,"hash":"9b109fac4a24678ea27b4afe2840bcd06c339b490508d6aa531e051fe3f034b7"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982000000000,"hash":"5d7c80dd077475acb7eb12227dc0e7cf38bbb5fd9e84c4eab9eb2be8c5406357"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/__init__.py":{"size":665,"mtime_ns":1780804649000000000,"hash":"349c40e55e8aae86d0913a17da2f9d338d842243e5fdf807bad52382bc2876d9"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/admin.py":{"size":52987,"mtime_ns":1786164471385093466,"hash":"4566289fd8c57c51d4047a461b3953f67f5ddb83dbd59cd999c9d0c77caf142a"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/__init__.py":{"size":827,"mtime_ns":1784980394000000000,"hash":"7605bb52b5361261c5e04d52bec9a1e704b49dd4ffe5c561da079831e8d8db9e"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/advance.py":{"size":13404,"mtime_ns":1784992736000000000,"hash":"7e4bacb178dde4fddc158713abe0eb9e8ab6fedf57e571701e45a5af3189f858"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/attachment.py":{"size":5758,"mtime_ns":1782790898000000000,"hash":"f2d20fc5b2a98748835ae1710666f11eae1aa323d80a4678189bb2d032559b4b"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/expense_item.py":{"size":3362,"mtime_ns":1782650913000000000,"hash":"142f0f6a3e45cb5d7b8153ee3048c5fb82258167264f325dd4fa8801b2e56c35"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/apLine truncated
{"/Users/wuzilong/work/00code/shanxia2.0/.agents/skills/brainstorming/scripts/start-server.sh":{"size":4606,"mtime_ns":1780108930000000000,"hash":"7c3c6c5cce454f755159758334ffb434c970a29960bbff6669efe0cb22d1f2fb"},"/Users/wuzilong/work/00code/shanxia2.0/.agents/skills/brainstorming/scripts/stop-server.sh":{"size":1314,"mtime_ns":1780108930000000000,"hash":"9581ff879388aaa10463aea1fc1e9f9c00d69f2106f055ac976f97513ab84aba"},"/Users/wuzilong/work/00code/shanxia2.0/.agents/skills/systematic-debugging/find-polluter.sh":{"size":1528,"mtime_ns":1780108930000000000,"hash":"8340aac47e1279874e46d7ecb4954b5faae531a883abf05b118951687d8ca044"},"/Users/wuzilong/work/00code/shanxia2.0/.cursor/mcp.json":{"size":468,"mtime_ns":1780120483000000000,"hash":"ce4b438767a095c6a830e4612c221303ea16ab46af23f438bfb033eace904c36"},"/Users/wuzilong/work/00code/shanxia2.0/.opencode/opencode.json":{"size":105,"mtime_ns":1780120095000000000,"hash":"936122822138b12f363fb9b7a8567edaa1f834c835137a4c72d81c17116ce11e"},"/Users/wuzilong/work/00code/shanxia2.0/.opencode/package.json":{"size":65,"mtime_ns":1780109751000000000,"hash":"cf97d417f1ebce0ba81ad4629a5345e13cc95cb799c6eb2632caead5364880de"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_17f66cf13ffeo6JpSbBDM7Jx4t.json":{"size":214,"mtime_ns":1780274139000000000,"hash":"26182a523926188400188b8cc668585f8e81586416fa2399fd3585b0c07ff86c"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_1818ffbf9ffezXCeRFVV3Jj01O.json":{"size":263,"mtime_ns":1780238536000000000,"hash":"bf2e860eaa6fa5051d4cb0f70a53cf419b558c57759154e559712efef636dcb5"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_181f43997ffeO8mkoWfZl8v2Ri.json":{"size":214,"mtime_ns":1780236423000000000,"hash":"8252a79ead22a73fe0e229ef4eb6e1aac159547cdc04607d4102c75b077501a2"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_1823f3f66ffesCD5qohVVcWtbf.json":{"size":214,"mtime_ns":1780230242000000000,"hash":"27ba5fb0606f8ddda44c4f01ce5fbce77d7429f890317c6a1a6f9456058b1f70"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_183d2d323ffeBMoLOLwwPmAXym.json":{"size":214,"mtime_ns":1780210339000000000,"hash":"26814a22a2f0ee80b65a7306c4f52becca5edb09358cc12b588efa896546f571"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_183f72fc4ffeCZI7jMs3P9gB5n.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"cd6c278503c190ec5dae59675957e9a963cbd94ce09cf974b838a0e717b42817"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_184f894aeffej3cvErXgdYFhY8.json":{"size":263,"mtime_ns":1780201410000000000,"hash":"d3bb6c88a4c332c09d2684c3a34f10e311620956a8777a894cb1492ab15cce86"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_186a13679ffenULQIK102x2NfS.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"eb2fe454abea5ef36854bc2858ba6e9ac5e8f4d18e0121901f007816e0e40ebe"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_187ac729affeBH1jDFb5X7rWz0.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"b571d2be1c871201f1d572c536b57c89a277b0b3eb3f3a3e4125db5008b37a3f"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_18880f06fffeJSijqM7oWZ6JYI.json":{"size":214,"mtime_ns":1780201410000000000,"hash":"efb86e46a8d0c2af314450407bfacd3f422ec8f68025291a440760cffe4ca092"},"/Users/wuzilong/work/00code/shanxia2.0/.sisyphus/run-continuation/ses_1892f6be2ffe1K80MWGl0oc2Fo.json":{"size":214,"mtime_ns":1780114611000000000,"hash":"9b109fac4a24678ea27b4afe2840bcd06c339b490508d6aa531e051fe3f034b7"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/__init__.py":{"size":21,"mtime_ns":1780113982000000000,"hash":"5d7c80dd077475acb7eb12227dc0e7cf38bbb5fd9e84c4eab9eb2be8c5406357"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/__init__.py":{"size":665,"mtime_ns":1780804649000000000,"hash":"349c40e55e8aae86d0913a17da2f9d338d842243e5fdf807bad52382bc2876d9"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/admin.py":{"size":52987,"mtime_ns":1786164471385093466,"hash":"4566289fd8c57c51d4047a461b3953f67f5ddb83dbd59cd999c9d0c77caf142a"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/__init__.py":{"size":827,"mtime_ns":1784980394000000000,"hash":"7605bb52b5361261c5e04d52bec9a1e704b49dd4ffe5c561da079831e8d8db9e"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/advance.py":{"size":13404,"mtime_ns":1784992736000000000,"hash":"7e4bacb178dde4fddc158713abe0eb9e8ab6fedf57e571701e45a5af3189f858"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/attachment.py":{"size":5758,"mtime_ns":1782790898000000000,"hash":"f2d20fc5b2a98748835ae1710666f11eae1aa323d80a4678189bb2d032559b4b"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/bx/expense_item.py":{"size":3362,"mtime_ns":1782650913000000000,"hash":"142f0f6a3e45cb5d7b8153ee3048c5fb82258167264f325dd4fa8801b2e56c35"},"/Users/wuzilong/work/00code/shanxia2.0/backend/app/apLine truncated
+5209 -174
View File
File diff suppressed because it is too large. Load diff
+9 -20
View File
@@ -129,11 +129,11 @@
"127": "selectContractRow",
"128": "Community 128",
"129": "Community 129",
"130": "emit",
"130": "prepareLineAliasAndBrandForSubmit",
"131": "prepareLineAliasAndBrandForSubmit",
"132": "Community 132",
"133": "Community 133",
"134": "index.vue",
"134": "VersionManager",
"135": "detail.vue",
"136": "Community 136",
"137": "Community 137",
@@ -146,28 +146,28 @@
"144": "Community 144",
"145": "api",
"146": "price-letter-template.ts",
"147": "loading.ts",
"147": "contractType.ts",
"148": "Community 148",
"149": "fetchCustomerDetail",
"150": "Community 150",
"151": "Community 151",
"152": "SettingDrawer.vue",
"153": "index.vue",
"154": "detail.vue",
"154": "index.vue",
"155": "contractType.ts",
"156": "v2.5.7 - 2025-09-14",
"157": "form.vue",
"158": "removeProductsNotAllowedByHazard",
"159": "detail.vue",
"160": "product.ts",
"160": "crm-owner-candidates.ts",
"161": "Community 161",
"162": "Community 162",
"163": "pool.ts",
"163": "index.vue",
"164": "Community 164",
"165": "handleDisburse",
"166": "Community 166",
"167": "Community 167",
"168": "AssignDialog.vue",
"168": "index.vue",
"169": "Community 169",
"170": "Community 170",
"171": "index.vue",
@@ -182,28 +182,19 @@
"180": "index.vue",
"181": "Community 181",
"182": "Community 182",
"183": "StorageConfig",
"184": "system.ts",
"185": "SettingActions.vue",
"186": "StorageKeyManager",
"187": "Community 187",
"188": "Community 188",
"189": "Community 189",
"190": "ViewKvTableCell.vue",
"191": "handleConfirmApproval",
"192": "index.vue",
"193": "navigation.ts",
"194": "canDeleteByCreator",
"195": "Community 195",
"196": "Community 196",
"197": "v2.4.2.1 - 2025-06-16",
"198": "index.vue",
"200": "index.js",
"201": "index.vue",
"201": "menuFavorite.ts",
"202": "Community 202",
"206": "index.vue",
"206": "form.vue",
"207": "Community 207",
"208": "Community 208",
"209": "Community 209",
"210": "Community 210",
"212": "index.js",
@@ -553,13 +544,11 @@
"1275": "index.js",
"1277": "plugin.js",
"1278": "plugin.min.js",
"1279": "index.js",
"1280": "plugin.js",
"1281": "plugin.min.js",
"1282": "index.js",
"1283": "plugin.js",
"1284": "plugin.min.js",
"1286": "index.js",
"1287": "plugin.js",
"1288": "plugin.min.js",
"1289": "index.js",
@@ -129,11 +129,10 @@
"127": "selectContractRow",
"128": "Community 128",
"129": "Community 129",
"130": "emit",
"131": "prepareLineAliasAndBrandForSubmit",
"132": "Community 132",
"133": "Community 133",
"134": "index.vue",
"134": "VersionManager",
"135": "detail.vue",
"136": "Community 136",
"137": "Community 137",
@@ -146,28 +145,24 @@
"144": "Community 144",
"145": "api",
"146": "price-letter-template.ts",
"147": "loading.ts",
"148": "Community 148",
"149": "fetchCustomerDetail",
"150": "Community 150",
"151": "Community 151",
"152": "SettingDrawer.vue",
"153": "index.vue",
"154": "detail.vue",
"154": "index.vue",
"155": "contractType.ts",
"156": "v2.5.7 - 2025-09-14",
"157": "form.vue",
"158": "removeProductsNotAllowedByHazard",
"159": "detail.vue",
"160": "product.ts",
"161": "Community 161",
"162": "Community 162",
"163": "pool.ts",
"164": "Community 164",
"165": "handleDisburse",
"166": "Community 166",
"167": "Community 167",
"168": "AssignDialog.vue",
"169": "Community 169",
"170": "Community 170",
"171": "index.vue",
@@ -184,21 +179,20 @@
"182": "Community 182",
"183": "StorageConfig",
"184": "system.ts",
"185": "SettingActions.vue",
"186": "StorageKeyManager",
"187": "Community 187",
"188": "Community 188",
"189": "Community 189",
"190": "ViewKvTableCell.vue",
"191": "handleConfirmApproval",
"194": "highlight.ts",
"195": "Community 195",
"196": "Community 196",
"200": "index.js",
"201": "index.vue",
"201": "menuFavorite.ts",
"202": "Community 202",
"206": "index.vue",
"206": "form.vue",
"207": "Community 207",
"208": "Community 208",
"209": "Community 209",
"210": "Community 210",
"212": "index.js",
@@ -554,7 +548,6 @@
"1282": "index.js",
"1283": "plugin.js",
"1284": "plugin.min.js",
"1286": "index.js",
"1287": "plugin.js",
"1288": "plugin.min.js",
"1289": "index.js",
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+10 -5
View File
@@ -60,10 +60,12 @@ const emit = defineEmits<{
}>()
const fileList = ref<UploadFile[]>([])
/** 本地 URL 列表,避免多文件并行上传完成时基于 stale props 互相覆盖 */
const localUrls = ref<string[]>([])
const isDirectUrl = (value: string) => /^https?:\/\//i.test(value)
const displayItems = computed(() => parseValue(props.modelValue))
const displayItems = computed(() => localUrls.value)
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','))
@@ -83,7 +85,7 @@ function listToString(items: string[]) {
}
function syncFileListFromModel() {
fileList.value = displayItems.value.map((url, index) => ({
fileList.value = localUrls.value.map((url, index) => ({
name: url.split('/').pop() || `file-${index + 1}`,
url,
status: 'success'
@@ -92,7 +94,8 @@ function syncFileListFromModel() {
watch(
() => props.modelValue,
() => {
(value) => {
localUrls.value = parseValue(value)
if (props.disabled) return
syncFileListFromModel()
},
@@ -124,7 +127,8 @@ async function handleUploadRequest(options: UploadRequestOptions) {
if (!fileUrl) {
throw new Error('上传失败')
}
const next = [...displayItems.value, fileUrl]
const next = [...localUrls.value, fileUrl]
localUrls.value = next
emit('update:modelValue', listToString(next))
options.onSuccess?.(res)
} catch (error: any) {
@@ -136,7 +140,8 @@ async function handleUploadRequest(options: UploadRequestOptions) {
function handleRemove(uploadFile: UploadFile) {
const removedUrl = uploadFile.url || (uploadFile.response as { fileUrl?: string })?.fileUrl
if (!removedUrl) return
const next = displayItems.value.filter((url) => url !== removedUrl)
const next = localUrls.value.filter((url) => url !== removedUrl)
localUrls.value = next
emit('update:modelValue', listToString(next))
}
@@ -123,7 +123,7 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import {
Plus,
Loading,
@@ -177,13 +177,23 @@
const isPointerInside = ref(false)
const isDragOver = ref(false)
let dragEnterDepth = 0
let clearUploadScheduled = false
/**
* 本地缓冲:el-upload 多选会同步连续触发 @change;
* 若基于尚未回写的 props.modelValue 展开累加,会只留下最后一个文件。
*/
const files = ref<BxAttachmentItem[]>([])
const files = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
watch(
() => props.modelValue,
(val) => {
files.value = Array.isArray(val) ? val.slice() : []
},
{ immediate: true }
)
function syncFiles(next: BxAttachmentItem[]) {
files.value = next
emit('update:modelValue', next)
}
@@ -256,12 +266,19 @@
}
}
async function handleAttachmentChange(uploadFile: UploadFile) {
const rawFile = uploadFile.raw
if (!rawFile) return
await uploadRawFile(rawFile)
async function scheduleClearUploadFiles() {
if (clearUploadScheduled) return
clearUploadScheduled = true
await nextTick()
uploadRef.value?.clearFiles()
clearUploadScheduled = false
}
function handleAttachmentChange(uploadFile: UploadFile) {
const rawFile = uploadFile.raw
if (!rawFile) return
void uploadRawFile(rawFile)
void scheduleClearUploadFiles()
}
function handlePointerEnter() {
@@ -95,12 +95,16 @@
const isPointerInside = ref(false)
const isDragOver = ref(false)
let dragEnterDepth = 0
let clearUploadScheduled = false
const previewUrlMap = ref<Record<string, string>>({})
const files = computed(() => props.modelValue || [])
/**
* 本地缓冲:el-upload 多选时会同步连续触发多次 @change,
* 若每次都基于尚未回写的 props.modelValue 累加,最终只会留下最后一个文件。
*/
const localFiles = ref<File[]>([])
const displayItems = computed<DisplayItem[]>(() =>
files.value.map((file, index) => {
localFiles.value.map((file, index) => {
const key = `${file.name}-${file.size}-${file.lastModified}-${index}`
return {
key,
@@ -119,7 +123,7 @@
function rebuildPreviews() {
revokeAllPreviews()
const next: Record<string, string> = {}
files.value.forEach((file, index) => {
localFiles.value.forEach((file, index) => {
if (!file.type.startsWith('image/')) return
const key = `${file.name}-${file.size}-${file.lastModified}-${index}`
next[key] = URL.createObjectURL(file)
@@ -129,12 +133,17 @@
watch(
() => props.modelValue,
() => rebuildPreviews(),
{ deep: true, immediate: true }
(val) => {
localFiles.value = Array.isArray(val) ? val.slice() : []
rebuildPreviews()
},
{ immediate: true }
)
function syncFiles(next: File[]) {
localFiles.value = next
emit('update:modelValue', next)
rebuildPreviews()
}
function isFileAccepted(file: File) {
@@ -181,7 +190,7 @@
function appendFiles(incoming: File[]) {
if (!incoming.length || props.disabled) return
const next = [...files.value]
const next = [...localFiles.value]
for (const raw of incoming) {
if (next.length >= props.limit) {
ElMessage.warning(`最多上传 ${props.limit} 个文件`)
@@ -196,16 +205,23 @@
syncFiles(next)
}
async function onChange(uploadFile: UploadFile) {
async function scheduleClearUploadFiles() {
if (clearUploadScheduled) return
clearUploadScheduled = true
await nextTick()
uploadRef.value?.clearFiles()
clearUploadScheduled = false
}
function onChange(uploadFile: UploadFile) {
const raw = uploadFile.raw
if (!raw) return
appendFiles([raw])
await nextTick()
uploadRef.value?.clearFiles()
void scheduleClearUploadFiles()
}
function removeAt(index: number) {
const next = [...files.value]
const next = [...localFiles.value]
next.splice(index, 1)
syncFiles(next)
}
@@ -21,6 +21,7 @@
type="button"
class="invoice-attach-card"
:class="{ 'is-selected': selectedKeys.has(item.key) }"
:style="{ '--invoice-group-color': invoiceGroupColor(item.invoiceId) }"
@click="toggleOne(item.key)"
>
<span class="invoice-attach-card__index">{{ item.serial }}</span>
@@ -48,7 +49,7 @@
</span>
<div class="invoice-attach-card__footer">
<span class="invoice-attach-card__amount">
{{ formatMoneyYuan(item.amount) }} 元
整票 {{ formatMoneyYuan(item.amount) }} 元
</span>
<el-icon
class="invoice-attach-card__download"
@@ -179,6 +180,25 @@
return `${y}年${Number(m)}月`
}
/** 同一发票(同一次上传)共用边框色,不同发票互相区分 */
const INVOICE_GROUP_COLORS = [
'#0891b2',
'#7c3aed',
'#d97706',
'#db2777',
'#2563eb',
'#059669',
'#ea580c',
'#4f46e5'
]
function invoiceGroupColor(invoiceId: string | number) {
const s = String(invoiceId || '')
let hash = 0
for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) >>> 0
return INVOICE_GROUP_COLORS[hash % INVOICE_GROUP_COLORS.length]
}
function invoiceAttachments(row: any): BxAttachmentItem[] {
const list = Array.isArray(row?.attachments) ? row.attachments : []
if (list.length) {
@@ -314,37 +334,34 @@
position: relative;
width: 140px;
padding: 0;
border: 1px solid var(--border, #d9e8e2);
border: 2px solid var(--invoice-group-color, var(--border, #d9e8e2));
border-radius: 8px;
background: var(--surface, #fff);
overflow: hidden;
cursor: pointer;
text-align: left;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease;
transition: box-shadow 0.15s ease;
&:hover {
border-color: var(--primary, #10b981);
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.12);
}
&.is-selected {
border-color: var(--primary, #10b981);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
}
}
.invoice-attach-card__index {
position: absolute;
z-index: 1;
top: 6px;
left: 8px;
font-size: 12px;
z-index: 2;
top: 0;
left: 0;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 8px 0 8px 0;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 600;
line-height: 1.2;
color: var(--primary, #10b981);
background: none;
line-height: 20px;
text-align: center;
}
.invoice-attach-card__preview {
+239 -213
View File
@@ -2,7 +2,7 @@ import type { AppRouteRecord } from '@/types/router'
/**
* 前端路由(报销管理、CRM 管理、地磅管理、工作流管理、系统管理;运输见 pl.ts)
* 与后端菜单对齐时请同步 `sys_menu`(bx 见 docs/sql/20260606_bx_menu.sql;db 见 docs/sql/20260607_db_menu.sql)。
* 与后端菜单对齐时请同步 `sys_menu`(bx / db 见历史迁移;系统管理二级分组见 docs/sql/migrations/2026-08-09-system-menu-groups.sql)。
*/
export const sqxpAdminRoutes: AppRouteRecord[] = [
// ──────────────────────────────────────────────
@@ -608,222 +608,248 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
},
children: [
{
id: '101',
name: 'Tenant',
path: 'tenant',
component: '/system/tenant/index',
id: 'SYSM000000000000000000000000000010',
name: 'SystemInfo',
path: 'info',
meta: {
title: '租户管理',
keepAlive: true,
menuCode: 'system:tenant:list'
}
title: '系统信息',
keepAlive: false
},
children: [
{
id: '101',
name: 'Tenant',
path: 'tenant',
component: '/system/tenant/index',
meta: {
title: '租户管理',
keepAlive: true,
menuCode: 'system:tenant:list'
}
},
{
id: '102',
name: 'Dict',
path: 'dict',
component: '/system/dict/index',
meta: {
title: '字典管理',
keepAlive: true,
menuCode: 'system:dict:list'
}
},
{
id: '106',
name: 'Menu',
path: 'menu',
component: '/system/menu/index',
meta: {
title: '菜单管理',
keepAlive: true,
menuCode: 'system:menu:list'
}
},
{
id: '103',
name: 'Dept',
path: 'dept',
component: '/system/dept/index',
meta: {
title: '组织管理',
keepAlive: true,
menuCode: 'system:dept:list'
}
},
{
id: '105',
name: 'Role',
path: 'role',
component: '/system/role/index',
meta: {
title: '角色管理',
keepAlive: true,
menuCode: 'system:role:list'
}
},
{
id: '104',
name: 'User',
path: 'user',
component: '/system/user/index',
meta: {
title: '用户管理',
keepAlive: true,
menuCode: 'system:user:list'
}
},
{
id: '108',
name: 'Post',
path: 'post',
component: '/system/post/index',
meta: {
title: '职位管理',
keepAlive: true,
menuCode: 'system:post:list'
}
},
{
id: 'SYS_OPERLOG',
name: 'SystemOperlog',
path: 'operlog',
component: '/system/operlog/index',
meta: {
title: '操作日志',
keepAlive: true,
menuCode: 'system:operlog:list'
}
},
{
id: '10000000000000000000000000000008',
name: 'UserDetail',
path: 'user/detail',
component: '/system/user/detail',
meta: { title: '用户详情', isHide: true, keepAlive: true }
}
]
},
{
id: '102',
name: 'Dict',
path: 'dict',
component: '/system/dict/index',
id: 'SYSM000000000000000000000000000011',
name: 'SystemTemp',
path: 'temp',
meta: {
title: '字典管理',
keepAlive: true,
menuCode: 'system:dict:list'
}
},
{
id: '106',
name: 'Menu',
path: 'menu',
component: '/system/menu/index',
meta: {
title: '菜单管理',
keepAlive: true,
menuCode: 'system:menu:list'
}
},
{
id: '103',
name: 'Dept',
path: 'dept',
component: '/system/dept/index',
meta: {
title: '组织管理',
keepAlive: true,
menuCode: 'system:dept:list'
}
},
{
id: '105',
name: 'Role',
path: 'role',
component: '/system/role/index',
meta: {
title: '角色管理',
keepAlive: true,
menuCode: 'system:role:list'
}
},
{
id: '104',
name: 'User',
path: 'user',
component: '/system/user/index',
meta: {
title: '用户管理',
keepAlive: true,
menuCode: 'system:user:list'
}
},
{
id: '108',
name: 'Post',
path: 'post',
component: '/system/post/index',
meta: {
title: '职位管理',
keepAlive: true,
menuCode: 'system:post:list'
}
},
{
id: 'SYS_OPERLOG',
name: 'SystemOperlog',
path: 'operlog',
component: '/system/operlog/index',
meta: {
title: '操作日志',
keepAlive: true,
menuCode: 'system:operlog:list'
}
},
{
id: 'SYS_APPROVER_SETTING',
name: 'SystemApproverSetting',
path: 'approver-setting',
component: '/system/approver-setting/index',
meta: {
title: '报价、合同审批人设置',
keepAlive: true,
menuCode: 'system:approverSetting'
}
},
{
id: 'CRM1000000000000000000000000010',
name: 'CrmClue',
path: 'clue',
component: '/crm/clue/index',
meta: { title: '线索管理', keepAlive: true, menuCode: 'crm:clue:list' }
},
{
id: 'CRM1000000000000000000000000011',
name: 'CrmClueDetail',
path: 'clue/detail/:id',
component: '/crm/clue/detail',
meta: {
title: '线索详情',
keepAlive: false,
isHide: true,
menuCode: 'crm:clue:query'
}
},
{
id: 'CRM1000000000000000000000000012',
name: 'CrmClueStatistics',
path: 'clue/statistics',
component: '/crm/clue/statistics/index',
meta: {
title: '线索统计',
keepAlive: true,
isHide: true,
menuCode: 'crm:clue:statistics'
}
},
{
id: 'CRM2000000000000000000000000010',
name: 'CrmOpportunity',
path: 'opportunity',
component: '/crm/opportunity/index',
meta: { title: '商机管理', keepAlive: true, menuCode: 'crm:opportunity:list' }
},
{
id: 'CRM2000000000000000000000000011',
name: 'CrmOpportunityDetail',
path: 'opportunity/detail/:id',
component: '/crm/opportunity/detail',
meta: {
title: '商机详情',
keepAlive: false,
isHide: true,
menuCode: 'crm:opportunity:query'
}
},
{
id: 'CRM2000000000000000000000000012',
name: 'CrmOpportunityKanban',
path: 'opportunity/kanban',
component: '/crm/opportunity/kanban',
meta: {
title: '商机看板',
keepAlive: true,
isHide: true,
menuCode: 'crm:opportunity:list'
}
},
{
id: 'CRM2000000000000000000000000013',
name: 'CrmOpportunityFunnel',
path: 'opportunity/funnel',
component: '/crm/opportunity/funnel',
meta: {
title: '销售漏斗',
keepAlive: true,
isHide: true,
menuCode: 'crm:opportunity:list'
}
},
{
id: 'CRM2000000000000000000000000014',
name: 'CrmOpportunityStatistics',
path: 'opportunity/statistics',
component: '/crm/opportunity/statistics',
meta: {
title: '商机统计',
keepAlive: true,
isHide: true,
menuCode: 'crm:opportunity:list'
}
},
{
id: 'CRM1000000000000000000000000003',
name: 'CrmCustomerPool',
path: 'customer-pool',
component: '/crm/customer/pool/index',
meta: { title: '公海池', keepAlive: true, menuCode: 'crm:customerPool:list' }
},
{
id: 'CRM1000000000000000000000000004',
name: 'CrmPoolRule',
path: 'pool-rule',
component: '/crm/customer/pool/RuleIndex',
meta: {
title: '公海池规则',
keepAlive: true,
isHide: true,
menuCode: 'crm:poolRule:list'
}
},
{
id: 'CRM3000000000000000000000000040',
name: 'CrmSupplierHazard',
path: 'supplier-hazard',
component: '/crm/supplier-hazard/index',
meta: { title: '主体危化品能力', keepAlive: true, menuCode: 'crm:supplierHazard:list' }
},
{
id: '10000000000000000000000000000008',
name: 'UserDetail',
path: 'user/detail',
component: '/system/user/detail',
meta: { title: '用户详情', isHide: true, keepAlive: true }
title: '临时存放',
keepAlive: false
},
children: [
{
id: 'SYS_APPROVER_SETTING',
name: 'SystemApproverSetting',
path: 'approver-setting',
component: '/system/approver-setting/index',
meta: {
title: '报价、合同审批人设置',
keepAlive: true,
menuCode: 'system:approverSetting'
}
},
{
id: 'CRM3000000000000000000000000040',
name: 'CrmSupplierHazard',
path: 'supplier-hazard',
component: '/crm/supplier-hazard/index',
meta: {
title: '主体危化品能力',
keepAlive: true,
menuCode: 'crm:supplierHazard:list'
}
},
{
id: 'CRM1000000000000000000000000010',
name: 'CrmClue',
path: 'clue',
component: '/crm/clue/index',
meta: { title: '线索管理', keepAlive: true, menuCode: 'crm:clue:list' }
},
{
id: 'CRM1000000000000000000000000011',
name: 'CrmClueDetail',
path: 'clue/detail/:id',
component: '/crm/clue/detail',
meta: {
title: '线索详情',
keepAlive: false,
isHide: true,
menuCode: 'crm:clue:query'
}
},
{
id: 'CRM1000000000000000000000000012',
name: 'CrmClueStatistics',
path: 'clue/statistics',
component: '/crm/clue/statistics/index',
meta: {
title: '线索统计',
keepAlive: true,
isHide: true,
menuCode: 'crm:clue:statistics'
}
},
{
id: 'CRM2000000000000000000000000010',
name: 'CrmOpportunity',
path: 'opportunity',
component: '/crm/opportunity/index',
meta: { title: '商机管理', keepAlive: true, menuCode: 'crm:opportunity:list' }
},
{
id: 'CRM2000000000000000000000000011',
name: 'CrmOpportunityDetail',
path: 'opportunity/detail/:id',
component: '/crm/opportunity/detail',
meta: {
title: '商机详情',
keepAlive: false,
isHide: true,
menuCode: 'crm:opportunity:query'
}
},
{
id: 'CRM2000000000000000000000000012',
name: 'CrmOpportunityKanban',
path: 'opportunity/kanban',
component: '/crm/opportunity/kanban',
meta: {
title: '商机看板',
keepAlive: true,
isHide: true,
menuCode: 'crm:opportunity:list'
}
},
{
id: 'CRM2000000000000000000000000013',
name: 'CrmOpportunityFunnel',
path: 'opportunity/funnel',
component: '/crm/opportunity/funnel',
meta: {
title: '销售漏斗',
keepAlive: true,
isHide: true,
menuCode: 'crm:opportunity:list'
}
},
{
id: 'CRM2000000000000000000000000014',
name: 'CrmOpportunityStatistics',
path: 'opportunity/statistics',
component: '/crm/opportunity/statistics',
meta: {
title: '商机统计',
keepAlive: true,
isHide: true,
menuCode: 'crm:opportunity:list'
}
},
{
id: 'CRM1000000000000000000000000003',
name: 'CrmCustomerPool',
path: 'customer-pool',
component: '/crm/customer/pool/index',
meta: { title: '公海池', keepAlive: true, menuCode: 'crm:customerPool:list' }
},
{
id: 'CRM1000000000000000000000000004',
name: 'CrmPoolRule',
path: 'pool-rule',
component: '/crm/customer/pool/RuleIndex',
meta: {
title: '公海池规则',
keepAlive: true,
isHide: true,
menuCode: 'crm:poolRule:list'
}
}
]
}
]
}
+1 -1
View File
@@ -102,7 +102,7 @@ export const useWorktabStore = defineStore(
/**
* 过滤不应展示在 worktab 中的父级占位路由。
* `/system` 仅用于承载与重定向,真实标签页为其子路由(如 `/system/tenant`)。
* `/system` 仅用于承载与重定向,真实标签页为其子路由(如 `/system/info/tenant`)。
*/
const shouldIgnoreTab = (tab: Partial<WorkTab>): boolean => {
if (tab.path === '/system') {
+2
View File
@@ -58,6 +58,8 @@ export const SIDEBAR_MENU_ICON_MAP: Record<string, string> = {
// ── 系统 ──
SystemManage: 'ri:settings-3-line',
SystemInfo: 'ri:information-line',
SystemTemp: 'ri:inbox-archive-line',
Tenant: 'ri:building-line',
Dict: 'ri:book-2-line',
Menu: 'ri:menu-line',
+82 -16
View File
@@ -11,11 +11,25 @@ export interface PortalHomeCardItem {
menuCode?: string
}
/** 一级模块下按二级菜单分组的一行 */
export interface PortalHomeGroup {
/** 二级菜单 path;叶子直属合并组用空串 */
groupPath: string
/** 二级菜单标题;空串表示不展示分组标题(一级下的叶子页) */
groupTitle: string
cards: PortalHomeCardItem[]
/** 为 true 时不渲染二级标题(CRM 等扁平二级叶子) */
hideTitle?: boolean
}
export interface PortalHomeSection {
/** 一级模块 path */
modulePath: string
moduleTitle: string
/** 扁平卡片(收藏 / 角标 / 检索共用) */
cards: PortalHomeCardItem[]
/** 按二级菜单分行展示 */
groups: PortalHomeGroup[]
}
/** 首页菜单目录条目(含拼音检索元数据) */
@@ -75,40 +89,92 @@ function toCard(item: AppRouteRecord): PortalHomeCardItem | null {
}
}
/** 从一级模块收集可点卡片(跳过不可导航二级标题) */
export function collectCardsForModule(module: AppRouteRecord): PortalHomeCardItem[] {
/** 从一级模块按二级收集分组与扁平卡片(跳过不可导航二级标题) */
export function collectGroupsForModule(module: AppRouteRecord): {
groups: PortalHomeGroup[]
cards: PortalHomeCardItem[]
} {
const l2List = filterSidebarRoutes(module.children || [])
const groups: PortalHomeGroup[] = []
const cards: PortalHomeCardItem[] = []
const seen = new Set<string>()
/** 一级下直接挂的叶子页,按菜单顺序合并为无标题行,避免「产品管理 / 产品管理」重复 */
let pendingFlat: PortalHomeCardItem[] | null = null
const push = (item: AppRouteRecord) => {
const pushUnique = (item: AppRouteRecord): PortalHomeCardItem | null => {
const card = toCard(item)
if (!card || seen.has(card.path)) return
if (!card || seen.has(card.path)) return null
seen.add(card.path)
cards.push(card)
return card
}
const flushFlat = () => {
if (!pendingFlat?.length) {
pendingFlat = null
return
}
groups.push({
groupPath: '',
groupTitle: '',
cards: pendingFlat,
hideTitle: true
})
pendingFlat = null
}
for (const l2 of l2List) {
const links = resolveMegaPanelL3Links(l2)
for (const link of links) {
push(link)
}
const hasKids = filterSidebarRoutes(l2.children || []).length > 0
if (hasKids && isSidebarNavigableRoute(l2)) {
push(l2)
const links = resolveMegaPanelL3Links(l2)
// 无真实子级:侧栏里的「二级」实际是一级下的叶子页(如 CRM 产品管理)
if (!hasKids) {
if (!pendingFlat) pendingFlat = []
for (const link of links) {
const card = pushUnique(link)
if (card) pendingFlat.push(card)
}
continue
}
flushFlat()
const groupCards: PortalHomeCardItem[] = []
for (const link of links) {
const card = pushUnique(link)
if (card) groupCards.push(card)
}
if (isSidebarNavigableRoute(l2)) {
const card = pushUnique(l2)
if (card) groupCards.push(card)
}
if (!groupCards.length) continue
groups.push({
groupPath: String(l2.path || ''),
groupTitle: routeTitle(l2),
cards: groupCards
})
}
return cards
flushFlat()
return { groups, cards }
}
/** 从一级模块收集可点卡片(跳过不可导航二级标题) */
export function collectCardsForModule(module: AppRouteRecord): PortalHomeCardItem[] {
return collectGroupsForModule(module).cards
}
export function collectPortalHomeSections(menuList: AppRouteRecord[]): PortalHomeSection[] {
const tops = filterPortalTopModules(menuList)
return tops
.map((mod) => ({
modulePath: String(mod.path || ''),
moduleTitle: routeTitle(mod),
cards: collectCardsForModule(mod)
}))
.map((mod) => {
const { groups, cards } = collectGroupsForModule(mod)
return {
modulePath: String(mod.path || ''),
moduleTitle: routeTitle(mod),
cards,
groups
}
})
.filter((s) => s.cards.length > 0)
}
@@ -358,6 +358,7 @@
v-for="pick in selectedInvoicePicks"
:key="pick.key"
class="invoice-selected-card"
:style="{ '--invoice-group-color': invoiceGroupColor(pick.invoiceId) }"
>
<button
type="button"
@@ -388,7 +389,7 @@
</span>
<div class="invoice-selected-card__footer">
<span class="invoice-selected-card__amount">
{{ formatMoneyYuan(pick.amount) }} 元
整票 {{ formatMoneyYuan(pick.amount) }} 元
</span>
<el-icon
class="invoice-selected-card__download"
@@ -761,6 +762,25 @@ const walletBalance = ref(0)
const editingId = ref<number | null>(null)
const titleAutoLinked = ref(true)
/** 同一发票共用边框色(与 BxInvoiceSelectDialog / bx-mobile 一致) */
const INVOICE_GROUP_COLORS = [
'#0891b2',
'#7c3aed',
'#d97706',
'#db2777',
'#2563eb',
'#059669',
'#ea580c',
'#4f46e5'
]
function invoiceGroupColor(invoiceId: string | number) {
const s = String(invoiceId || '')
let hash = 0
for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) >>> 0
return INVOICE_GROUP_COLORS[hash % INVOICE_GROUP_COLORS.length]
}
/** 关联发票固定展示顺序:上传/创建时间从新到旧,同时间按附件 ID */
function sortInvoicePicksByFixedOrder<T extends { createTime?: string; attachmentId?: string | number; id?: string | number; key?: string }>(
items: T[]
@@ -1384,7 +1404,7 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
.invoice-selected-card {
position: relative;
width: 96px;
border: 1px solid var(--border, #d9e8e2);
border: 2px solid var(--invoice-group-color, var(--border, #d9e8e2));
border-radius: 6px;
background: var(--surface, #fff);
overflow: hidden;
+1 -1
View File
@@ -20,7 +20,7 @@
const detailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
function goBack() {
router.push({ path: '/system/clue' })
router.push({ path: '/system/temp/clue' })
}
onMounted(() => {
+1 -1
View File
@@ -2236,7 +2236,7 @@ const handleViewCustomer = () => {
/** 查看商机 */
const handleViewOpportunity = () => {
router.push({
path: '/system/opportunity/detail',
path: '/system/temp/opportunity/detail',
query: { id: contract.value?.opportunityId }
});
};
@@ -23,7 +23,7 @@
const opportunityId = ref<string>('')
function goBack() {
router.push({ path: '/system/opportunity' })
router.push({ path: '/system/temp/opportunity' })
}
onMounted(() => {
@@ -1029,7 +1029,7 @@
/** 切换视图模式 */
const switchView = (mode: 'list' | 'kanban') => {
if (mode === 'kanban') {
router.push('/system/opportunity/kanban')
router.push('/system/temp/opportunity/kanban')
} else {
viewMode.value = mode
}
@@ -1037,7 +1037,7 @@
/** 跳转到漏斗分析 */
const goToFunnel = () => {
router.push('/system/opportunity/funnel')
router.push('/system/temp/opportunity/funnel')
}
watchDebounced(
@@ -185,13 +185,13 @@ const resetQuery = () => {
}
const handleViewDetail = (id: number) => {
router.push({ path: '/system/opportunity/detail', query: { id } })
router.push({ path: '/system/temp/opportunity/detail', query: { id } })
}
const handleCardAction = (command: string, opp: OpportunityVO) => {
switch (command) {
case 'edit':
router.push({ path: '/system/opportunity', query: { id: opp.id, action: 'edit' } })
router.push({ path: '/system/temp/opportunity', query: { id: opp.id, action: 'edit' } })
break
case 'stage':
changeStageDialogRef.value?.open(opp.id)
@@ -239,7 +239,7 @@ let winRateChart: echarts.ECharts | null = null
let ownerChart: echarts.ECharts | null = null
const goBack = () => {
router.push('/system/opportunity')
router.push('/system/temp/opportunity')
}
const handleQuery = () => {
+79 -48
View File
@@ -78,49 +78,62 @@
<p v-else class="portal-home-empty">暂无收藏,可在各模块菜单卡片上点星标添加</p>
</section>
<section v-else class="portal-home-block">
<div class="portal-home-grid">
<button
v-for="card in activeCards"
:key="card.path"
type="button"
class="portal-home-card"
@click="openCard(card.path)"
>
<span
v-if="pendingCountFor(card) > 0"
class="portal-home-card__pending"
:aria-label="`${pendingCountFor(card)} 条待审批`"
>
{{ formatPendingCount(pendingCountFor(card)) }}
</span>
<span
class="portal-home-card__badge"
:data-tone="toneIndex(activeModule)"
aria-hidden="true"
>
{{ titleAbbr(card.title) }}
</span>
<span class="portal-home-card__body">
<span class="portal-home-card__title">{{ card.title }}</span>
<span class="portal-home-card__sub">{{ panelTitle }}</span>
</span>
<template v-else>
<section
v-for="(group, gIdx) in activeGroups"
:key="group.groupPath || `flat-${gIdx}`"
class="portal-home-block"
:class="{ 'portal-home-block--flat': group.hideTitle }"
>
<div v-if="!group.hideTitle && group.groupTitle" class="portal-home-block__head">
<h3 class="title-bar-heading">{{ group.groupTitle }}</h3>
<span class="portal-home-block__tip">{{ group.cards.length }} 项</span>
</div>
<div class="portal-home-grid">
<button
v-for="card in group.cards"
:key="card.path"
type="button"
class="portal-home-card__star"
:class="{ 'is-on': favoritePathSet.has(card.path) }"
:aria-label="favoritePathSet.has(card.path) ? '取消收藏' : '加入收藏'"
@click.stop="onToggleStar(card.path)"
class="portal-home-card"
@click="openCard(card.path)"
>
<ElIcon>
<StarFilled v-if="favoritePathSet.has(card.path)" />
<Star v-else />
</ElIcon>
<span
v-if="pendingCountFor(card) > 0"
class="portal-home-card__pending"
:aria-label="`${pendingCountFor(card)} 条待审批`"
>
{{ formatPendingCount(pendingCountFor(card)) }}
</span>
<span
class="portal-home-card__badge"
:data-tone="toneIndex(group.groupPath || activeModule)"
aria-hidden="true"
>
{{ titleAbbr(card.title) }}
</span>
<span class="portal-home-card__body">
<span class="portal-home-card__title">{{ card.title }}</span>
<span class="portal-home-card__sub">{{
group.hideTitle || !group.groupTitle ? panelTitle : group.groupTitle
}}</span>
</span>
<button
type="button"
class="portal-home-card__star"
:class="{ 'is-on': favoritePathSet.has(card.path) }"
:aria-label="favoritePathSet.has(card.path) ? '取消收藏' : '加入收藏'"
@click.stop="onToggleStar(card.path)"
>
<ElIcon>
<StarFilled v-if="favoritePathSet.has(card.path)" />
<Star v-else />
</ElIcon>
</button>
</button>
</button>
</div>
<p v-if="!activeCards.length" class="portal-home-empty">该模块暂无可用菜单</p>
</section>
</div>
</section>
<p v-if="!activeGroups.length" class="portal-home-empty">该模块暂无可用菜单</p>
</template>
</div>
</div>
@@ -190,19 +203,22 @@
const isFavView = computed(() => activeModule.value === FAV_KEY)
const activeCards = computed(() => {
if (isFavView.value) return []
const sec = modules.value.find((m) => m.modulePath === activeModule.value)
return sec?.cards || []
const activeSection = computed(() => {
if (isFavView.value) return undefined
return modules.value.find((m) => m.modulePath === activeModule.value)
})
const activeGroups = computed(() => activeSection.value?.groups || [])
const activeCards = computed(() => activeSection.value?.cards || [])
const panelTitle = computed(() => {
if (isFavView.value) return '收藏'
return modules.value.find((m) => m.modulePath === activeModule.value)?.moduleTitle || ''
return activeSection.value?.moduleTitle || ''
})
const panelDesc = computed(() =>
isFavView.value ? '可拖拽调整顺序,星标取消收藏' : '点击卡片进入功能,星标加入收藏'
isFavView.value ? '可拖拽调整顺序,星标取消收藏' : '按二级菜单分组展示,点击卡片进入功能'
)
const panelCount = computed(() =>
@@ -217,6 +233,11 @@
const favSubTitle = (path: string) => {
for (const sec of modules.value) {
for (const group of sec.groups || []) {
if (!group.cards.some((c) => c.path === path)) continue
if (group.hideTitle || !group.groupTitle) return sec.moduleTitle
return `${sec.moduleTitle} / ${group.groupTitle}`
}
if (sec.cards.some((c) => c.path === path)) return sec.moduleTitle
}
return '收藏入口'
@@ -494,15 +515,23 @@
}
.portal-home-block {
margin: 0 18px 18px;
margin: 14px 18px 8px;
&:last-child {
&:last-of-type {
margin-bottom: 4px;
}
&--fav {
margin-top: 14px;
}
&--flat {
margin-top: 14px;
.portal-home-grid {
margin-top: 0;
}
}
}
.portal-home-block__head {
@@ -510,11 +539,13 @@
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: space-between;
}
.portal-home-block__tip {
font-size: 12px;
font-weight: 400;
font-weight: 500;
font-variant-numeric: tabular-nums;
color: var(--mint-text-muted, #94a3b8);
}
+1 -1
View File
@@ -408,7 +408,7 @@
// 返回列表
const goBack = () => {
router.push('/system/user')
router.push('/system/info/user')
}
// 加载数据