feat(finance): 客服端「我的备用金」改为「我的钱包」,四平台并列
客服原来只能看到备用金现金,平台积分藏在 Tab 里且是空的。 现在四个钱包并列展示:南航积分 / 乐玩积分 / ROCK 积分 / 备用金现金。 后端: - 客服查询积分账户时自动补建三个空账户,保证四个钱包始终并列展示, 不会出现「点了没反应」的空白卡片。 前端: - 标题「我的备用金」→「我的钱包」,菜单同步改名。 - 顶部四张卡:三个积分平台各一张(显示积分余额 + 折合金额 + 换算率标签), 备用金现金一张。余额为 0 时置灰。 - 四个钱包卡片与备用金账户解耦:没开通备用金账户的客服也能看到积分钱包, 只是现金卡显示「尚未开通备用金账户」。 - Tab 调整为「积分流水 / 备用金流水 / 我的支出」,积分流水独立展示 (平台、类型、变动、余额、折合金额、备注、时间)。 - 备用金账户详情卡(账户编号、责任主管、状态、登记支出、缴回备用金) 仍然只在有账户时显示。 校验:后端 765 项 / 0 失败,前端 143 项全通过(含 lint、tsc -b、build)。 Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
d07d88e25c
commit
0a743f00b1
@@ -90,6 +90,11 @@ export async function listPlatformCreditAccounts(session: AdminSession) {
|
||||
clauses.push(`pca.owner_admin_user_id = $${params.length}`)
|
||||
}
|
||||
|
||||
// 客服第一次进来时补建三个空账户,保证四个钱包始终并列展示。
|
||||
if (!isPrivileged(session)) {
|
||||
await ensurePlatformCreditAccounts(session.userId)
|
||||
}
|
||||
|
||||
const where = clauses.length > 0 ? `AND ${clauses.join(' AND ')}` : ''
|
||||
const result = await query<CreditAccountRow>(
|
||||
`
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function AdminLayout() {
|
||||
{
|
||||
key: '/admin/fund',
|
||||
icon: <WalletOutlined />,
|
||||
label: role === 'support' ? '我的备用金' : '备用金',
|
||||
label: role === 'support' ? '我的钱包' : '备用金',
|
||||
},
|
||||
...(role === 'support'
|
||||
? []
|
||||
|
||||
@@ -34,16 +34,18 @@ import {
|
||||
createAdminFundExpense,
|
||||
createAdminFundReturn,
|
||||
fetchAdminFundMyAccount,
|
||||
fetchAdminPlatformCreditAccounts,
|
||||
fetchAdminPlatformCreditEntries,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminFundEntry,
|
||||
AdminFundExpense,
|
||||
AdminFundExpenseCategory,
|
||||
AdminFundPayeeType,
|
||||
PlatformCreditEntry,
|
||||
} from '@/types/admin'
|
||||
import type { UploadedFile } from '@/types/worker-platform'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { PlatformCreditPanel } from './PlatformCreditPanel'
|
||||
import {
|
||||
FUND_ACCOUNT_STATUS_LABELS,
|
||||
FUND_CATEGORY_LABELS,
|
||||
@@ -94,11 +96,36 @@ export default function FundSupportView() {
|
||||
const entries = data?.entries || []
|
||||
const expenses = data?.expenses || []
|
||||
|
||||
const creditAccountsQuery = useQuery({
|
||||
queryKey: ['admin-platform-credit-accounts'],
|
||||
queryFn: fetchAdminPlatformCreditAccounts,
|
||||
})
|
||||
const creditEntriesQuery = useQuery({
|
||||
queryKey: ['admin-platform-credit-entries', 'mine'],
|
||||
queryFn: () => fetchAdminPlatformCreditEntries({ pageSize: 50 }),
|
||||
})
|
||||
const creditAccounts = creditAccountsQuery.data?.data.items || []
|
||||
const creditEntries = creditEntriesQuery.data?.data.items || []
|
||||
const creditPlatforms = creditAccountsQuery.data?.data.platforms || []
|
||||
|
||||
/** 客服自己的三个积分钱包,按平台顺序排好。 */
|
||||
const myCreditWallets = creditPlatforms.map((platform) => {
|
||||
const found = creditAccounts.find((item) => item.platformKey === platform.key)
|
||||
return {
|
||||
platform,
|
||||
balanceQuantity: found?.balanceQuantity ?? 0,
|
||||
balanceAmountFen: found?.balanceAmountFen ?? 0,
|
||||
unitName: found?.unitName ?? '积分',
|
||||
}
|
||||
})
|
||||
|
||||
function refresh() {
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-fund-overview'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-fund-expenses'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-accounts'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-entries'] })
|
||||
}
|
||||
|
||||
function openExpense() {
|
||||
@@ -216,6 +243,71 @@ export default function FundSupportView() {
|
||||
},
|
||||
]
|
||||
|
||||
const creditEntryColumns: TableColumnsType<PlatformCreditEntry> = [
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platformKey',
|
||||
width: 100,
|
||||
render: (value: string, row) => (
|
||||
<Tag color={value === 'rock' ? 'cyan' : value === 'lewan' ? 'purple' : 'blue'}>
|
||||
{row.platformLabel}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'entryType',
|
||||
width: 80,
|
||||
render: (value: string) => (
|
||||
<Tag color={value === 'grant' ? 'green' : value === 'consume' ? 'red' : 'gold'}>
|
||||
{value === 'grant'
|
||||
? '充值'
|
||||
: value === 'consume'
|
||||
? '消耗'
|
||||
: value === 'return'
|
||||
? '退回'
|
||||
: '调整'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '变动',
|
||||
dataIndex: 'quantityDelta',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
render: (value: number) =>
|
||||
value > 0 ? (
|
||||
<Typography.Text style={{ color: '#389e0d' }}>+{value}</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text style={{ color: '#cf1322' }}>{value}</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '余额',
|
||||
dataIndex: 'balanceAfter',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
title: '折合金额',
|
||||
dataIndex: 'amountFen',
|
||||
width: 110,
|
||||
align: 'right',
|
||||
render: (value: number) => `¥${(Number(value || 0) / 100).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'note',
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 150,
|
||||
render: (value: string) => formatAdminDateTime(value),
|
||||
},
|
||||
]
|
||||
|
||||
const expenseColumns: TableColumnsType<AdminFundExpense> = [
|
||||
{
|
||||
title: '付款时间',
|
||||
@@ -281,10 +373,14 @@ export default function FundSupportView() {
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<PageHeader
|
||||
title="我的备用金"
|
||||
description="查看当前余额、登记每一笔支出;余额由流水自动计算,不能手工修改。"
|
||||
title="我的钱包"
|
||||
description="四个平台钱包:南航 / 乐玩 / ROCK 积分与备用金现金。余额由流水自动计算,不能手工修改。"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} loading={myQuery.isFetching} onClick={refresh}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={myQuery.isFetching || creditAccountsQuery.isFetching}
|
||||
onClick={refresh}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
@@ -303,124 +399,135 @@ export default function FundSupportView() {
|
||||
type="warning"
|
||||
showIcon
|
||||
message="还没有备用金账户"
|
||||
description="请联系主管或管理员为你开通客服备用金账户。"
|
||||
description="请联系主管或管理员为你开通客服备用金账户。平台积分钱包不受影响,可直接使用。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{account ? (
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="当前余额"
|
||||
value={account.balanceFen / 100}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="本月发放"
|
||||
value={account.monthInFen / 100}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="本月支出/缴回"
|
||||
value={account.monthOutFen / 100}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="最近对账"
|
||||
value={
|
||||
account.lastReconciledAt
|
||||
? formatAdminDateTime(account.lastReconciledAt)
|
||||
: '未对账'
|
||||
}
|
||||
valueStyle={{ fontSize: 18 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{myCreditWallets.map((wallet) => (
|
||||
<Col xs={24} sm={12} lg={6} key={wallet.platform.key}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title={
|
||||
<Space size={6}>
|
||||
<span>{wallet.platform.label}</span>
|
||||
<Tag color={wallet.platform.creditUnitPriceFen >= 100 ? 'cyan' : 'blue'}>
|
||||
1积分=
|
||||
{wallet.platform.creditUnitPriceFen >= 100 ? '1元' : '1分'}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
value={wallet.balanceQuantity}
|
||||
suffix="积分"
|
||||
valueStyle={{
|
||||
color: wallet.balanceQuantity > 0 ? undefined : '#999',
|
||||
}}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
折合 ¥{(wallet.balanceAmountFen / 100).toFixed(2)}
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Descriptions column={2} size="small">
|
||||
<Descriptions.Item label="账户编号">{account.accountNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="责任主管">
|
||||
{account.responsibleSupervisorName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账户状态">
|
||||
{FUND_ACCOUNT_STATUS_LABELS[account.status] || account.status}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近流水">
|
||||
{account.lastEntryAt ? formatAdminDateTime(account.lastEntryAt) : '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider />
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openExpense}>
|
||||
登记支出
|
||||
</Button>
|
||||
<Button icon={<RollbackOutlined />} onClick={openReturn}>
|
||||
缴回备用金
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'entries',
|
||||
label: '资金流水',
|
||||
children: (
|
||||
<Table<AdminFundEntry>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={entryColumns}
|
||||
dataSource={entries}
|
||||
pagination={false}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'expenses',
|
||||
label: '我的支出',
|
||||
children: (
|
||||
<Table<AdminFundExpense>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={expenseColumns}
|
||||
dataSource={expenses}
|
||||
pagination={false}
|
||||
scroll={{ x: 1100 }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'platform-credits',
|
||||
label: '平台积分',
|
||||
children: <PlatformCreditPanel />,
|
||||
},
|
||||
]}
|
||||
<Statistic
|
||||
title="备用金现金"
|
||||
value={account ? account.balanceFen / 100 : 0}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
valueStyle={{
|
||||
color: account && account.balanceFen > 0 ? undefined : '#999',
|
||||
}}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{account
|
||||
? `本月发放 ¥${(account.monthInFen / 100).toFixed(2)} · 支出/缴回 ¥${(account.monthOutFen / 100).toFixed(2)}`
|
||||
: '尚未开通备用金账户'}
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{account ? (
|
||||
<Card>
|
||||
<Descriptions column={2} size="small">
|
||||
<Descriptions.Item label="账户编号">{account.accountNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="责任主管">
|
||||
{account.responsibleSupervisorName || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账户状态">
|
||||
{FUND_ACCOUNT_STATUS_LABELS[account.status] || account.status}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最近流水">
|
||||
{account.lastEntryAt ? formatAdminDateTime(account.lastEntryAt) : '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Divider />
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openExpense}>
|
||||
登记支出
|
||||
</Button>
|
||||
<Button icon={<RollbackOutlined />} onClick={openReturn}>
|
||||
缴回备用金
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'credit-entries',
|
||||
label: '积分流水',
|
||||
children: (
|
||||
<Table<PlatformCreditEntry>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={creditEntryColumns}
|
||||
dataSource={creditEntries}
|
||||
loading={creditEntriesQuery.isLoading}
|
||||
pagination={false}
|
||||
scroll={{ x: 900 }}
|
||||
locale={{ emptyText: '还没有积分流水' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'entries',
|
||||
label: '备用金流水',
|
||||
children: (
|
||||
<Table<AdminFundEntry>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={entryColumns}
|
||||
dataSource={entries}
|
||||
pagination={false}
|
||||
scroll={{ x: 900 }}
|
||||
locale={{ emptyText: '还没有备用金流水' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'expenses',
|
||||
label: '我的支出',
|
||||
children: (
|
||||
<Table<AdminFundExpense>
|
||||
rowKey="id"
|
||||
size="small"
|
||||
columns={expenseColumns}
|
||||
dataSource={expenses}
|
||||
pagination={false}
|
||||
scroll={{ x: 1100 }}
|
||||
locale={{ emptyText: '还没有支出记录' }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="登记备用金支出"
|
||||
open={expenseOpen}
|
||||
|
||||
Reference in New Issue
Block a user