This commit is contained in:
2026-04-22 20:03:10 +08:00
parent 4047f87e8c
commit 7473fdcc03
65 changed files with 13311 additions and 10192 deletions
+2
View File
@@ -7,6 +7,7 @@ import MainLayout from '@/layouts/MainLayout';
import Login from '@/pages/Login';
import Dashboard from '@/pages/Dashboard';
import MerchantList from '@/pages/MerchantList';
import RoomAudit from '@/pages/RoomAudit';
import UserList from '@/pages/UserList';
import OrderList from '@/pages/OrderList';
import Finance from '@/pages/Finance';
@@ -29,6 +30,7 @@ const App: React.FC = () => (
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="merchants" element={<MerchantList />} />
<Route path="room-audit" element={<RoomAudit />} />
<Route path="users" element={<UserList />} />
<Route path="orders" element={<OrderList />} />
<Route path="finance" element={<Finance />} />
+14
View File
@@ -0,0 +1,14 @@
import request from '@/utils/request';
// 房源审核管理
export function getAdminRoomList(params: any) {
return request.get('/admin/rooms', { params });
}
export function approveRoom(id: number) {
return request.put(`/admin/rooms/${id}/approve`);
}
export function rejectRoom(id: number, reason: string) {
return request.put(`/admin/rooms/${id}/reject`, { reason });
}
@@ -11,6 +11,7 @@ import {
SettingOutlined,
LogoutOutlined,
UserOutlined,
HomeOutlined,
} from '@ant-design/icons';
import { useAuthStore } from '@/store/auth';
@@ -19,6 +20,7 @@ const { Header, Sider, Content } = Layout;
const menuItems = [
{ key: '/dashboard', icon: <DashboardOutlined />, label: '数据概览' },
{ key: '/merchants', icon: <ShopOutlined />, label: '商家管理' },
{ key: '/room-audit', icon: <HomeOutlined />, label: '房源审核' },
{ key: '/users', icon: <TeamOutlined />, label: '用户管理' },
{ key: '/orders', icon: <UnorderedListOutlined />, label: '订单管理' },
{ key: '/finance', icon: <WalletOutlined />, label: '财务管理' },
+163
View File
@@ -0,0 +1,163 @@
import React, { useEffect, useState } from 'react';
import { Table, Button, Space, Tag, Modal, Input, Select, message, Image } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getAdminRoomList, approveRoom, rejectRoom } from '@/api/room';
const typeLabels: Record<string, string> = { hotel: '酒店', homestay: '民宿', apartment: '公寓', hostel: '青旅' };
const auditStatusMap: Record<string, { label: string; color: string }> = {
pending: { label: '待审核', color: 'orange' },
approved: { label: '已通过', color: 'green' },
rejected: { label: '已拒绝', color: 'red' },
};
const RoomAudit: React.FC = () => {
const [data, setData] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState<{ keyword?: string; auditStatus?: string; type?: string }>({});
const [rejectModal, setRejectModal] = useState<{ visible: boolean; id: number | null; reason: string }>({
visible: false, id: null, reason: '',
});
const fetchData = async () => {
setLoading(true);
try {
const res: any = await getAdminRoomList({ page, pageSize: 10, ...filters });
setData(res.data?.list || []);
setTotal(res.data?.total || 0);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, [page, filters]);
const handleApprove = async (id: number) => {
await approveRoom(id);
message.success('审核通过');
fetchData();
};
const handleReject = () => {
if (!rejectModal.reason.trim()) {
message.warning('请输入拒绝原因');
return;
}
rejectRoom(rejectModal.id!, rejectModal.reason).then(() => {
message.success('已拒绝');
setRejectModal({ visible: false, id: null, reason: '' });
fetchData();
});
};
const columns: ColumnsType<any> = [
{
title: '封面', dataIndex: 'coverImage', width: 100,
render: (url) => url ? <Image src={url} width={80} height={60} style={{ borderRadius: 4, objectFit: 'cover' }} /> : '-',
},
{ title: '名称', dataIndex: 'name', width: 160, ellipsis: true },
{
title: '商家', dataIndex: 'merchant', width: 120, ellipsis: true,
render: (m) => m?.shopName || '-',
},
{ title: '类型', dataIndex: 'type', width: 80, render: (t) => typeLabels[t] || t },
{ title: '价格/晚', dataIndex: 'price', width: 100, render: (v) => `¥${v}` },
{
title: '审核状态', dataIndex: 'auditStatus', width: 100,
render: (s) => {
const info = auditStatusMap[s] || { label: s, color: 'default' };
return <Tag color={info.color}>{info.label}</Tag>;
},
},
{
title: '拒绝原因', dataIndex: 'auditRejectReason', width: 160, ellipsis: true,
render: (v) => v || '-',
},
{ title: '提交时间', dataIndex: 'createdAt', width: 170 },
{
title: '操作', width: 180, fixed: 'right',
render: (_, r) => (
<Space size="small">
{r.auditStatus === 'pending' && (
<>
<Button type="link" size="small" onClick={() => handleApprove(r.id)}></Button>
<Button type="link" danger size="small" onClick={() => setRejectModal({ visible: true, id: r.id, reason: '' })}>
</Button>
</>
)}
{r.auditStatus === 'approved' && <span style={{ color: '#999' }}></span>}
{r.auditStatus === 'rejected' && <span style={{ color: '#999' }}></span>}
</Space>
),
},
];
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Space>
<h2 style={{ margin: 0 }}></h2>
<Input.Search
placeholder="搜索房源名称"
allowClear
style={{ width: 200 }}
onSearch={(v) => { setFilters((f) => ({ ...f, keyword: v || undefined })); setPage(1); }}
/>
<Select
placeholder="审核状态"
allowClear
style={{ width: 120 }}
onChange={(v) => { setFilters((f) => ({ ...f, auditStatus: v })); setPage(1); }}
options={[
{ label: '待审核', value: 'pending' },
{ label: '已通过', value: 'approved' },
{ label: '已拒绝', value: 'rejected' },
]}
/>
<Select
placeholder="房源类型"
allowClear
style={{ width: 120 }}
onChange={(v) => { setFilters((f) => ({ ...f, type: v })); setPage(1); }}
options={[
{ label: '酒店', value: 'hotel' },
{ label: '民宿', value: 'homestay' },
{ label: '公寓', value: 'apartment' },
{ label: '青旅', value: 'hostel' },
]}
/>
</Space>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: 1200 }}
pagination={{ current: page, total, pageSize: 10, onChange: setPage }}
/>
<Modal
title="拒绝审核"
open={rejectModal.visible}
onOk={handleReject}
onCancel={() => setRejectModal({ visible: false, id: null, reason: '' })}
okText="确认拒绝"
cancelText="取消"
>
<Input.TextArea
rows={4}
placeholder="请输入拒绝原因"
maxLength={500}
value={rejectModal.reason}
onChange={(e) => setRejectModal((m) => ({ ...m, reason: e.target.value }))}
/>
</Modal>
</div>
);
};
export default RoomAudit;