feat: 迭代
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export function getServiceFees(params: any) {
|
||||
return request.get('/admin/orders/service-fees/list', { params });
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user