feat: 新增财务模块并优化组织管理、角色权限与我的订单
- 新增财务菜单与应收应付、充值记录、账户流水页面及路由 - 组织管理改为左右分栏,优化左侧树层级展示与选中交互 - 角色权限分配改为列表形式,支持各级菜单独立勾选 - 我的订单工具栏按钮样式与独立 loading 交互优化 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5cd6a5462d
commit
2063621daf
1006
docs/微信登录整合方案.md
1006
docs/微信登录整合方案.md
File diff suppressed because it is too large
Load Diff
1048
docs/数据权限设计.md
Normal file
1048
docs/数据权限设计.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
},
|
||||
|
||||
// 一级菜单 - 系统
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
57
src/api/finance/receivablePayable.ts
Normal file
57
src/api/finance/receivablePayable.ts
Normal 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)
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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': '上一页',
|
||||
|
||||
48
src/router/routes/newModules/finance.ts
Normal file
48
src/router/routes/newModules/finance.ts
Normal 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;
|
||||
@ -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: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
98
src/utils/orgLeftMenuTree.ts
Normal file
98
src/utils/orgLeftMenuTree.ts
Normal 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
|
||||
}
|
||||
113
src/utils/rbacPermissionAssignRows.ts
Normal file
113
src/utils/rbacPermissionAssignRows.ts
Normal 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)
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -91,7 +91,7 @@ import dayjs from 'dayjs'
|
||||
import { getAccountFlowList } from '@/api/customer/accountFlow'
|
||||
|
||||
defineOptions({
|
||||
name: 'UserAccountFlow',
|
||||
name: 'FinanceAccountFlow',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
299
src/views/finance/receivablePayable/index.vue
Normal file
299
src/views/finance/receivablePayable/index.vue
Normal 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>
|
||||
@ -142,7 +142,7 @@ import {
|
||||
} from '@/api/recharge/rechargeRecord'
|
||||
|
||||
defineOptions({
|
||||
name: 'VipRechargeRechargeRecord',
|
||||
name: 'FinanceRechargeRecord',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
@ -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
@ -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 {
|
||||
|
||||
@ -264,7 +264,7 @@ function goRecharge() {
|
||||
}
|
||||
|
||||
function goRechargeRecord() {
|
||||
router.push({ name: 'VipRechargeRechargeRecord' })
|
||||
router.push({ name: 'FinanceRechargeRecord' })
|
||||
}
|
||||
|
||||
function goMyOrders() {
|
||||
|
||||
@ -280,7 +280,7 @@ const reset = () => {
|
||||
};
|
||||
|
||||
const goToRecord = () => {
|
||||
router.push({ name: 'VipRechargeRechargeRecord' });
|
||||
router.push({ name: 'FinanceRechargeRecord' });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -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 中设置中间件,确保在代理之前执行
|
||||
|
||||
Loading…
Reference in New Issue
Block a user