feat: 新增财务模块并优化组织管理、角色权限与我的订单

- 新增财务菜单与应收应付、充值记录、账户流水页面及路由

- 组织管理改为左右分栏,优化左侧树层级展示与选中交互

- 角色权限分配改为列表形式,支持各级菜单独立勾选

- 我的订单工具栏按钮样式与独立 loading 交互优化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-04 12:15:14 +08:00
parent 5cd6a5462d
commit 2063621daf
25 changed files with 3589 additions and 1428 deletions

File diff suppressed because it is too large Load Diff

1048
docs/数据权限设计.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -508,16 +508,51 @@ const menuData = [
hideInMenu: true,
parentRoute: '/user',
},
// 一级菜单 - 财务
{
level: 1,
name: '财务',
permissionCode: 'Finance',
route: '/finance',
component: null,
icon: 'icon-storage',
order: 6,
hideInMenu: false,
},
// 二级菜单 - 财务
{
level: 2,
name: '充值记录',
permissionCode: 'User.RechargeRecord',
route: '/user/rechargeRecord',
component: 'views/user/recharge/rechargeRecord',
permissionCode: 'Finance.RechargeRecord',
route: '/finance/rechargeRecord',
component: 'views/finance/rechargeRecord/index',
icon: null,
order: 1,
hideInMenu: false,
parentRoute: '/finance',
},
{
level: 2,
name: '资金流水',
permissionCode: 'Finance.AccountFlow',
route: '/finance/accountFlow',
component: 'views/finance/accountFlow/index',
icon: null,
order: 2,
hideInMenu: false,
parentRoute: '/user',
parentRoute: '/finance',
},
{
level: 2,
name: '应收应付',
permissionCode: 'Finance.ReceivablePayable',
route: '/finance/receivablePayable',
component: 'views/finance/receivablePayable/index',
icon: null,
order: 3,
hideInMenu: false,
parentRoute: '/finance',
},
// 一级菜单 - 系统

View File

@ -103,6 +103,42 @@ server {
try_files /index.html =200;
}
# ============ 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)============
# 页面路径为 camelCase;API 为 kebab(如 /system/dict/list、/system/batch-email/…),勿用宽泛前缀匹配 dict
location = /system/dict {
try_files /index.html =200;
}
location = /system/warehouseAddress {
try_files /index.html =200;
}
location = /system/announcement {
try_files /index.html =200;
}
location ^~ /system/users {
try_files /index.html =200;
}
location ^~ /system/roles {
try_files /index.html =200;
}
location ^~ /system/menus {
try_files /index.html =200;
}
location ^~ /system/organizations {
try_files /index.html =200;
}
location ^~ /system/batchEmail {
try_files /index.html =200;
}
location ^~ /system/otherConfig {
try_files /index.html =200;
}
location ^~ /system/xhsAiAssistant {
try_files /index.html =200;
}
location ^~ /system/dataPerm/ {
try_files /index.html =200;
}
# ============ 微信公众号服务器回调(admin API,非 itfx)============
location ^~ /api/apiwechat/ {
rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break;

View File

@ -39,6 +39,19 @@ server {
location ^~ /operation/outboundScan { try_files /index.html =200; }
location ^~ /operation/printLabel { try_files /index.html =200; }
# 系统管理前端路由(^~ 须比下方 /system/ API 正则代理更优先)
location = /system/dict { try_files /index.html =200; }
location = /system/warehouseAddress { try_files /index.html =200; }
location = /system/announcement { try_files /index.html =200; }
location ^~ /system/users { try_files /index.html =200; }
location ^~ /system/roles { try_files /index.html =200; }
location ^~ /system/menus { try_files /index.html =200; }
location ^~ /system/organizations { try_files /index.html =200; }
location ^~ /system/batchEmail { try_files /index.html =200; }
location ^~ /system/otherConfig { try_files /index.html =200; }
location ^~ /system/xhsAiAssistant { try_files /index.html =200; }
location ^~ /system/dataPerm/ { try_files /index.html =200; }
location ^~ /api/apiwechat/ {
rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break;
proxy_pass http://api.nanafa.com;

View File

@ -0,0 +1,57 @@
import { axiosInstance } from '@/api/rbac'
import { omsUrl, parseListPayload } from '@/api/oms/common'
export interface ReceivablePayableListParams {
page: number
pageSize: number
keyword?: string
feeType?: string
costItem?: string
chargingStage?: string
customerId?: number
startTime?: string
endTime?: string
}
export interface ReceivablePayableRow {
serviceNo: string
clientNo?: string
trackNo?: string
customerCode?: string
orderStatus: string
orderStatusName?: string
isPaid: number
payTime?: string
receiveTime?: string
shipTime?: string
productCode?: string
channelCode?: string
consigneeCountryName?: string
feeType: string
feeTypeName?: string
costItem: string
amount: number
currency: string
downstreamProvider?: string
priceNo?: string
priceEffectiveTime?: string
chargingStage?: string
chargingStageName?: string
}
export async function getReceivablePayableList(params: ReceivablePayableListParams) {
const res = await axiosInstance.get(omsUrl('/finance/receivable-payable/list'), {
params: {
page: params.page,
pageSize: params.pageSize,
keyword: params.keyword || undefined,
feeType: params.feeType && params.feeType !== 'all' ? params.feeType : undefined,
costItem: params.costItem || undefined,
chargingStage: params.chargingStage && params.chargingStage !== 'all' ? params.chargingStage : undefined,
customerId: params.customerId || undefined,
startTime: params.startTime || undefined,
endTime: params.endTime || undefined,
},
})
return parseListPayload<ReceivablePayableRow>(res.data)
}

View File

@ -36,6 +36,8 @@
showPagination?: boolean;
pageSize?: number;
currentPage?: number;
/** 树形层级展示(保留缩进/展开箭头,适用于组织等多级树) */
hierarchical?: boolean;
}>(),
{
selectedKeys: () => [],
@ -62,6 +64,7 @@
showPagination: true,
pageSize: 10,
currentPage: 1,
hierarchical: false,
}
);
@ -268,6 +271,14 @@
if (nodeData) emit('nodeClick', key, nodeData);
}
function selectTreeNode(key: string) {
if (!key) return;
selectedKeysModel.value = [key];
const nodeData = getNodeData(key);
emit('select', [key], { node: nodeData });
if (nodeData) emit('nodeClick', key, nodeData);
}
defineExpose({
getNodeData,
getNodeIndex,
@ -317,7 +328,10 @@
</div>
</div>
<div class="interface-tree">
<div
class="interface-tree"
:class="{ 'interface-tree--hierarchical': hierarchical }"
>
<div v-if="loading" class="loading-state">
<a-spin />
<p>{{ loadingText }}</p>
@ -335,14 +349,20 @@
<template #title="{ key, title }">
<div
class="tree-node"
:class="{ selected: selectedKeysModel.includes(key) }"
:class="{
selected: selectedKeysModel.includes(key),
'tree-node--hierarchical': hierarchical,
'tree-node--branch': hierarchical && getNodeData(key)?.isLeaf !== true,
[`tree-node--depth-${getNodeData(key)?.depth || 1}`]: hierarchical,
}"
@click="hierarchical && selectTreeNode(key)"
>
<table class="node-table">
<tbody>
<tr>
<td class="node-content-cell">
<span v-if="selectedKeysModel.includes(key)" class="selected-dot"></span>
<span v-if="getNodeData(key)?.isLeaf && showNodeIndex" class="node-number">{{ getNodeIndex(key) }}</span>
<span v-if="selectedKeysModel.includes(key) && !hierarchical" class="selected-dot"></span>
<span v-if="getNodeData(key)?.isLeaf && showNodeIndex && !hierarchical" class="node-number">{{ getNodeIndex(key) }}</span>
<span v-if="getNodeData(key)?.isLeaf && showMethod" class="post-method">{{ methodLabel }}</span>
<span class="node-title">{{ title }}</span>
<span v-if="getNodeData(key)?.isLeaf && showMapCount" class="map-count">
@ -421,7 +441,10 @@
</td>
</tr>
<tr
v-if="!getNodeData(key)?.isLeaf && (getNodeData(key)?.subtitle || $slots['branch-node-subtitle'])"
v-if="
(hierarchical || getNodeData(key)?.isLeaf !== true) &&
(getNodeData(key)?.subtitle || $slots['branch-node-subtitle'])
"
>
<td colspan="2" class="node-subtitle-cell">
<slot name="branch-node-subtitle" :node-data="getNodeData(key)" :node-key="key">
@ -616,55 +639,211 @@
border-radius: 16px;
padding-right: 4px;
:deep(.arco-tree),
:deep(.arco-tree-show-line),
:deep(.arco-tree-size-medium),
:deep(.arco-tree-node),
:deep(.arco-tree-node-title),
:deep(.arco-tree-node-title-text),
:deep(.arco-tree-node-content),
:deep(.arco-tree-node-content-wrapper) {
display: contents !important;
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
&:not(.interface-tree--hierarchical) {
:deep(.arco-tree),
:deep(.arco-tree-show-line),
:deep(.arco-tree-size-medium),
:deep(.arco-tree-node),
:deep(.arco-tree-node-title),
:deep(.arco-tree-node-title-text),
:deep(.arco-tree-node-content),
:deep(.arco-tree-node-content-wrapper) {
display: contents !important;
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
:deep(.arco-tree-node-icon),
:deep(.arco-tree-node-indent),
:deep(.arco-tree-node-switcher),
:deep(.arco-icon),
:deep(.arco-tree-node-title .arco-icon),
:deep(.arco-tree-node-line),
:deep(.arco-tree-node-line-top),
:deep(.arco-tree-node-line-bottom) {
display: none !important;
}
:deep(.arco-tree-node-wrapper),
:deep(.arco-tree-node-selected),
:deep(.arco-tree-node-selected > .arco-tree-node-title) {
display: contents !important;
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
border: none !important;
}
:deep(.arco-tree-node-content) {
display: block !important;
}
:deep(.arco-tree-list),
:deep(.arco-tree-list-holder) {
display: contents !important;
padding: 0 !important;
margin: 0 !important;
}
}
:deep(.arco-tree-node-icon),
:deep(.arco-tree-node-indent),
:deep(.arco-tree-node-switcher),
:deep(.arco-icon),
:deep(.arco-tree-node-title .arco-icon),
:deep(.arco-tree-node-line),
:deep(.arco-tree-node-line-top),
:deep(.arco-tree-node-line-bottom) {
display: none !important;
}
&.interface-tree--hierarchical {
overflow-y: auto;
padding-right: 2px;
:deep(.arco-tree-node-wrapper),
:deep(.arco-tree-node-selected),
:deep(.arco-tree-node-selected > .arco-tree-node-title) {
display: contents !important;
padding: 0 !important;
margin: 0 !important;
background: transparent !important;
border: none !important;
}
:deep(.arco-tree-node) {
align-items: flex-start;
}
:deep(.arco-tree-node-content) {
display: block !important;
}
:deep(.arco-tree-node-switcher) {
display: inline-flex !important;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.72);
margin-top: 10px;
}
:deep(.arco-tree-list),
:deep(.arco-tree-list-holder) {
display: contents !important;
padding: 0 !important;
margin: 0 !important;
:deep(.arco-tree-node-indent-block) {
display: inline-block !important;
}
:deep(.arco-tree-node-title),
:deep(.arco-tree-node-title-text),
:deep(.arco-tree-node-content),
:deep(.arco-tree-node-content-wrapper) {
flex: 1;
min-width: 0;
width: 100%;
padding: 0 !important;
background: transparent !important;
}
:deep(.arco-tree-node-title) {
cursor: pointer;
}
:deep(.arco-tree-node-title-text) {
display: block;
width: 100%;
}
:deep(.arco-tree-node-selected .arco-tree-node-title),
:deep(.arco-tree-node-selected .arco-tree-node-content) {
background: transparent !important;
}
:deep(.arco-tree-node-line),
:deep(.arco-tree-node-line-top),
:deep(.arco-tree-node-line-bottom) {
border-color: rgba(255, 255, 255, 0.18) !important;
}
.tree-node {
width: 100%;
margin: 2px 0 6px;
padding: 8px 10px;
border-radius: 10px;
border-left-width: 3px;
border-left-style: solid;
transform: none;
cursor: pointer;
box-sizing: border-box;
&:hover {
transform: none;
}
&.selected {
background: #fff394 !important;
border-left-color: #fff394 !important;
box-shadow: none;
color: #000 !important;
.node-title,
.node-subtitle {
color: #000 !important;
}
&.tree-node--branch .node-title::before {
background: rgba(0, 0, 0, 0.45);
}
}
}
.tree-node--depth-1 {
border-left-color: var(--app-primary-color);
background: rgba(255, 255, 255, 0.08);
.node-title {
font-size: 14px;
font-weight: 700;
}
}
.tree-node--depth-2 {
border-left-color: rgba(45, 140, 140, 0.75);
background: rgba(255, 255, 255, 0.05);
.node-title {
font-size: 13px;
font-weight: 600;
}
}
.tree-node--depth-3 {
border-left-color: rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.03);
.node-title {
font-size: 13px;
font-weight: 500;
}
}
.tree-node--depth-4,
.tree-node--depth-5,
.tree-node--depth-6 {
border-left-color: rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.02);
padding: 6px 10px;
.node-title {
font-size: 12px;
font-weight: 500;
}
}
.tree-node--branch .node-title::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
margin-right: 6px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.45);
vertical-align: middle;
transform: translateY(-1px);
}
.node-subtitle {
color: rgba(255, 255, 255, 0.58);
font-size: 11px;
}
}
}
.interface-tree--hierarchical::-webkit-scrollbar {
width: 4px;
display: block;
}
.interface-tree--hierarchical::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.18);
border-radius: 4px;
}
.interface-tree::-webkit-scrollbar {
display: none;
}

