fix
This commit is contained in:
@@ -10,7 +10,9 @@ import RoomList from '@/pages/RoomList';
|
||||
import RoomForm from '@/pages/RoomForm';
|
||||
import RoomCalendar from '@/pages/RoomCalendar';
|
||||
import ReviewList from '@/pages/ReviewList';
|
||||
import Finance from '@/pages/Finance';
|
||||
import FinanceSettlements from '@/pages/finance/Settlements';
|
||||
import FinanceWithdrawals from '@/pages/finance/Withdrawals';
|
||||
import FinanceWallet from '@/pages/finance/Wallet';
|
||||
import Settings from '@/pages/Settings';
|
||||
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
@@ -35,7 +37,12 @@ const App: React.FC = () => (
|
||||
<Route path="room-calendar" element={<RoomCalendar />} />
|
||||
<Route path="room-calendar/:roomId" element={<RoomCalendar />} />
|
||||
<Route path="reviews" element={<ReviewList />} />
|
||||
<Route path="finance" element={<Finance />} />
|
||||
<Route path="finance">
|
||||
<Route index element={<Navigate to="/finance/settlements" replace />} />
|
||||
<Route path="settlements" element={<FinanceSettlements />} />
|
||||
<Route path="withdrawals" element={<FinanceWithdrawals />} />
|
||||
<Route path="wallet" element={<FinanceWallet />} />
|
||||
</Route>
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
// 对账单列表
|
||||
export const getSettlements = (params?: { page?: number; pageSize?: number; status?: string }) =>
|
||||
request.get('/seller/finance/settlements', { params });
|
||||
|
||||
// 对账单详情
|
||||
export const getSettlementDetail = (id: number) =>
|
||||
request.get(`/seller/finance/settlements/${id}`);
|
||||
|
||||
// 提现记录列表
|
||||
export const getWithdrawals = (params?: { page?: number; pageSize?: number; status?: string }) =>
|
||||
request.get('/seller/finance/withdrawals', { params });
|
||||
|
||||
// 申请提现
|
||||
export const createWithdrawal = (data: { amount: number; bankName: string; bankAccount: string; accountName: string }) =>
|
||||
request.post('/seller/finance/withdraw', data);
|
||||
|
||||
// 获取钱包信息
|
||||
export const getWallet = () =>
|
||||
request.get('/seller/finance/wallet');
|
||||
|
||||
// 更新银行卡信息
|
||||
export const updateBankInfo = (data: { bankName: string; bankAccount: string; accountName: string }) =>
|
||||
request.put('/seller/finance/bank', data);
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Layout, Menu, Button, Avatar, Dropdown } from 'antd';
|
||||
import {
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
CalendarOutlined,
|
||||
AuditOutlined,
|
||||
PayCircleOutlined,
|
||||
AccountBookOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
|
||||
@@ -22,7 +25,16 @@ const menuItems = [
|
||||
{ key: '/rooms', icon: <HomeOutlined />, label: '房源管理' },
|
||||
{ key: '/room-calendar', icon: <CalendarOutlined />, label: '房量房价' },
|
||||
{ key: '/reviews', icon: <CommentOutlined />, label: '评价管理' },
|
||||
{ key: '/finance', icon: <WalletOutlined />, label: '财务管理' },
|
||||
{
|
||||
key: '/finance',
|
||||
icon: <WalletOutlined />,
|
||||
label: '财务管理',
|
||||
children: [
|
||||
{ key: '/finance/settlements', icon: <AuditOutlined />, label: '结算对账' },
|
||||
{ key: '/finance/withdrawals', icon: <PayCircleOutlined />, label: '收款记录' },
|
||||
{ key: '/finance/wallet', icon: <AccountBookOutlined />, label: '我的钱包' },
|
||||
],
|
||||
},
|
||||
{ key: '/settings', icon: <SettingOutlined />, label: '店铺设置' },
|
||||
];
|
||||
|
||||
@@ -31,15 +43,22 @@ const MainLayout: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const { sellerInfo, logout } = useAuthStore();
|
||||
|
||||
const openKeys = useMemo(() => {
|
||||
const path = location.pathname;
|
||||
if (path.startsWith('/finance')) return ['/finance'];
|
||||
return [];
|
||||
}, [location.pathname]);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider width={220} theme="light" style={{ borderRight: '1px solid #f0f0f0' }}>
|
||||
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<h2 style={{ margin: 0, color: '#FF6B35', fontSize: 18 }}>短租预订平台</h2>
|
||||
<h2 style={{ margin: 0, color: '#FF6B35', fontSize: 18 }}>品住会商家平台</h2>
|
||||
</div>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
defaultOpenKeys={openKeys}
|
||||
items={menuItems}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
style={{ borderRight: 0 }}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Button, Select, Modal, Descriptions, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { getSettlements, getSettlementDetail } from '@/api/finance';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const statusMap: Record<string, { color: string; label: string }> = {
|
||||
pending: { color: 'gold', label: '待审核' },
|
||||
approved: { color: 'green', label: '已审核' },
|
||||
rejected: { color: 'red', label: '已拒绝' },
|
||||
};
|
||||
|
||||
const Settlements: React.FC = () => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
const [detail, setDetail] = useState<any>(null);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getSettlements({ page, pageSize: 10, status: status || undefined });
|
||||
setData(res.data?.list || []);
|
||||
setTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [page, status]);
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res: any = await getSettlementDetail(id);
|
||||
setDetail(res.data);
|
||||
setDetailVisible(true);
|
||||
} catch {
|
||||
message.error('获取详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: '对账单号', dataIndex: 'settlementNo', width: 200 },
|
||||
{ title: '周期', width: 200, render: (_, r) => `${r.periodStart} ~ ${r.periodEnd}` },
|
||||
{ title: '订单数', dataIndex: 'orderCount', width: 80, align: 'center' },
|
||||
{ title: '订单金额', dataIndex: 'orderAmount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '佣金', dataIndex: 'commissionAmount', width: 100, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '结算金额', dataIndex: 'settlementAmount', width: 120, render: (v) => <span style={{ color: '#FF6B35', fontWeight: 600 }}>¥{Number(v).toFixed(2)}</span> },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', width: 100,
|
||||
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 180 },
|
||||
{
|
||||
title: '操作', width: 80, fixed: 'right',
|
||||
render: (_, r) => <Button type="link" size="small" onClick={() => showDetail(r.id)}>详情</Button>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<h2>结算对账</h2>
|
||||
<Select value={status || undefined} placeholder="状态筛选" style={{ width: 150 }} allowClear onChange={(v) => { setStatus(v || ''); setPage(1); }}>
|
||||
{Object.entries(statusMap).map(([k, v]) => <Option key={k} value={k}>{v.label}</Option>)}
|
||||
</Select>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
scroll={{ x: 1100 }}
|
||||
pagination={{ current: page, total, pageSize: 10, onChange: setPage }}
|
||||
/>
|
||||
<Modal
|
||||
title="对账单详情"
|
||||
open={detailVisible}
|
||||
footer={null}
|
||||
width={720}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
>
|
||||
{detail && (
|
||||
<>
|
||||
<Descriptions bordered size="small" column={2} style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="对账单号">{detail.settlementNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态"><Tag color={statusMap[detail.status]?.color}>{statusMap[detail.status]?.label}</Tag></Descriptions.Item>
|
||||
<Descriptions.Item label="周期">{detail.periodStart} ~ {detail.periodEnd}</Descriptions.Item>
|
||||
<Descriptions.Item label="订单数">{detail.orderCount}</Descriptions.Item>
|
||||
<Descriptions.Item label="订单金额">¥{Number(detail.orderAmount).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="佣金比例">{Number(detail.commissionRate) * 100}%</Descriptions.Item>
|
||||
<Descriptions.Item label="佣金金额">¥{Number(detail.commissionAmount).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="结算金额"><span style={{ color: '#FF6B35', fontWeight: 600 }}>¥{Number(detail.settlementAmount).toFixed(2)}</span></Descriptions.Item>
|
||||
</Descriptions>
|
||||
{detail.rejectReason && (
|
||||
<div style={{ color: 'red', marginBottom: 16 }}>拒绝原因:{detail.rejectReason}</div>
|
||||
)}
|
||||
{detail.items?.length > 0 && (
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
dataSource={detail.items}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'orderNo', width: 200 },
|
||||
{ title: '订单金额', dataIndex: 'orderAmount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '创建时间', dataIndex: 'createdAt' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settlements;
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Statistic, Button, Modal, Form, Input, InputNumber, Descriptions, message, Spin } from 'antd';
|
||||
import { WalletOutlined, BankOutlined } from '@ant-design/icons';
|
||||
import { getWallet, createWithdrawal, updateBankInfo } from '@/api/finance';
|
||||
|
||||
const Wallet: React.FC = () => {
|
||||
const [wallet, setWallet] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [withdrawVisible, setWithdrawVisible] = useState(false);
|
||||
const [bankVisible, setBankVisible] = useState(false);
|
||||
const [withdrawForm] = Form.useForm();
|
||||
const [bankForm] = Form.useForm();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const fetchWallet = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getWallet();
|
||||
setWallet(res.data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchWallet(); }, []);
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
try {
|
||||
const values = await withdrawForm.validateFields();
|
||||
setSubmitting(true);
|
||||
await createWithdrawal(values);
|
||||
message.success('提现申请已提交');
|
||||
setWithdrawVisible(false);
|
||||
withdrawForm.resetFields();
|
||||
fetchWallet();
|
||||
} catch (e: any) {
|
||||
if (e?.message) message.error(e.message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBankUpdate = async () => {
|
||||
try {
|
||||
const values = await bankForm.validateFields();
|
||||
setSubmitting(true);
|
||||
await updateBankInfo(values);
|
||||
message.success('银行卡信息已更新');
|
||||
setBankVisible(false);
|
||||
fetchWallet();
|
||||
} catch (e: any) {
|
||||
if (e?.message) message.error(e.message);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openWithdraw = () => {
|
||||
withdrawForm.setFieldsValue({
|
||||
bankName: wallet?.bankName || '',
|
||||
bankAccount: wallet?.bankAccount || '',
|
||||
accountName: wallet?.accountName || '',
|
||||
});
|
||||
setWithdrawVisible(true);
|
||||
};
|
||||
|
||||
const openBankEdit = () => {
|
||||
bankForm.setFieldsValue({
|
||||
bankName: wallet?.bankName || '',
|
||||
bankAccount: wallet?.bankAccount || '',
|
||||
accountName: wallet?.accountName || '',
|
||||
});
|
||||
setBankVisible(true);
|
||||
};
|
||||
|
||||
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>我的钱包</h2>
|
||||
|
||||
<div style={{ display: 'flex', gap: 24, marginBottom: 24 }}>
|
||||
<Card style={{ flex: 1 }}>
|
||||
<Statistic
|
||||
title="待提现余额"
|
||||
value={wallet?.walletBalance || 0}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#FF6B35' }}
|
||||
/>
|
||||
<Button type="primary" style={{ marginTop: 16 }} disabled={!wallet?.walletBalance || Number(wallet.walletBalance) < 100} onClick={openWithdraw}>
|
||||
申请提现
|
||||
</Button>
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#999' }}>最低提现金额:¥100.00</div>
|
||||
</Card>
|
||||
<Card style={{ flex: 1 }}>
|
||||
<Statistic
|
||||
title="提现处理中"
|
||||
value={wallet?.pendingWithdrawAmount || 0}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card title={<><BankOutlined style={{ marginRight: 8 }} />银行卡信息</>} extra={<Button type="link" onClick={openBankEdit}>编辑</Button>}>
|
||||
<Descriptions column={3}>
|
||||
<Descriptions.Item label="开户银行">{wallet?.bankName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="银行账号">{wallet?.bankAccount || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="账户名">{wallet?.accountName || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* 提现弹窗 */}
|
||||
<Modal
|
||||
title="申请提现"
|
||||
open={withdrawVisible}
|
||||
onOk={handleWithdraw}
|
||||
onCancel={() => setWithdrawVisible(false)}
|
||||
confirmLoading={submitting}
|
||||
okText="确认提现"
|
||||
>
|
||||
<div style={{ marginBottom: 16, color: '#999' }}>
|
||||
可提现余额:<span style={{ color: '#FF6B35', fontWeight: 600 }}>¥{Number(wallet?.walletBalance || 0).toFixed(2)}</span>
|
||||
</div>
|
||||
<Form form={withdrawForm} layout="vertical">
|
||||
<Form.Item name="amount" label="提现金额" rules={[{ required: true, message: '请输入提现金额' }]}>
|
||||
<InputNumber style={{ width: '100%' }} min={100} max={Number(wallet?.walletBalance || 0)} precision={2} prefix="¥" placeholder="请输入提现金额" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankName" label="开户银行" rules={[{ required: true, message: '请输入开户银行' }]}>
|
||||
<Input placeholder="请输入开户银行" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccount" label="银行账号" rules={[{ required: true, message: '请输入银行账号' }]}>
|
||||
<Input placeholder="请输入银行账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="账户名" rules={[{ required: true, message: '请输入账户名' }]}>
|
||||
<Input placeholder="请输入账户名" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div style={{ fontSize: 12, color: '#999' }}>
|
||||
手续费率 0.6%,实际到账 = 提现金额 - 手续费
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 银行卡编辑弹窗 */}
|
||||
<Modal
|
||||
title="编辑银行卡信息"
|
||||
open={bankVisible}
|
||||
onOk={handleBankUpdate}
|
||||
onCancel={() => setBankVisible(false)}
|
||||
confirmLoading={submitting}
|
||||
okText="保存"
|
||||
>
|
||||
<Form form={bankForm} layout="vertical">
|
||||
<Form.Item name="bankName" label="开户银行" rules={[{ required: true, message: '请输入开户银行' }]}>
|
||||
<Input placeholder="请输入开户银行" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccount" label="银行账号" rules={[{ required: true, message: '请输入银行账号' }]}>
|
||||
<Input placeholder="请输入银行账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="accountName" label="账户名" rules={[{ required: true, message: '请输入账户名' }]}>
|
||||
<Input placeholder="请输入账户名" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wallet;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Select } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { getWithdrawals } from '@/api/finance';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const statusMap: Record<string, { color: string; label: string }> = {
|
||||
pending: { color: 'gold', label: '待审核' },
|
||||
approved: { color: 'blue', label: '审核通过' },
|
||||
rejected: { color: 'red', label: '已拒绝' },
|
||||
paid: { color: 'green', label: '已打款' },
|
||||
};
|
||||
|
||||
const Withdrawals: React.FC = () => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getWithdrawals({ page, pageSize: 10, status: status || undefined });
|
||||
setData(res.data?.list || []);
|
||||
setTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [page, status]);
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: '提现金额', dataIndex: 'amount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '手续费', dataIndex: 'fee', width: 100, render: (v) => `¥${Number(v).toFixed(2)}` },
|
||||
{ title: '实际到账', dataIndex: 'actualAmount', width: 120, render: (v) => <span style={{ color: '#FF6B35', fontWeight: 600 }}>¥{Number(v).toFixed(2)}</span> },
|
||||
{ title: '开户银行', dataIndex: 'bankName', width: 120 },
|
||||
{ title: '银行账号', dataIndex: 'bankAccount', width: 160 },
|
||||
{ title: '账户名', dataIndex: 'accountName', width: 100 },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', width: 100,
|
||||
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
|
||||
},
|
||||
{ title: '申请时间', dataIndex: 'createdAt', width: 180 },
|
||||
{ title: '打款时间', dataIndex: 'paidAt', width: 180 },
|
||||
{
|
||||
title: '备注', dataIndex: 'rejectReason', width: 150, ellipsis: true,
|
||||
render: (v) => v || '-',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<h2>收款记录</h2>
|
||||
<Select value={status || undefined} placeholder="状态筛选" style={{ width: 150 }} allowClear onChange={(v) => { setStatus(v || ''); setPage(1); }}>
|
||||
{Object.entries(statusMap).map(([k, v]) => <Option key={k} value={k}>{v.label}</Option>)}
|
||||
</Select>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{ current: page, total, pageSize: 10, onChange: setPage }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Withdrawals;
|
||||
Reference in New Issue
Block a user