feat: 迭代

This commit is contained in:
2026-05-08 20:16:34 +08:00
parent 350b9a94a1
commit da0f304a87
49 changed files with 3958 additions and 1004 deletions
+4
View File
@@ -15,7 +15,9 @@ import SystemSettings from '@/pages/SystemSettings';
import FinanceSettlements from '@/pages/finance/Settlements';
import FinanceWithdrawals from '@/pages/finance/Withdrawals';
import FinanceEarnings from '@/pages/finance/Earnings';
import FinanceServiceFees from '@/pages/finance/ServiceFees';
import InviteManage from '@/pages/InviteManage';
import ReviewManage from '@/pages/ReviewManage';
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const token = localStorage.getItem('admin_token');
@@ -41,8 +43,10 @@ const App: React.FC = () => (
<Route path="settlements" element={<FinanceSettlements />} />
<Route path="withdrawals" element={<FinanceWithdrawals />} />
<Route path="earnings" element={<FinanceEarnings />} />
<Route path="service-fees" element={<FinanceServiceFees />} />
</Route>
<Route path="invite" element={<InviteManage />} />
<Route path="reviews" element={<ReviewManage />} />
<Route path="promotions" element={<Promotion />} />
<Route path="settings" element={<SystemSettings />} />
</Route>
+5
View File
@@ -0,0 +1,5 @@
import request from '@/utils/request';
export function getServiceFees(params: any) {
return request.get('/admin/orders/service-fees/list', { params });
}
+13
View File
@@ -0,0 +1,13 @@
import request from '@/utils/request';
export function getReviews(params: any) {
return request.get('/admin/reviews', { params });
}
export function approveReview(id: number) {
return request.put(`/admin/reviews/${id}/approve`);
}
export function rejectReview(id: number, reason: string) {
return request.put(`/admin/reviews/${id}/reject`, { reason });
}
@@ -16,6 +16,8 @@ import {
PayCircleOutlined,
FundOutlined,
TrophyOutlined,
DollarOutlined,
StarOutlined,
} from '@ant-design/icons';
import { useAuthStore } from '@/store/auth';
@@ -27,6 +29,7 @@ const menuItems = [
{ key: '/room-audit', icon: <HomeOutlined />, label: '房源审核' },
{ key: '/users', icon: <TeamOutlined />, label: '用户管理' },
{ key: '/orders', icon: <UnorderedListOutlined />, label: '订单管理' },
{ key: '/reviews', icon: <StarOutlined />, label: '评价管理' },
{
key: '/finance',
icon: <WalletOutlined />,
@@ -35,6 +38,7 @@ const menuItems = [
{ key: '/finance/settlements', icon: <AuditOutlined />, label: '对账审核' },
{ key: '/finance/withdrawals', icon: <PayCircleOutlined />, label: '提现审核' },
{ key: '/finance/earnings', icon: <FundOutlined />, label: '平台收益' },
{ key: '/finance/service-fees', icon: <DollarOutlined />, label: '服务费管理' },
],
},
{ key: '/invite', icon: <TrophyOutlined />, label: '邀请返现' },
@@ -0,0 +1,204 @@
import React, { useState, useEffect } from 'react';
import { Table, Card, Rate, Image, Tag, Space, Button, message, Modal, Input, Select } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getReviews, approveReview, rejectReview } from '@/api/review';
import dayjs from 'dayjs';
const { TextArea } = Input;
const { Option } = Select;
const statusMap: Record<string, { color: string; label: string }> = {
pending: { color: 'orange', label: '待审核' },
visible: { color: 'green', label: '已通过' },
hidden: { color: 'red', label: '已隐藏' },
};
const ReviewManage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [reviews, setReviews] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [status, setStatus] = useState<string>('');
const [rejectModal, setRejectModal] = useState<{ visible: boolean; id: number | null; reason: string }>({
visible: false,
id: null,
reason: '',
});
const fetchReviews = async () => {
setLoading(true);
try {
const res: any = await getReviews({ page, pageSize, auditStatus: status || undefined });
setReviews(res.data?.list || []);
setTotal(res.data?.total || 0);
} catch (error) {
message.error('获取评价列表失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchReviews();
}, [page, pageSize, status]);
const handleApprove = async (id: number) => {
try {
await approveReview(id);
message.success('审核通过');
fetchReviews();
} catch (error: any) {
message.error(error.message || '操作失败');
}
};
const handleReject = async () => {
if (!rejectModal.reason.trim()) {
message.warning('请输入拒绝原因');
return;
}
try {
await rejectReview(rejectModal.id!, rejectModal.reason);
message.success('已拒绝');
setRejectModal({ visible: false, id: null, reason: '' });
fetchReviews();
} catch (error: any) {
message.error(error.message || '操作失败');
}
};
const columns: ColumnsType<any> = [
{
title: '订单号',
dataIndex: ['order', 'orderNo'],
width: 180,
},
{
title: '评分',
dataIndex: 'rating',
width: 150,
render: (rating: number) => <Rate disabled value={rating} />,
},
{
title: '评价内容',
dataIndex: 'content',
ellipsis: true,
render: (content: string) => content || '-',
},
{
title: '图片',
dataIndex: 'images',
width: 120,
render: (images: string[]) => {
if (!images || images.length === 0) return '-';
return (
<Image.PreviewGroup>
<Space>
{images.slice(0, 3).map((img, idx) => (
<Image key={idx} src={img} width={40} height={40} style={{ objectFit: 'cover' }} />
))}
{images.length > 3 && <span>+{images.length - 3}</span>}
</Space>
</Image.PreviewGroup>
);
},
},
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (status: string) => (
<Tag color={statusMap[status]?.color}>{statusMap[status]?.label || status}</Tag>
),
},
{
title: '评价时间',
dataIndex: 'createdAt',
width: 180,
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '操作',
width: 180,
fixed: 'right',
render: (_, record) => (
<Space>
{record.status === 'pending' && (
<>
<Button type="primary" size="small" onClick={() => handleApprove(record.id)}>
</Button>
<Button
danger
size="small"
onClick={() => setRejectModal({ visible: true, id: record.id, reason: '' })}
>
</Button>
</>
)}
</Space>
),
},
];
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
columns={columns}
dataSource={reviews}
rowKey="id"
loading={loading}
scroll={{ x: 1200 }}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setPage(page);
setPageSize(pageSize);
},
}}
/>
<Modal
title="拒绝评价"
open={rejectModal.visible}
onOk={handleReject}
onCancel={() => setRejectModal({ visible: false, id: null, reason: '' })}
>
<TextArea
rows={3}
placeholder="请输入拒绝原因"
value={rejectModal.reason}
onChange={(e) => setRejectModal({ ...rejectModal, reason: e.target.value })}
/>
</Modal>
</div>
);
};
export default ReviewManage;
@@ -0,0 +1,145 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, DatePicker, Input, Card, Statistic, Row, Col } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getServiceFees } from '@/api/order';
import dayjs from 'dayjs';
const { RangePicker } = DatePicker;
const { Search } = Input;
const ServiceFees: React.FC = () => {
const [data, setData] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [totalServiceFee, setTotalServiceFee] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [orderNo, setOrderNo] = useState('');
const [dateRange, setDateRange] = useState<any>(null);
const fetchData = async () => {
setLoading(true);
try {
const params: any = { page, pageSize: 10 };
if (orderNo) params.orderNo = orderNo;
if (dateRange) {
params.startDate = dateRange[0].format('YYYY-MM-DD');
params.endDate = dateRange[1].format('YYYY-MM-DD');
}
const res: any = await getServiceFees(params);
setData(res.data?.list || []);
setTotal(res.data?.total || 0);
setTotalServiceFee(res.data?.totalServiceFee || 0);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, [page, orderNo, dateRange]);
const columns: ColumnsType<any> = [
{ title: '订单号', dataIndex: 'orderNo', width: 200 },
{ title: '商家', dataIndex: ['merchant', 'shopName'], width: 150, ellipsis: true },
{ title: '房源', dataIndex: ['room', 'name'], width: 150, ellipsis: true },
{ title: '入住日期', dataIndex: 'checkInDate', width: 120 },
{ title: '离店日期', dataIndex: 'checkOutDate', width: 120 },
{
title: '订单金额',
dataIndex: 'payAmount',
width: 120,
render: (v) => `¥${v}`,
align: 'right',
},
{
title: '服务费',
dataIndex: 'serviceFee',
width: 120,
render: (v) => <span style={{ color: '#722ed1', fontWeight: 600 }}>¥{v}</span>,
align: 'right',
},
{
title: '商家收入',
dataIndex: 'merchantIncome',
width: 120,
render: (v) => `¥${v}`,
align: 'right',
},
{
title: '完成时间',
dataIndex: 'checkoutAt',
width: 180,
render: (v) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-',
},
];
return (
<div>
<div style={{ marginBottom: 24 }}>
<h2></h2>
<p style={{ color: '#999', marginTop: 8 }}></p>
</div>
<Row gutter={24} style={{ marginBottom: 24 }}>
<Col span={8}>
<Card>
<Statistic
title="服务费总收入"
value={totalServiceFee}
precision={2}
prefix="¥"
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="订单总数"
value={total}
suffix="笔"
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="平均服务费"
value={total > 0 ? totalServiceFee / total : 0}
precision={2}
prefix="¥"
/>
</Card>
</Col>
</Row>
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
<Search
placeholder="搜索订单号"
allowClear
style={{ width: 300 }}
onSearch={(v) => { setOrderNo(v); setPage(1); }}
/>
<RangePicker
placeholder={['开始日期', '结束日期']}
onChange={(dates) => { setDateRange(dates); setPage(1); }}
/>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: 1400 }}
pagination={{
current: page,
total,
pageSize: 10,
onChange: setPage,
showTotal: (total) => `${total} 条记录`,
}}
/>
</div>
);
};
export default ServiceFees;