feat embed wallets and add withdrawal records

This commit is contained in:
david
2026-07-29 15:29:49 +08:00
parent 0c94c8033a
commit a7d318a013
7 changed files with 102 additions and 32 deletions

View File

@@ -13,6 +13,10 @@ const frontendDefinitions = [
...source.matchAll(/define\('([^']+)',\s*'[^']+',\s*'([^']+)'/g),
].map((match) => ({ name: match[1], mode: match[2] }));
const frontendNames = frontendDefinitions.map((item) => item.name);
const embeddedResources = new Set([
'wallet_basic', 'wallet_bank', 'wallet_payment', 'wallet_record',
'wallet_refund', 'wallet_apply_cash',
]);
for (const name of frontendNames) {
const item = backend.get(name);
@@ -29,7 +33,7 @@ for (const item of contract.resources) {
throw new Error(
`资源模式不一致:${item.name},后端=${item.mode},前端=${frontend?.mode}`,
);
if (!routes.includes(`'/${item.name}'`))
if (!embeddedResources.has(item.name) && !routes.includes(`'/${item.name}'`))
throw new Error(`缺少后端资源路由:${item.name}`);
}

View File

@@ -53,6 +53,7 @@ export type ResourceUiDefinition = {
relationKey: string;
title: string;
};
walletOwnerType?: 'gas' | 'delivery' | 'staff' | 'user';
};
const fieldLabels: Record<string, string> = {
@@ -136,6 +137,8 @@ const fieldLabels: Record<string, string> = {
channel: '渠道',
review_reason: '审核原因',
reviewed_at: '审核时间',
reviewer_name: '审核人',
reviewer_identity: '审核人标识',
ymd: '日期',
ym: '月份',
contract_no: '合同编号',
@@ -281,13 +284,13 @@ function define(
const reason = [f('reason', { required: true })];
export const resources: ResourceUiDefinition[] = [
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' } },
{ ...define('gas_basic', '气站管理', 'writable', [f('code', { required: true }), f('name', { required: true }), f('credit_code'), f('principal'), f('address'), f('longitude'), f('latitude')]), accountManagement: { resource: '/gas_account', relationKey: 'gas_basic_identity', title: '气站账户' }, walletOwnerType: 'gas' },
define('gas_account', '气站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('gas_basic_identity', '/gas_basic', true)]),
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' } },
{ ...define('delivery_basic', '配送点管理', 'writable', [f('delivery_code', { required: true }), f('name', { required: true }), relation('gas_basic_identity', '/gas_basic'), f('principal'), f('address')]), accountManagement: { resource: '/delivery_account', relationKey: 'delivery_basic_identity', title: '配送点账户' }, walletOwnerType: 'delivery' },
define('delivery_account', '配送站账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('display_name'), f('role_code'), relation('delivery_basic_identity', '/delivery_basic', true)]),
define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]),
{ ...define('staff_account', '工作人员', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('role_code', { required: true, type: 'select', options: [{ label: '安装人员', value: 'installer' }, { label: '配送人员', value: 'delivery' }, { label: '运维人员', value: 'operations' }] }), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), f('work_status', { type: 'select', options: [{ label: '在岗', value: 'on_duty' }, { label: '离岗', value: 'off_duty' }] })]), walletOwnerType: 'staff' },
define('staff_credential', '工作人员资质', 'writable', [relation('staff_account_identity', '/staff_account', true), f('credential_type', { required: true }), f('credential_no'), f('expired_at')]),
define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]),
{ ...define('user_account', '用户账户', 'writable', [f('username', { required: true }), f('password', { required: true }), f('name', { required: true }), f('phone'), f('avatar'), f('real_name')]), walletOwnerType: 'user' },
define('user_address', '用户地址', 'writable', [relation('user_account_identity', '/user_account', true), f('address', { required: true }), f('longitude'), f('latitude'), f('is_default')]),
define('user_service_relation', '用户服务关系', 'writable', [relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic'), relation('delivery_basic_identity', '/delivery_basic'), relation('staff_account_identity', '/staff_account')]),
@@ -344,7 +347,24 @@ export const resources: ResourceUiDefinition[] = [
define('wallet_payment', '钱包支付记录', 'readonly', []),
define('wallet_record', '钱包流水', 'readonly', []),
define('wallet_refund', '退款记录', 'readonly', []),
define('wallet_apply_cash', '提现申请', 'readonly', [], 'list', [
define('wallet_apply_cash', '提现记录', 'readonly', [
f('cash_no'),
relation('wallet_basic_identity', '/wallet_basic'),
f('amount'),
f('apply_status', { type: 'select', options: [
{ label: '待处理', value: 10 },
{ label: '已通过', value: 25 },
{ label: '已驳回', value: 26 },
{ label: '已完成', value: 23 },
] }),
f('reviewer_name'),
f('review_reason'),
f('reviewed_at'),
f('completed_at'),
f('channel'),
f('trade_no'),
f('remark'),
], 'list', [
{ name: '审核通过', resource: '/wallet_apply_cash/:identity/approve', fields: reason },
{ name: '审核驳回', resource: '/wallet_apply_cash/:identity/reject', danger: true, fields: reason },
]),

View File

@@ -108,15 +108,8 @@ const routes: AppRouteRecordRaw[] = [
child('ec', 'order-items', 'order-items', '订单明细', '/ec_order_item', 'ec_order', true, 'ec-orders'),
child('ec', 'reviews', 'reviews', '商品评价', '/ec_review', 'ec_review'),
]),
group('wallet', 'wallet', '钱包管理', 'icon-safe', 80, [
child('wallet', 'wallet-basic', 'basic', '钱包', '/wallet_basic', 'wallet_basic'),
child('wallet', 'banks', 'banks', '银行卡', '/wallet_bank', 'wallet_bank'),
child('wallet', 'payments', 'payments', '支付记录', '/wallet_payment', 'wallet_payment'),
child('wallet', 'records', 'records', '钱包流水', '/wallet_record', 'wallet_record'),
child('wallet', 'refunds', 'refunds', '退款记录', '/wallet_refund', 'wallet_refund'),
child('wallet', 'apply-cash', 'apply-cash', '提现申请', '/wallet_apply_cash', 'wallet_apply_cash'),
]),
group('finance', 'finance', '财务管理', 'icon-bar-chart', 90, [
child('finance', 'withdrawals', 'withdrawals', '提现记录', '/wallet_apply_cash', 'wallet_apply_cash'),
child('finance', 'payments', 'payments', '支付记录', '/fin_payment', 'fin_payment'),
child('finance', 'settlements', 'settlements', '财务结算', '/fin_settlement', 'fin_settlement'),
child('finance', 'reconciliations', 'reconciliations', '财务对账', '/fin_reconciliation', 'fin_reconciliation'),

View File

@@ -45,6 +45,14 @@
</a-tag>
</template>
</a-table-column>
<a-table-column v-if="definition.walletOwnerType" title="钱包" :width="150">
<template #cell="{ record }">
<span v-if="walletByOwner[String(record.identity)]">
¥{{ (Number(walletByOwner[String(record.identity)].balance ?? 0) / 100).toFixed(2) }}
</span>
<span v-else class="muted-text">未开通</span>
</template>
</a-table-column>
<a-table-column title="操作" :width="definition.accountManagement ? 350 : 280" fixed="right">
<template #cell="{ record }">
<a-space>
@@ -161,6 +169,7 @@ const pageSize = 20;
const total = ref(0);
const list = ref<Row[]>([]);
const accountCounts = ref<Record<string, number>>({});
const walletByOwner = ref<Record<string, Row>>({});
const filters = reactive({
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
});
@@ -236,15 +245,25 @@ const displayFields = computed(() =>
.filter((field) => field.key !== 'password')
.slice(0, 6),
);
const detailEntries = computed(() =>
Object.entries(detail.value).filter(
const detailEntries = computed(() => {
const entries = Object.entries(detail.value).filter(
([key, value]) =>
key !== 'id' &&
!key.endsWith('_id') &&
!Array.isArray(value) &&
!(value && typeof value === 'object' && key !== 'order' && key !== 'contract'),
),
);
);
if (props.definition.walletOwnerType) {
const wallet = walletByOwner.value[String(detail.value.identity ?? '')];
entries.push(['wallet', wallet ? {
identity: wallet.identity,
balance: `¥${(Number(wallet.balance ?? 0) / 100).toFixed(2)}`,
withdrawal_balance: `¥${(Number(wallet.withdrawal_balance ?? 0) / 100).toFixed(2)}`,
status: wallet.status,
} : '未开通']);
}
return entries;
});
const detailCollections = computed(() =>
Object.entries(detail.value)
.filter(([, value]) => Array.isArray(value) && value.length > 0)
@@ -323,6 +342,7 @@ async function load() {
total.value = result.total;
}
await loadAccountCounts();
await loadWallets();
} catch (error) {
Message.error((error as Error).message);
} finally {
@@ -330,6 +350,21 @@ async function load() {
}
}
async function loadWallets() {
const ownerType = props.definition.walletOwnerType;
if (!ownerType) {
walletByOwner.value = {};
return;
}
const wallets = await loadAllRows('/wallet_basic');
walletByOwner.value = wallets.reduce<Record<string, Row>>((result, wallet) => {
if (wallet.owner_type === ownerType) {
result[String(wallet.owner_identity ?? '')] = wallet;
}
return result;
}, {});
}
async function loadAllRows(resource: string) {
const rows: Row[] = [];
for (let currentPage = 1; currentPage <= 100; currentPage += 1) {
@@ -621,6 +656,7 @@ function fieldLabel(key: string) {
products: '合同气瓶',
order: '订单',
contract: '合同',
wallet: '钱包',
};
const normalizedKey = key.endsWith('_masked')
? key.slice(0, -'_masked'.length)
@@ -641,6 +677,10 @@ function relationLabel(field: ResourceField, identity: string) {
function displayFieldValue(field: ResourceField, row: Row) {
const value = row[field.key] ?? row[`${field.key}_masked`];
if (field.options) {
const option = field.options.find((item) => item.value === value);
if (option) return option.label;
}
if (
(field.type === 'identity' || field.type === 'identity-list') &&
typeof value === 'string'
@@ -754,6 +794,9 @@ function optionLabel(option: Row) {
padding: 12px;
background: var(--color-fill-1);
}
.muted-text {
color: var(--color-text-3);
}
.json-value {
max-height: 260px;
margin: 0;