提交
This commit is contained in:
1 parent
b45ce2c32c
commit
a5210a6a9e
30 files changed
+11989
-5917
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
18897
|
||||
85846
|
||||
Vendored
-3
@@ -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']
|
||||
|
||||
@@ -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;
|
||||
Vendored
+1
-1
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
+2604
-2329
File diff suppressed because it is too large.
Load diff
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
const detailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
|
||||
function goBack() {
|
||||
router.push({ path: '/system/clue' })
|
||||
router.push({ path: '/system/temp/clue' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -408,7 +408,7 @@
|
||||
|
||||
// 返回列表
|
||||
const goBack = () => {
|
||||
router.push('/system/user')
|
||||
router.push('/system/info/user')
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
|
||||
Reference in new issue
Block a user