View File

@ -479,7 +479,10 @@ const menuNameToLocaleKeyMap: Record<string, string> = {
'客户管理': 'menu.user.customer',
'个人中心': 'menu.user.profile',
'资金充值': 'menu.user.recharge',
'充值记录': 'menu.user.rechargeRecord',
'财务': 'menu.finance',
'充值记录': 'menu.finance.rechargeRecord',
'资金流水': 'menu.finance.accountFlow',
'应收应付': 'menu.finance.receivablePayable',
'订单': 'menu.order',
'我要查价': 'menu.order.priceCheck',
'我要下单': 'menu.order.placeOrder',
@ -539,6 +542,10 @@ const menuNameToLocaleKeyMap: Record<string, string> = {
'技能卡片': 'menu.xhs.skills',
'拟人设置': 'menu.xhs.humanSettings',
'AI 助手': 'menu.system.xhsAiAssistant',
'数据权限': 'menu.system.dataPerm',
'用户数据权限': 'menu.system.dataPermUsers',
'权限模板': 'menu.system.dataPermTemplates',
'权限审计': 'menu.system.dataPermAudit',
};
// 格式化菜单标题
@ -904,9 +911,9 @@ const onBalanceMenuClick = (event: any) => {
if (key === 'recharge') {
router.push({ name: 'VipRechargeDeposit' });
} else if (key === 'rechargeRecord') {
router.push({ name: 'VipRechargeRechargeRecord' });
router.push({ name: 'FinanceRechargeRecord' });
} else if (key === 'accountFlow') {
router.push({ name: 'UserAccountFlow' });
router.push({ name: 'FinanceAccountFlow' });
}
};

View File

@ -63,8 +63,10 @@ export default {
'menu.user.subAccount': 'Sub Accounts',
'menu.user.profile': 'Personal Center',
'menu.user.recharge': 'Recharge',
'menu.user.rechargeRecord': 'Recharge Records',
'menu.user.accountFlow': 'Account Transactions',
'menu.finance': 'Finance',
'menu.finance.rechargeRecord': 'Recharge Records',
'menu.finance.accountFlow': 'Account Transactions',
'menu.finance.receivablePayable': 'Receivables & Payables',
'menu.arcoWebsite': 'Arco Design',
'menu.faq': 'FAQ',
'menu.example': 'Example',
@ -312,6 +314,7 @@ export default {
'order.myOrders.actions.print': 'Print',
'order.myOrders.actions.export': 'Export',
'order.myOrders.actions.modifyOrder': 'Edit order',
'order.myOrders.actions.more': 'More',
'order.myOrders.pagination.totalRecords': 'Total {count} records',
'order.myOrders.pagination.perPage': 'per page',
@ -1020,7 +1023,7 @@ export default {
// System - Role Management (Additional)
'system.role.modal.add': 'Add Role',
'system.role.modal.edit': 'Edit Role',
'system.role.modal.assignPermissions': 'Assign Permissions',
'system.role.modal.assignPermissions': 'Assign Menus',
'system.role.form.name': 'Role Name',
'system.role.form.namePlaceholder': 'Enter role name',
'system.role.form.code': 'Role Code',
@ -1051,6 +1054,9 @@ export default {
'system.role.permission.total': 'Total',
'system.role.permission.selectedCount': '{selected} / {total} selected',
'system.role.permission.noMatch': 'No matching permissions',
'system.role.permission.col.level1': 'Level 1 menu',
'system.role.permission.col.level2': 'Level 2 menu',
'system.role.permission.col.level3': 'Level 3 buttons',
'system.role.tag.creating': 'Creating',
'system.role.tag.boundWarehouse': 'Warehouse bound',
'system.role.message.deleteSuccess': 'Delete successful',
@ -1124,6 +1130,12 @@ export default {
'system.org.message.addSuccess': 'Add successfully',
'system.org.message.addFailed': 'Add failed',
'system.org.message.getDetailFailed': 'Failed to get organization details',
'system.org.loading': 'Loading organizations...',
'system.org.level.root': 'Level 1',
'system.org.level.n': 'Level {n}',
'system.org.level.children': '{count} sub-orgs',
'system.org.users.emptySelect': 'Select an organization',
'system.org.users.emptyDesc': 'Select an organization on the left to view its users',
// System - Position Management
'system.position.search.placeholder.orgName': 'Enter organization name',
@ -1761,6 +1773,47 @@ export default {
'user.accountFlow.table.balanceAfter': 'Balance after',
'user.accountFlow.table.remark': 'Remark',
'user.accountFlow.table.customerCode': 'Customer code',
// Finance - receivable & payable
'finance.receivablePayable.search.keywordPlaceholder': 'Service no. / client no. / track no.',
'finance.receivablePayable.search.feeTypePlaceholder': 'Fee type',
'finance.receivablePayable.search.feeTypeAll': 'All',
'finance.receivablePayable.search.costItemPlaceholder': 'Cost item',
'finance.receivablePayable.search.chargingStagePlaceholder': 'Charging stage',
'finance.receivablePayable.search.chargingStageAll': 'All',
'finance.receivablePayable.search.customerIdPlaceholder': 'Customer ID (optional)',
'finance.receivablePayable.search.dateRange': 'Order created time',
'finance.receivablePayable.search.actions.search': 'Search',
'finance.receivablePayable.search.actions.reset': 'Reset',
'finance.receivablePayable.feeType.receivable': 'Receivable',
'finance.receivablePayable.feeType.payable': 'Payable',
'finance.receivablePayable.chargingStage.forecast': 'Forecast',
'finance.receivablePayable.chargingStage.inbound': 'Inbound',
'finance.receivablePayable.chargingStage.outbound': 'Outbound',
'finance.receivablePayable.isPaid.yes': 'Paid',
'finance.receivablePayable.isPaid.no': 'Unpaid',
'finance.receivablePayable.message.loadError': 'Failed to load receivables & payables',
'finance.receivablePayable.table.serviceNo': 'Service no.',
'finance.receivablePayable.table.clientNo': 'Client no.',
'finance.receivablePayable.table.trackNo': 'Track no.',
'finance.receivablePayable.table.customerCode': 'Customer code',
'finance.receivablePayable.table.feeType': 'Fee type',
'finance.receivablePayable.table.costItem': 'Cost item',
'finance.receivablePayable.table.amount': 'Amount',
'finance.receivablePayable.table.currency': 'Currency',
'finance.receivablePayable.table.chargingStage': 'Charging stage',
'finance.receivablePayable.table.orderStatus': 'Order status',
'finance.receivablePayable.table.isPaid': 'Payment status',
'finance.receivablePayable.table.productCode': 'Product code',
'finance.receivablePayable.table.channelCode': 'Channel code',
'finance.receivablePayable.table.consigneeCountryName': 'Destination country',
'finance.receivablePayable.table.downstreamProvider': 'Downstream provider',
'finance.receivablePayable.table.priceNo': 'Price no.',
'finance.receivablePayable.table.priceEffectiveTime': 'Price effective time',
'finance.receivablePayable.table.payTime': 'Pay time',
'finance.receivablePayable.table.receiveTime': 'Receive time',
'finance.receivablePayable.table.shipTime': 'Ship time',
'user.recharge.record.message.exportError': 'Export failed',
'user.recharge.record.pagination.total': '{count} total',
'user.recharge.record.pagination.prev': 'Prev',

View File

@ -63,8 +63,10 @@ export default {
'menu.user.subAccount': '子账号',
'menu.user.profile': '个人中心',
'menu.user.recharge': '资金充值',
'menu.user.rechargeRecord': '充值记录',
'menu.user.accountFlow': '资金流水',
'menu.finance': '财务',
'menu.finance.rechargeRecord': '充值记录',
'menu.finance.accountFlow': '资金流水',
'menu.finance.receivablePayable': '应收应付',
'menu.arcoWebsite': 'Arco Design',
'menu.faq': '常见问题',
'menu.example': '示例页',
@ -198,6 +200,7 @@ export default {
'order.myOrders.actions.print': '打印',
'order.myOrders.actions.export': '导出',
'order.myOrders.actions.modifyOrder': '修改订单',
'order.myOrders.actions.more': '更多',
'order.myOrders.pagination.totalRecords': '共{count}条记录',
'order.myOrders.pagination.perPage': '条/页',
@ -906,7 +909,7 @@ export default {
// 系统 - 角色管理(补充)
'system.role.modal.add': '新增角色',
'system.role.modal.edit': '编辑角色',
'system.role.modal.assignPermissions': '分配权限',
'system.role.modal.assignPermissions': '分配菜单',
'system.role.form.name': '角色名称',
'system.role.form.namePlaceholder': '请输入角色名称',
'system.role.form.code': '角色编码',
@ -937,6 +940,9 @@ export default {
'system.role.permission.total': '总计',
'system.role.permission.selectedCount': '已选 {selected} / 共 {total}',
'system.role.permission.noMatch': '未找到匹配的权限',
'system.role.permission.col.level1': '一级菜单',
'system.role.permission.col.level2': '二级菜单',
'system.role.permission.col.level3': '三级按钮',
'system.role.tag.creating': '新建中',
'system.role.tag.boundWarehouse': '已绑仓库',
'system.role.message.deleteSuccess': '删除成功',
@ -1010,6 +1016,12 @@ export default {
'system.org.message.addSuccess': '新增成功',
'system.org.message.addFailed': '新增失败',
'system.org.message.getDetailFailed': '获取组织详情失败',
'system.org.loading': '正在加载组织...',
'system.org.level.root': '一级组织',
'system.org.level.n': '{n} 级组织',
'system.org.level.children': '{count} 个下级',
'system.org.users.emptySelect': '请选择组织',
'system.org.users.emptyDesc': '请从左侧选择组织,查看该组织下的用户列表',
// 系统 - 职位管理
'system.position.search.placeholder.orgName': '请输入组织名称',
@ -1780,6 +1792,47 @@ export default {
'user.accountFlow.table.balanceAfter': '变动后余额',
'user.accountFlow.table.remark': '备注',
'user.accountFlow.table.customerCode': '客户编码',
// 财务 - 应收应付
'finance.receivablePayable.search.keywordPlaceholder': '运单号 / 客户单号 / 跟踪号',
'finance.receivablePayable.search.feeTypePlaceholder': '费用类型',
'finance.receivablePayable.search.feeTypeAll': '全部',
'finance.receivablePayable.search.costItemPlaceholder': '费用项',
'finance.receivablePayable.search.chargingStagePlaceholder': '计费阶段',
'finance.receivablePayable.search.chargingStageAll': '全部',
'finance.receivablePayable.search.customerIdPlaceholder': '客户ID(选填)',
'finance.receivablePayable.search.dateRange': '订单创建时间',
'finance.receivablePayable.search.actions.search': '搜索',
'finance.receivablePayable.search.actions.reset': '重置',
'finance.receivablePayable.feeType.receivable': '应收',
'finance.receivablePayable.feeType.payable': '应付',
'finance.receivablePayable.chargingStage.forecast': '预报',
'finance.receivablePayable.chargingStage.inbound': '入库',
'finance.receivablePayable.chargingStage.outbound': '出库',
'finance.receivablePayable.isPaid.yes': '已支付',
'finance.receivablePayable.isPaid.no': '未支付',
'finance.receivablePayable.message.loadError': '加载应收应付失败',
'finance.receivablePayable.table.serviceNo': '运单号',
'finance.receivablePayable.table.clientNo': '客户单号',
'finance.receivablePayable.table.trackNo': '跟踪号',
'finance.receivablePayable.table.customerCode': '客户编码',
'finance.receivablePayable.table.feeType': '费用类型',
'finance.receivablePayable.table.costItem': '费用项',
'finance.receivablePayable.table.amount': '金额',
'finance.receivablePayable.table.currency': '币种',
'finance.receivablePayable.table.chargingStage': '计费阶段',
'finance.receivablePayable.table.orderStatus': '订单状态',
'finance.receivablePayable.table.isPaid': '支付状态',
'finance.receivablePayable.table.productCode': '产品编码',
'finance.receivablePayable.table.channelCode': '渠道编码',
'finance.receivablePayable.table.consigneeCountryName': '目的国',
'finance.receivablePayable.table.downstreamProvider': '下游服务商',
'finance.receivablePayable.table.priceNo': '价格编号',
'finance.receivablePayable.table.priceEffectiveTime': '价格生效时间',
'finance.receivablePayable.table.payTime': '支付时间',
'finance.receivablePayable.table.receiveTime': '收货时间',
'finance.receivablePayable.table.shipTime': '发货时间',
'user.recharge.record.message.exportError': '导出失败',
'user.recharge.record.pagination.total': '共 {count} 条',
'user.recharge.record.pagination.prev': '上一页',

View File

@ -0,0 +1,48 @@
import { DEFAULT_LAYOUT } from '../base';
import { AppRouteRecordRaw } from '../types';
const FINANCE: AppRouteRecordRaw = {
path: '/finance',
name: 'Finance',
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.finance',
icon: 'icon-storage',
requiresAuth: true,
order: 6,
},
children: [
{
path: 'rechargeRecord',
name: 'FinanceRechargeRecord',
component: () => import('@/views/finance/rechargeRecord/index.vue'),
meta: {
locale: 'menu.finance.rechargeRecord',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'accountFlow',
name: 'FinanceAccountFlow',
component: () => import('@/views/finance/accountFlow/index.vue'),
meta: {
locale: 'menu.finance.accountFlow',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'receivablePayable',
name: 'FinanceReceivablePayable',
component: () => import('@/views/finance/receivablePayable/index.vue'),
meta: {
locale: 'menu.finance.receivablePayable',
requiresAuth: true,
roles: ['*'],
},
},
],
};
export default FINANCE;

View File

@ -55,26 +55,6 @@ const USER: AppRouteRecordRaw = {
hideInMenu: true,
},
},
{
path: 'rechargeRecord',
name: 'VipRechargeRechargeRecord',
component: () => import('@/views/user/recharge/rechargeRecord.vue'),
meta: {
locale: 'menu.user.rechargeRecord',
requiresAuth: true,
roles: ['*'],
},
},
{
path: 'accountFlow',
name: 'UserAccountFlow',
component: () => import('@/views/user/accountFlow/index.vue'),
meta: {
locale: 'menu.user.accountFlow',
requiresAuth: true,
roles: ['*'],
},
},
],
};

View File

@ -61,7 +61,9 @@ export function resolveLoginRouteNameFromPath(path: string): string {
path.startsWith('/user/profile') ||
path.startsWith('/user/recharge') ||
path.startsWith('/user/sub-account') ||
path === '/user/rechargeRecord'
path.startsWith('/finance/rechargeRecord') ||
path.startsWith('/finance/accountFlow') ||
path.startsWith('/finance/receivablePayable')
) {
return CUSTOMER_LOGIN_ROUTE_NAME
}

View File

@ -0,0 +1,98 @@
import type { OrganizationTreeDto } from '@/api/rbac'
import type { CustomLeftMenuTreeNode } from '@/components/CustomLeftMenu/types'
export function buildOrganizationLeftMenuTree(
data: OrganizationTreeDto[],
getTypeLabel?: (type?: number, depth?: number, childCount?: number) => string,
depth = 1
): CustomLeftMenuTreeNode[] {
return data.map((item) => {
const hasChildren = Boolean(item.children?.length)
const childCount = item.children?.length ?? 0
return {
key: `org-${item.id}`,
title: item.name || item.code || '',
isLeaf: !hasChildren,
id: item.id,
name: item.name,
code: item.code || '',
depth,
childCount,
subtitle: getTypeLabel?.(item.type, depth, childCount),
children: hasChildren
? buildOrganizationLeftMenuTree(item.children!, getTypeLabel, depth + 1)
: undefined,
}
})
}
export function findOrganizationByMenuKey(
key: string,
tree: OrganizationTreeDto[]
): OrganizationTreeDto | undefined {
if (!key.startsWith('org-')) return undefined
const id = key.slice(4)
function walk(nodes: OrganizationTreeDto[]): OrganizationTreeDto | undefined {
for (const node of nodes) {
if (node.id === id) return node
if (node.children?.length) {
const found = walk(node.children)
if (found) return found
}
}
return undefined
}
return walk(tree)
}
export function filterOrganizationMenuTree(
nodes: CustomLeftMenuTreeNode[],
keyword: string
): CustomLeftMenuTreeNode[] {
const trimmed = keyword.trim()
if (!trimmed) return nodes
const lower = trimmed.toLowerCase()
function filter(list: CustomLeftMenuTreeNode[]): CustomLeftMenuTreeNode[] {
const result: CustomLeftMenuTreeNode[] = []
list.forEach((node) => {
const children = node.children?.length ? filter(node.children) : []
const selfMatch =
node.title?.toLowerCase().includes(lower) ||
node.code?.toLowerCase().includes(lower)
if (selfMatch || children.length) {
result.push({
...node,
children: children.length ? children : selfMatch ? node.children : undefined,
})
}
})
return result
}
return filter(nodes)
}
export function collectOrganizationMenuExpandKeys(
nodes: CustomLeftMenuTreeNode[],
maxDepth = 2,
depth = 1
): string[] {
const keys: string[] = []
nodes.forEach((node) => {
if (node.children?.length && depth <= maxDepth) {
keys.push(node.key)
keys.push(...collectOrganizationMenuExpandKeys(node.children, maxDepth, depth + 1))
}
})
return keys
}
export function findFirstOrganizationMenuKey(nodes: CustomLeftMenuTreeNode[]): string | undefined {
if (!nodes.length) return undefined
const first = nodes[0]
return first.key
}

View File

@ -0,0 +1,113 @@
import type { PermissionDefinitionDto, PermissionGroupDto } from '@/api/rbac'
export interface PermissionAssignButton {
code: string
name: string
}
export interface PermissionAssignRow {
rowKey: string
level1Code: string
level1Name: string
level2Code: string
level2Name: string
/** 二级菜单下是否还有按钮级子权限 */
level2HasChildren: boolean
buttons: PermissionAssignButton[]
}
export function buildPermissionAssignRows(
groups: PermissionGroupDto[]
): PermissionAssignRow[] {
const rows: PermissionAssignRow[] = []
groups.forEach((group) => {
const level1Code = group.code || ''
const level1Name = group.name || level1Code
;(group.permissions || []).forEach((perm) => {
const row = mapPermissionToRow(perm, level1Code, level1Name)
if (row) rows.push(row)
})
})
return rows
}
function mapPermissionToRow(
perm: PermissionDefinitionDto,
level1Code: string,
level1Name: string
): PermissionAssignRow | null {
const level2Code = perm.code || ''
const level2Name = perm.name || level2Code
if (!level2Code) return null
const childButtons = (perm.children || [])
.filter((child) => child.code)
.map((child) => ({
code: child.code!,
name: child.name || child.code!,
}))
return {
rowKey: level2Code,
level1Code,
level1Name,
level2Code,
level2Name,
level2HasChildren: childButtons.length > 0,
buttons: childButtons,
}
}
export function computeLevel1RowSpans(rows: PermissionAssignRow[]): number[] {
const spans = new Array<number>(rows.length).fill(1)
let index = 0
while (index < rows.length) {
let end = index + 1
while (end < rows.length && rows[end].level1Code === rows[index].level1Code) {
end += 1
}
spans[index] = end - index
for (let i = index + 1; i < end; i += 1) {
spans[i] = 0
}
index = end
}
return spans
}
export function filterPermissionAssignRows(
rows: PermissionAssignRow[],
keyword: string
): PermissionAssignRow[] {
const trimmed = keyword.trim()
if (!trimmed) return rows
const lower = trimmed.toLowerCase()
return rows.filter((row) => {
if (row.level1Name.toLowerCase().includes(lower)) return true
if (row.level1Code.toLowerCase().includes(lower)) return true
if (row.level2Name.toLowerCase().includes(lower)) return true
if (row.level2Code.toLowerCase().includes(lower)) return true
return row.buttons.some(
(btn) =>
btn.name.toLowerCase().includes(lower) ||
btn.code.toLowerCase().includes(lower)
)
})
}
export function collectPermissionCodesFromRows(rows: PermissionAssignRow[]): string[] {
const codes = new Set<string>()
rows.forEach((row) => {
if (row.level1Code) codes.add(row.level1Code)
if (row.level2Code) codes.add(row.level2Code)
row.buttons.forEach((btn) => {
if (btn.code) codes.add(btn.code)
})
})
return Array.from(codes)
}

View File

@ -92,8 +92,14 @@ const routeNameMap: Record<string, string> = {
'/user': 'User',
'user_recharge': 'VipRechargeDeposit',
'/user/recharge': 'VipRechargeDeposit',
'user_recharge_record': 'VipRechargeRechargeRecord',
'/user/rechargeRecord': 'VipRechargeRechargeRecord',
'finance': 'Finance',
'/finance': 'Finance',
'finance_recharge_record': 'FinanceRechargeRecord',
'/finance/rechargeRecord': 'FinanceRechargeRecord',
'finance_account_flow': 'FinanceAccountFlow',
'/finance/accountFlow': 'FinanceAccountFlow',
'finance_receivable_payable': 'FinanceReceivablePayable',
'/finance/receivablePayable': 'FinanceReceivablePayable',
// 渠道相关
'channel': 'Channel',

View File

@ -91,7 +91,7 @@ import dayjs from 'dayjs'
import { getAccountFlowList } from '@/api/customer/accountFlow'
defineOptions({
name: 'UserAccountFlow',
name: 'FinanceAccountFlow',
})
const { t } = useI18n()

View File

@ -0,0 +1,299 @@
<template>
<div class="receivable-payable-page">
<div class="receivable-payable-page-card">
<div class="table-toolbar">
<div class="toolbar-left">
<a-space wrap :size="12">
<a-input
v-model="keyword"
allow-clear
:placeholder="t('finance.receivablePayable.search.keywordPlaceholder')"
style="width: 220px"
@press-enter="handleSearch"
/>
<a-select
v-model="feeTypeFilter"
:placeholder="t('finance.receivablePayable.search.feeTypePlaceholder')"
allow-clear
style="width: 140px"
>
<a-option value="all">{{ t('finance.receivablePayable.search.feeTypeAll') }}</a-option>
<a-option value="receivable">{{ t('finance.receivablePayable.feeType.receivable') }}</a-option>
<a-option value="payable">{{ t('finance.receivablePayable.feeType.payable') }}</a-option>
</a-select>
<a-input
v-model="costItem"
allow-clear
:placeholder="t('finance.receivablePayable.search.costItemPlaceholder')"
style="width: 140px"
@press-enter="handleSearch"
/>
<a-select
v-model="chargingStageFilter"
:placeholder="t('finance.receivablePayable.search.chargingStagePlaceholder')"
allow-clear
style="width: 140px"
>
<a-option value="all">{{ t('finance.receivablePayable.search.chargingStageAll') }}</a-option>
<a-option value="forecast">{{ t('finance.receivablePayable.chargingStage.forecast') }}</a-option>
<a-option value="inbound">{{ t('finance.receivablePayable.chargingStage.inbound') }}</a-option>
<a-option value="outbound">{{ t('finance.receivablePayable.chargingStage.outbound') }}</a-option>
</a-select>
<a-input-number
v-model="customerIdFilter"
:placeholder="t('finance.receivablePayable.search.customerIdPlaceholder')"
:min="1"
:precision="0"
allow-clear
hide-button
style="width: 140px"
/>
<a-range-picker
v-model="dateRange"
show-time
format="YYYY-MM-DD HH:mm:ss"
:placeholder="[
t('finance.receivablePayable.search.dateRange'),
t('finance.receivablePayable.search.dateRange'),
]"
style="width: 380px"
/>
<a-button type="primary" @click="handleSearch">
{{ t('finance.receivablePayable.search.actions.search') }}
</a-button>
<a-button @click="handleReset">
{{ t('finance.receivablePayable.search.actions.reset') }}
</a-button>
</a-space>
</div>
<div class="toolbar-right">
<div class="pagination-controls">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
</div>
<a-spin class="list-spin" :loading="listLoading">
<div ref="tableContainerRef" class="list-wrap">
<HandsontablePro
:columns="hotColumns"
:data="tableRows"
:total="totalCount"
:current-page="currentPage"
:page-size="pageSize"
:read-only="true"
:stretch-columns="true"
:custom-cell-renderer="customCellRenderer"
:height="handsontableHeight"
/>
</div>
</a-spin>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import dayjs from 'dayjs'
import {
getReceivablePayableList,
type ReceivablePayableRow,
} from '@/api/finance/receivablePayable'
defineOptions({
name: 'FinanceReceivablePayable',
})
const { t } = useI18n()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 400))
const tableRows = ref<ReceivablePayableRow[]>([])
const totalCount = ref(0)
const listLoading = ref(false)
const keyword = ref('')
const feeTypeFilter = ref('all')
const costItem = ref('')
const chargingStageFilter = ref('all')
const customerIdFilter = ref<number | undefined>(undefined)
const dateRange = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(50)
const hotColumns = computed(() => [
{ key: 'serviceNo', label: t('finance.receivablePayable.table.serviceNo'), width: 160 },
{ key: 'clientNo', label: t('finance.receivablePayable.table.clientNo'), width: 140 },
{ key: 'trackNo', label: t('finance.receivablePayable.table.trackNo'), width: 140 },
{ key: 'customerCode', label: t('finance.receivablePayable.table.customerCode'), width: 110 },
{ key: 'feeTypeName', label: t('finance.receivablePayable.table.feeType'), width: 90 },
{ key: 'costItem', label: t('finance.receivablePayable.table.costItem'), width: 120 },
{ key: 'amount', label: t('finance.receivablePayable.table.amount'), width: 100 },
{ key: 'currency', label: t('finance.receivablePayable.table.currency'), width: 80 },
{ key: 'chargingStageName', label: t('finance.receivablePayable.table.chargingStage'), width: 90 },
{ key: 'orderStatusName', label: t('finance.receivablePayable.table.orderStatus'), width: 100 },
{ key: 'isPaid', label: t('finance.receivablePayable.table.isPaid'), width: 90 },
{ key: 'productCode', label: t('finance.receivablePayable.table.productCode'), width: 120 },
{ key: 'channelCode', label: t('finance.receivablePayable.table.channelCode'), width: 120 },
{ key: 'consigneeCountryName', label: t('finance.receivablePayable.table.consigneeCountryName'), width: 100 },
{ key: 'downstreamProvider', label: t('finance.receivablePayable.table.downstreamProvider'), width: 120 },
{ key: 'priceNo', label: t('finance.receivablePayable.table.priceNo'), width: 120 },
{ key: 'priceEffectiveTime', label: t('finance.receivablePayable.table.priceEffectiveTime'), width: 170 },
{ key: 'payTime', label: t('finance.receivablePayable.table.payTime'), width: 170 },
{ key: 'receiveTime', label: t('finance.receivablePayable.table.receiveTime'), width: 170 },
{ key: 'shipTime', label: t('finance.receivablePayable.table.shipTime'), width: 170 },
])
function formatMoney(value: unknown) {
const num = Number(value)
return Number.isFinite(num) ? num.toFixed(2) : '--'
}
const customCellRenderer = (rowData: ReceivablePayableRow, colKey: string, value: any) => {
if (colKey === 'amount') {
const num = Number(value ?? 0)
const color = rowData.feeType === 'payable' ? '#f53f3f' : '#00b42a'
return `<span style="color:${color};font-weight:500;">${formatMoney(num)}</span>`
}
if (colKey === 'isPaid') {
const paid = Number(value) === 1
const text = paid
? t('finance.receivablePayable.isPaid.yes')
: t('finance.receivablePayable.isPaid.no')
const style = paid
? 'color:#00b42a;background:#e8ffea;border:1px solid #b9f0c2;'
: 'color:#4e5969;background:#f2f3f5;border:1px solid #e5e6eb;'
return `<span style="display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:4px;font-size:12px;font-weight:500;${style}">${text}</span>`
}
return null
}
function buildListParams() {
const params: Record<string, any> = {
page: currentPage.value,
pageSize: pageSize.value,
}
const kw = keyword.value.trim()
if (kw) params.keyword = kw
if (feeTypeFilter.value && feeTypeFilter.value !== 'all') {
params.feeType = feeTypeFilter.value
}
const costItemValue = costItem.value.trim()
if (costItemValue) params.costItem = costItemValue
if (chargingStageFilter.value && chargingStageFilter.value !== 'all') {
params.chargingStage = chargingStageFilter.value
}
if (customerIdFilter.value != null && !Number.isNaN(customerIdFilter.value)) {
params.customerId = customerIdFilter.value
}
if (dateRange.value?.length === 2 && dateRange.value[0] && dateRange.value[1]) {
params.startTime = dayjs(dateRange.value[0]).format('YYYY-MM-DD HH:mm:ss')
params.endTime = dayjs(dateRange.value[1]).format('YYYY-MM-DD HH:mm:ss')
}
return params
}
async function loadList() {
listLoading.value = true
try {
const { items, total } = await getReceivablePayableList(buildListParams() as any)
totalCount.value = total
tableRows.value = items || []
} catch (e: any) {
Message.error(e?.message || t('finance.receivablePayable.message.loadError'))
tableRows.value = []
totalCount.value = 0
} finally {
listLoading.value = false
}
}
function handleSearch() {
currentPage.value = 1
loadList()
}
function handleReset() {
keyword.value = ''
feeTypeFilter.value = 'all'
costItem.value = ''
chargingStageFilter.value = 'all'
customerIdFilter.value = undefined
dateRange.value = []
currentPage.value = 1
loadList()
}
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
onMounted(() => {
loadList()
})
</script>
<style lang="less" scoped>
.receivable-payable-page {
padding: 16px;
background: #f0f3f4;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.receivable-payable-page-card {
background: #fff;
border-radius: 4px;
padding: 16px;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
flex-shrink: 0;
}
.list-spin {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.list-wrap {
flex: 1;
min-height: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
</style>

View File

@ -142,7 +142,7 @@ import {
} from '@/api/recharge/rechargeRecord'
defineOptions({
name: 'VipRechargeRechargeRecord',
name: 'FinanceRechargeRecord',
})
const { t } = useI18n()

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { ref, computed, onMounted, nextTick } from 'vue';
import { IconSettings } from '@arco-design/web-vue/es/icon';
import { IconDown } from '@arco-design/web-vue/es/icon';
import { Message, Modal } from '@arco-design/web-vue';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
@ -183,7 +183,31 @@
const selectedRowCount = ref(0);
/** 表格选中行(用于批量操作) */
const selectedOrderRows = ref<any[]>([]);
const actionSubmitting = ref(false);
/** 当前正在执行的 toolbar 操作(仅对应按钮显示 loading,避免全部按钮联动) */
const actionLoadingKey = ref<string | null>(null);
function isActionLoading(key: string) {
return actionLoadingKey.value === key;
}
function isActionBusy(exceptKey?: string) {
const current = actionLoadingKey.value;
return current != null && current !== exceptKey;
}
async function withOrderActionLoading<T>(key: string, fn: () => Promise<T>): Promise<T> {
if (actionLoadingKey.value) {
return Promise.reject(new Error('busy'));
}
actionLoadingKey.value = key;
try {
return await fn();
} finally {
if (actionLoadingKey.value === key) {
actionLoadingKey.value = null;
}
}
}
const printConnectionState = useHiprintClientState();
const singlePrintVisible = ref(false);
const batchPrintVisible = ref(false);
@ -217,9 +241,8 @@
const handleCreateTrackOrder = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
actionSubmitting.value = true;
const resultBlocks: string[] = [];
try {
await withOrderActionLoading('track', async () => {
const resultBlocks: string[] = [];
for (const row of rows) {
const id = getOrderRowId(row)!;
const waybillNo = row.waybillNo || row.serviceNo || String(id);
@ -232,9 +255,7 @@
}
trackUpsertResultText.value = resultBlocks.join('\n\n');
trackUpsertModalVisible.value = true;
} finally {
actionSubmitting.value = false;
}
}).catch(() => undefined);
};
function formatDownstreamForwardResult(
@ -263,9 +284,8 @@
const handleForwardDownstreamCreate = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
actionSubmitting.value = true;
const resultBlocks: string[] = [];
try {
await withOrderActionLoading('downstream', async () => {
const resultBlocks: string[] = [];
for (const row of rows) {
const id = getOrderRowId(row)!;
const waybillNo = row.waybillNo || row.serviceNo || String(id);
@ -278,9 +298,7 @@
}
downstreamForwardResultText.value = resultBlocks.join('\n\n');
downstreamForwardModalVisible.value = true;
} finally {
actionSubmitting.value = false;
}
}).catch(() => undefined);
};
function getOrderRowId(row: any): number | string | undefined {
@ -350,69 +368,73 @@
const handlePay = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
actionSubmitting.value = true;
try {
for (const row of rows) {
await postOrderPay({ id: getOrderRowId(row)! });
}
Message.success('支付请求已提交');
notifyCustomerBalanceChanged();
await refreshListAndTabCounts();
await withOrderActionLoading('pay', async () => {
for (const row of rows) {
await postOrderPay({ id: getOrderRowId(row)! });
}
Message.success('支付请求已提交');
notifyCustomerBalanceChanged();
await refreshListAndTabCounts();
});
} catch (e: any) {
Message.error(e?.message || '操作失败');
} finally {
actionSubmitting.value = false;
if (e?.message !== 'busy') {
Message.error(e?.message || '操作失败');
}
}
};
const handlePreDeclare = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
actionSubmitting.value = true;
try {
for (const row of rows) {
await postOrderPreDeclare({ id: getOrderRowId(row)! });
}
Message.success('预报已提交');
await refreshListAndTabCounts();
await withOrderActionLoading('predeclare', async () => {
for (const row of rows) {
await postOrderPreDeclare({ id: getOrderRowId(row)! });
}
Message.success('预报已提交');
await refreshListAndTabCounts();
});
} catch (e: any) {
Message.error(e?.message || '操作失败');
} finally {
actionSubmitting.value = false;
if (e?.message !== 'busy') {
Message.error(e?.message || '操作失败');
}
}
};
const handleIntercept = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
actionSubmitting.value = true;
try {
for (const row of rows) {
await postOrderIntercept({ id: getOrderRowId(row)! });
}
Message.success('拦截已提交');
await refreshListAndTabCounts();
await withOrderActionLoading('intercept', async () => {
for (const row of rows) {
await postOrderIntercept({ id: getOrderRowId(row)! });
}
Message.success('拦截已提交');
await refreshListAndTabCounts();
});
} catch (e: any) {
Message.error(e?.message || '操作失败');
} finally {
actionSubmitting.value = false;
if (e?.message !== 'busy') {
Message.error(e?.message || '操作失败');
}
}
};
const handleCancelIntercept = async () => {
const rows = requireSelectedRows();
if (!rows.length) return;
actionSubmitting.value = true;
try {
for (const row of rows) {
await postOrderCancelIntercept({ id: getOrderRowId(row)! });
}
Message.success('已取消拦截');
await refreshListAndTabCounts();
await withOrderActionLoading('cancelIntercept', async () => {
for (const row of rows) {
await postOrderCancelIntercept({ id: getOrderRowId(row)! });
}
Message.success('已取消拦截');
await refreshListAndTabCounts();
});
} catch (e: any) {
Message.error(e?.message || '操作失败');
} finally {
actionSubmitting.value = false;
if (e?.message !== 'busy') {
Message.error(e?.message || '操作失败');
}
}
};
@ -428,18 +450,19 @@
title: '确认删除',
content: `确定删除选中的 ${rows.length} 条订单吗?`,
onOk: async () => {
actionSubmitting.value = true;
try {
for (const row of rows) {
await postOrderDelete({ id: getOrderRowId(row)! });
}
Message.success('已删除');
await refreshListAndTabCounts();
await withOrderActionLoading('delete', async () => {
for (const row of rows) {
await postOrderDelete({ id: getOrderRowId(row)! });
}
Message.success('已删除');
await refreshListAndTabCounts();
});
} catch (e: any) {
Message.error(e?.message || '删除失败');
if (e?.message !== 'busy') {
Message.error(e?.message || '删除失败');
}
throw e;
} finally {
actionSubmitting.value = false;
}
},
});
@ -471,21 +494,22 @@
Message.warning('请先在顶部打开并连接打印开关');
return;
}
actionSubmitting.value = true;
try {
const row = rows[0];
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
if (!isUsablePrintUrl(printUrl)) {
Message.info('未获取到打印文件,已打开打印弹窗供预览布局');
await withOrderActionLoading('singlePrint', async () => {
const row = rows[0];
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
if (!isUsablePrintUrl(printUrl)) {
Message.info('未获取到打印文件,已打开打印弹窗供预览布局');
singlePrintVisible.value = true;
return;
}
singlePrintItems.value = [buildPrintItem(row, printUrl)];
singlePrintVisible.value = true;
return;
}
singlePrintItems.value = [buildPrintItem(row, printUrl)];
singlePrintVisible.value = true;
});
} catch (e: any) {
Message.error(e?.message || '打印失败');
} finally {
actionSubmitting.value = false;
if (e?.message !== 'busy') {
Message.error(e?.message || '打印失败');
}
}
};
@ -498,45 +522,47 @@
Message.warning('请先在顶部打开并连接打印开关');
return;
}
actionSubmitting.value = true;
try {
const items: Array<{ id: number | string; label: string; url: string }> = [];
for (const row of rows) {
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
if (isUsablePrintUrl(printUrl)) {
items.push(buildPrintItem(row, printUrl));
await withOrderActionLoading('batchPrint', async () => {
const items: Array<{ id: number | string; label: string; url: string }> = [];
for (const row of rows) {
const { printUrl } = await postOrderPrint({ id: getOrderRowId(row)! });
if (isUsablePrintUrl(printUrl)) {
items.push(buildPrintItem(row, printUrl));
}
}
}
if (!items.length) {
Message.info('未获取到可打印文件,已打开打印弹窗供预览布局');
if (!items.length) {
Message.info('未获取到可打印文件,已打开打印弹窗供预览布局');
batchPrintVisible.value = true;
return;
}
batchPrintItems.value = items;
batchPrintVisible.value = true;
return;
}
batchPrintItems.value = items;
batchPrintVisible.value = true;
});
} catch (e: any) {
Message.error(e?.message || '批量打印失败');
} finally {
actionSubmitting.value = false;
if (e?.message !== 'busy') {
Message.error(e?.message || '批量打印失败');
}
}
};
const handleExport = async () => {
actionSubmitting.value = true;
try {
const status = activeTab.value === ORDER_STATUS_ALL ? ORDER_STATUS_ALL : activeTab.value;
const { downloadUrl } = await postOrderExport({
status,
keyword: '',
createTimeStart: '',
createTimeEnd: '',
await withOrderActionLoading('export', async () => {
const status = activeTab.value === ORDER_STATUS_ALL ? ORDER_STATUS_ALL : activeTab.value;
const { downloadUrl } = await postOrderExport({
status,
keyword: '',
createTimeStart: '',
createTimeEnd: '',
});
if (downloadUrl) window.open(downloadUrl, '_blank');
Message.success(downloadUrl ? '导出链接已打开' : '导出已提交');
});
if (downloadUrl) window.open(downloadUrl, '_blank');
Message.success(downloadUrl ? '导出链接已打开' : '导出已提交');
} catch (e: any) {
Message.error(e?.message || '导出失败');
} finally {
actionSubmitting.value = false;
if (e?.message !== 'busy') {
Message.error(e?.message || '导出失败');
}
}
};
@ -675,15 +701,46 @@
</div>
<div class="actions-right">
<a-button class="btn-pay" :loading="actionSubmitting" @click="handlePay">{{ t('order.myOrders.actions.pay') }}</a-button>
<a-button class="btn-predeclare" :loading="actionSubmitting" @click="handlePreDeclare">{{ t('order.myOrders.actions.predeclare') }}</a-button>
<a-button class="btn-track" :loading="actionSubmitting" @click="handleCreateTrackOrder">{{ t('order.myOrders.actions.createTrackOrder') }}</a-button>
<a-button class="btn-downstream" :loading="actionSubmitting" @click="handleForwardDownstreamCreate">{{ t('order.myOrders.actions.forwardDownstreamCreate') }}</a-button>
<a-button class="btn-print" :loading="actionSubmitting" @click="handleSinglePrint">单个打印</a-button>
<a-button class="btn-print" :loading="actionSubmitting" @click="handleBatchPrint">批量打印</a-button>
<a-dropdown trigger="click" position="bl">
<a-button class="btn-settings">
<icon-settings />
<a-button
class="btn-pay"
:loading="isActionLoading('pay')"
:disabled="isActionBusy('pay')"
@click="handlePay"
>{{ t('order.myOrders.actions.pay') }}</a-button>
<a-button
type="primary"
:loading="isActionLoading('predeclare')"
:disabled="isActionBusy('predeclare')"
@click="handlePreDeclare"
>{{ t('order.myOrders.actions.predeclare') }}</a-button>
<a-button
class="btn-track"
:loading="isActionLoading('track')"
:disabled="isActionBusy('track')"
@click="handleCreateTrackOrder"
>{{ t('order.myOrders.actions.createTrackOrder') }}</a-button>
<a-button
class="btn-downstream"
:loading="isActionLoading('downstream')"
:disabled="isActionBusy('downstream')"
@click="handleForwardDownstreamCreate"
>{{ t('order.myOrders.actions.forwardDownstreamCreate') }}</a-button>
<a-button
type="primary"
:loading="isActionLoading('singlePrint')"
:disabled="isActionBusy('singlePrint')"
@click="handleSinglePrint"
>单个打印</a-button>
<a-button
type="primary"
:loading="isActionLoading('batchPrint')"
:disabled="isActionBusy('batchPrint')"
@click="handleBatchPrint"
>批量打印</a-button>
<a-dropdown trigger="click" position="bl" :disabled="!!actionLoadingKey">
<a-button type="text" class="btn-more" :disabled="!!actionLoadingKey">
{{ t('order.myOrders.actions.more') }}
<icon-down class="btn-more-arrow" />
</a-button>
<template #content>
<a-doption @click="onMoreActionClick('intercept')">{{ t('order.myOrders.actions.intercept') }}</a-doption>
@ -850,204 +907,78 @@
.actions-right {
display: flex;
flex-wrap: wrap;
gap: var(--app-button-gap, 8px);
align-items: center;
padding: 16px 0;
.arco-btn {
padding: 12px 0;
flex-shrink: 0;
:deep(.arco-btn) {
height: 32px;
padding: 0 12px;
border-radius: 4px;
font-weight: 500;
transition: all 0.3s ease;
border: 2px solid;
position: relative;
overflow: hidden;
padding: 8px 16px;
height: auto;
// 支付按钮 - 淡黄色
&.btn-pay {
background: #fffbe6;
border-color: #ffd666;
font-size: 14px;
font-weight: 400;
line-height: 30px;
}
:deep(.arco-btn.btn-pay) {
color: #d46b08;
background: #fffbe6;
border-color: #ffd666;
&:hover:not(:disabled) {
color: #d46b08;
&:hover:not(:disabled) {
background: #fff7d1;
border-color: #ffc53d;
}
&:active:not(:disabled) {
background: #fff1b8;
border-color: #ffb020;
}
background: #fff7d1;
border-color: #ffc53d;
}
// 预报按钮 - 主题色
&.btn-predeclare {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
:deep(.arco-btn.btn-track),
:deep(.arco-btn.btn-downstream) {
color: #fff;
background: #000;
border-color: #000;
&:hover:not(:disabled) {
color: #fff;
background: #262626;
border-color: #262626;
}
&.btn-track {
background: #fff;
border-color: var(--app-primary-color);
&:active:not(:disabled) {
color: #fff;
background: #141414;
border-color: #141414;
}
}
:deep(.arco-btn.btn-more) {
height: 32px;
padding: 0 4px;
color: #4e5969;
background: transparent !important;
border: none !important;
box-shadow: none !important;
display: inline-flex;
align-items: center;
gap: 2px;
.btn-more-arrow {
font-size: 12px;
margin-left: 2px;
transition: transform 0.2s ease;
}
&:hover:not(:disabled) {
color: var(--app-primary-color);
&:hover:not(:disabled) {
background: rgba(var(--primary-6), 0.06);
}
background: transparent !important;
border: none !important;
}
&.btn-downstream {
background: #f6ffed;
border-color: #95de64;
color: #389e0d;
&:hover:not(:disabled) {
background: #d9f7be;
}
}
// 拦截按钮 - 主题色
&.btn-intercept {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
// 取消拦截按钮 - 主题色
&.btn-cancel-intercept {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
// 删除按钮 - 主题色
&.btn-delete {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
// 打印按钮 - 主题色
&.btn-print {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
// 导出按钮 - 主题色
&.btn-export {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
// 修改订单按钮 - 主题色
&.btn-modify {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
// 设置按钮 - 主题色
&.btn-settings {
background: var(--app-primary-color);
border-color: var(--app-primary-color);
color: #ffffff;
padding: 8px 12px;
min-width: 36px;
&:hover:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
&:active:not(:disabled) {
background: var(--app-primary-color-hover);
border-color: var(--app-primary-color-hover);
}
}
// 禁用状态
&:disabled {
opacity: 1;
cursor: not-allowed;
background: rgba(169, 169, 169, 0.8) !important;
border-color: rgba(169, 169, 169, 0.8) !important;
color: #ffffff !important;
transform: none !important;
box-shadow: none !important;
&:active:not(:disabled) {
color: var(--app-primary-color-hover);
background: transparent !important;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -108,8 +108,6 @@
allow-clear
style="width: 220px"
/>
<a-button size="small" @click="expandAllPermissions">{{ t('system.role.permission.expandAll') }}</a-button>
<a-button size="small" @click="collapseAllPermissions">{{ t('system.role.permission.collapseAll') }}</a-button>
<a-button size="small" @click="checkAllPermissions">{{ t('system.role.permission.checkAll') }}</a-button>
<a-button size="small" @click="uncheckAllPermissions">{{ t('system.role.permission.uncheckAll') }}</a-button>
</a-space>
@ -117,16 +115,66 @@
{{ t('system.role.permission.selectedCount', { selected: selectedPermissions.length, total: totalPermissionCount }) }}
</span>
</div>
<div class="section-card permission-tree-card">
<a-tree
v-if="filteredPermissionTreeData.length > 0"
v-model:checked-keys="selectedPermissions"
v-model:expanded-keys="permissionExpandedKeys"
checkable
:data="filteredPermissionTreeData"
:field-names="{ title: 'name', key: 'code', children: 'children' }"
check-strictly
/>
<div class="section-card permission-list-card">
<a-table
v-if="filteredPermissionRows.length > 0"
:data="filteredPermissionRows"
:pagination="false"
:bordered="{ cell: true }"
:span-method="permissionSpanMethod"
row-key="rowKey"
size="small"
:scroll="{ y: permissionTableScrollY }"
>
<template #columns>
<a-table-column
:title="t('system.role.permission.col.level1')"
data-index="level1Name"
:width="180"
>
<template #cell="{ record }">
<a-checkbox
:model-value="isPermissionChecked(record.level1Code)"
@change="(checked) => togglePermission(record.level1Code, checked)"
>
{{ record.level1Name }}
</a-checkbox>
</template>
</a-table-column>
<a-table-column
:title="t('system.role.permission.col.level2')"
data-index="level2Name"
:width="200"
>
<template #cell="{ record }">
<a-checkbox
:model-value="isPermissionChecked(record.level2Code)"
@change="(checked) => togglePermission(record.level2Code, checked)"
>
{{ record.level2Name }}
</a-checkbox>
</template>
</a-table-column>
<a-table-column :title="t('system.role.permission.col.level3')">
<template #cell="{ record }">
<a-checkbox-group
v-if="record.level2HasChildren"
v-model="selectedPermissions"
class="permission-button-group"
>
<a-checkbox
v-for="btn in record.buttons"
:key="btn.code"
:value="btn.code"
>
{{ btn.name }}
</a-checkbox>
</a-checkbox-group>
<span v-else class="permission-no-buttons">—</span>
</template>
</a-table-column>
</template>
</a-table>
<div v-else class="permission-empty">
{{ permissionKeyword ? t('system.role.permission.noMatch') : t('system.role.permission.loading') }}
</div>
@ -151,12 +199,17 @@ import type {
RoleCreateDto,
RoleUpdateDto,
PermissionGroupDto,
PermissionDefinitionDto,
} from '@/api/rbac'
import { useWarehouseList } from '@/composables/useWarehouseList'
import { hasPermission } from '@/utils/rbac-permission'
import { handleApiError } from '@/utils/rbac-error-handler'
import { buildFlatLeftMenuTree, findLeftMenuItemByKey } from '@/utils/rbacLeftMenuTree'
import {
buildPermissionAssignRows,
collectPermissionCodesFromRows,
computeLevel1RowSpans,
filterPermissionAssignRows,
} from '@/utils/rbacPermissionAssignRows'
import dayjs from 'dayjs'
const { t } = useI18n()
@ -192,7 +245,7 @@ const permissionLoading = ref(false)
const permissionSaving = ref(false)
const permissionTree = ref<PermissionGroupDto[]>([])
const selectedPermissions = ref<string[]>([])
const permissionExpandedKeys = ref<string[]>([])
const permissionTableScrollY = 'calc(100vh - 340px)'
const internalRoles = computed(() =>
dataSource.value.filter((role) => {
@ -212,24 +265,16 @@ const selectedKeys = computed(() =>
selectedRole.value?.id ? [`item-${selectedRole.value.id}`] : []
)
const permissionTreeData = computed(() => {
const treeData: PermissionDefinitionDto[] = []
permissionTree.value.forEach((group: PermissionGroupDto) => {
if (group.permissions?.length) {
treeData.push({
code: group.code || '',
name: group.name || '',
children: group.permissions || [],
})
}
})
return treeData
})
const permissionRows = computed(() => buildPermissionAssignRows(permissionTree.value))
const totalPermissionCount = computed(() => countLeafCodes(permissionTreeData.value))
const filteredPermissionRows = computed(() =>
filterPermissionAssignRows(permissionRows.value, permissionKeyword.value)
)
const filteredPermissionTreeData = computed(() =>
filterPermissionTree(permissionTreeData.value, permissionKeyword.value.trim())
const level1RowSpans = computed(() => computeLevel1RowSpans(filteredPermissionRows.value))
const totalPermissionCount = computed(() =>
collectPermissionCodesFromRows(permissionRows.value).length
)
const roleTags = computed((): RbacDetailTag[] => {
@ -261,38 +306,30 @@ const roleTabs = computed(() => {
return tabs
})
function countLeafCodes(nodes: PermissionDefinitionDto[]): number {
let count = 0
nodes.forEach((node) => {
if (node.children?.length) {
count += countLeafCodes(node.children)
} else if (node.code) {
count += 1
}
})
return count
function permissionSpanMethod({
rowIndex,
columnIndex,
}: {
rowIndex: number
columnIndex: number
}) {
if (columnIndex !== 0) return { rowspan: 1, colspan: 1 }
const rowspan = level1RowSpans.value[rowIndex] ?? 1
if (rowspan === 0) return { rowspan: 0, colspan: 0 }
return { rowspan, colspan: 1 }
}
function filterPermissionTree(
nodes: PermissionDefinitionDto[],
keyword: string
): PermissionDefinitionDto[] {
if (!keyword) return nodes
const lower = keyword.toLowerCase()
const result: PermissionDefinitionDto[] = []
nodes.forEach((node) => {
const children = node.children?.length ? filterPermissionTree(node.children, keyword) : []
const selfMatch =
(node.name || '').toLowerCase().includes(lower) ||
(node.code || '').toLowerCase().includes(lower)
if (selfMatch || children.length) {
result.push({
...node,
children: children.length ? children : selfMatch ? node.children : undefined,
})
}
})
return result
function isPermissionChecked(code: string) {
return selectedPermissions.value.includes(code)
}
function togglePermission(code: string, checked: boolean | (string | number | boolean)) {
if (!code) return
const enabled = checked === true
const next = new Set(selectedPermissions.value)
if (enabled) next.add(code)
else next.delete(code)
selectedPermissions.value = Array.from(next)
}
function formatTime(value?: string | null) {
@ -365,7 +402,6 @@ async function loadRolePermissions(roleId: string) {
try {
await loadPermissionTree()
selectedPermissions.value = (await RoleService.getRbacRoleGetRolePermissions(roleId)) || []
permissionExpandedKeys.value = expandAllCodes(permissionTreeData.value)
} catch (error) {
handleApiError(error)
} finally {
@ -373,13 +409,12 @@ async function loadRolePermissions(roleId: string) {
}
}
function expandAllCodes(nodes: PermissionDefinitionDto[]): string[] {
const keys: string[] = []
nodes.forEach((node) => {
if (node.code) keys.push(node.code)
if (node.children?.length) keys.push(...expandAllCodes(node.children))
})
return keys
function checkAllPermissions() {
selectedPermissions.value = collectPermissionCodesFromRows(permissionRows.value)
}
function uncheckAllPermissions() {
selectedPermissions.value = []
}
function handleSearch() {
@ -466,22 +501,6 @@ async function handleSavePermissions() {
}
}
function expandAllPermissions() {
permissionExpandedKeys.value = expandAllCodes(filteredPermissionTreeData.value)
}
function collapseAllPermissions() {
permissionExpandedKeys.value = []
}
function checkAllPermissions() {
selectedPermissions.value = expandAllCodes(permissionTreeData.value)
}
function uncheckAllPermissions() {
selectedPermissions.value = []
}
watch(
() => selectedRole.value?.id,
async (roleId) => {
@ -494,12 +513,6 @@ watch(
}
)
watch(permissionKeyword, (kw) => {
if (kw.trim() && activeTab.value === 'permissions') {
permissionExpandedKeys.value = expandAllCodes(filteredPermissionTreeData.value)
}
})
onMounted(async () => {
await loadData()
await loadPermissionTree()
@ -519,10 +532,31 @@ onMounted(async () => {
gap: 0;
}
.permission-tree-card {
.permission-list-card {
min-height: 320px;
max-height: calc(100vh - 340px);
overflow: auto;
}
.permission-button-group {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
padding: 4px 0;
}
.permission-no-buttons {
color: var(--color-text-4);
}
.permission-list-card :deep(.arco-checkbox) {
align-items: flex-start;
}
.permission-list-card :deep(.arco-table-th) {
background: #fafafa;
}
.permission-list-card :deep(.arco-table-td) {
vertical-align: top;
}
.perm-summary {

View File

@ -264,7 +264,7 @@ function goRecharge() {
}
function goRechargeRecord() {
router.push({ name: 'VipRechargeRechargeRecord' })
router.push({ name: 'FinanceRechargeRecord' })
}
function goMyOrders() {

View File

@ -280,7 +280,7 @@ const reset = () => {
};
const goToRecord = () => {
router.push({ name: 'VipRechargeRechargeRecord' });
router.push({ name: 'FinanceRechargeRecord' });
};
</script>

View File

@ -100,6 +100,26 @@ export default defineConfig(({ mode }) => {
target: 'http://api.nanafa.com/admin',
changeOrigin: true,
secure: false,
bypass(req) {
const path = req.url?.split('?')[0] ?? '';
const systemSpaExact = ['/system/dict', '/system/warehouseAddress', '/system/announcement'];
if (systemSpaExact.some((p) => path === p || path === `${p}/`)) {
return '/index.html';
}
const systemSpaPrefixes = [
'/system/users',
'/system/roles',
'/system/menus',
'/system/organizations',
'/system/batchEmail',
'/system/otherConfig',
'/system/xhsAiAssistant',
'/system/dataPerm',
];
if (systemSpaPrefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
return '/index.html';
}
},
},
// 小红书 OMS-Xhs 接口(开发默认走本机 Admin API,与桌面 Agent 一致)
'/xhs': {
@ -265,9 +285,14 @@ export default defineConfig(({ mode }) => {
'/system/organizations',
'/system/batchEmail',
'/system/xhsAiAssistant',
'/system/otherConfig',
'/system/dataPerm',
// User 相关前端路由
'/user/recharge',
'/user/rechargeRecord',
// Finance 相关前端路由
'/finance/rechargeRecord',
'/finance/accountFlow',
'/finance/receivablePayable',
];
// 直接在 configureServer 中设置中间件,确保在代理之前执行