517 lines
17 KiB
TypeScript
517 lines
17 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Button, Card, Space, Table, Tag, Typography, message, Modal, Form, Input, InputNumber, Upload,
|
|
} from 'antd';
|
|
import {
|
|
HistoryOutlined, ReloadOutlined, PlusOutlined, UploadOutlined, EyeOutlined, EditOutlined, DeleteOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getOpenTypeList, getOpenType, createOpenType, updateOpenType, deleteOpenType, uploadImage } from '../api';
|
|
import { formatDate } from '../utils/formatDate';
|
|
|
|
interface OpenType {
|
|
id: string;
|
|
openType: number;
|
|
typeName: string;
|
|
description: string;
|
|
thumb?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
const normalizeUploadError = (e: unknown): Error => {
|
|
if (e instanceof Error) {
|
|
return e;
|
|
}
|
|
|
|
if (typeof e === 'string') {
|
|
return new Error(e);
|
|
}
|
|
|
|
return new Error('上传失败');
|
|
};
|
|
|
|
const assertUploadFile = (file: unknown): File => {
|
|
if (file instanceof File) {
|
|
return file;
|
|
}
|
|
|
|
throw new Error('请选择有效图片文件');
|
|
};
|
|
|
|
const AdminPlatform: React.FC = () => {
|
|
const [openTypes, setOpenTypes] = useState<OpenType[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(20);
|
|
|
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
|
const [updateModalVisible, setUpdateModalVisible] = useState(false);
|
|
const [currentOpenType, setCurrentOpenType] = useState<OpenType | null>(null);
|
|
const [createThumbUrl, setCreateThumbUrl] = useState('');
|
|
const [updateThumbUrl, setUpdateThumbUrl] = useState('');
|
|
|
|
const [createForm] = Form.useForm();
|
|
const [updateForm] = Form.useForm();
|
|
|
|
const load = async (p?: number, ps?: number) => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await getOpenTypeList({
|
|
page: p || page,
|
|
page_size: ps || pageSize,
|
|
});
|
|
setOpenTypes(res.data || []);
|
|
setTotal(res.pagination.total || 0);
|
|
} catch {
|
|
message.error('加载开户方式列表失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, []);
|
|
|
|
const handleCreate = async () => {
|
|
try {
|
|
const values = await createForm.validateFields();
|
|
const thumbUrl = createThumbUrl;
|
|
const thumbPath = thumbUrl.startsWith('http') ? thumbUrl.replace(/^https?:\/\/[^/]+/, '') : thumbUrl;
|
|
await createOpenType({
|
|
open_type: values.open_type,
|
|
type_name: values.type_name,
|
|
description: values.description,
|
|
thumb: thumbPath,
|
|
});
|
|
message.success('创建成功');
|
|
setCreateModalVisible(false);
|
|
createForm.resetFields();
|
|
setCreateThumbUrl('');
|
|
load();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '创建失败');
|
|
}
|
|
};
|
|
|
|
const handleDetail = async (id: string) => {
|
|
try {
|
|
const openType = await getOpenType(id);
|
|
setCurrentOpenType(openType.data || {});
|
|
setDetailModalVisible(true);
|
|
} catch (e: any) {
|
|
message.error(e?.message || '获取详情失败');
|
|
}
|
|
};
|
|
|
|
const handleUpdate = async (id: string) => {
|
|
try {
|
|
const openType = await getOpenType(id);
|
|
const thumb = openType.data?.thumb || '';
|
|
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
|
const fullThumbUrl = thumb.startsWith('http') ? thumb : `${baseUrl}${thumb}`;
|
|
setCurrentOpenType(openType.data || {});
|
|
setUpdateThumbUrl(fullThumbUrl);
|
|
setUpdateModalVisible(true);
|
|
} catch (e: any) {
|
|
message.error(e?.message || '获取详情失败');
|
|
}
|
|
};
|
|
|
|
const handleSaveUpdate = async () => {
|
|
if (!currentOpenType) return;
|
|
try {
|
|
const values = await updateForm.validateFields();
|
|
const thumbUrl = updateThumbUrl;
|
|
const thumbPath = thumbUrl.startsWith('http') ? thumbUrl.replace(/^https?:\/\/[^/]+/, '') : thumbUrl;
|
|
await updateOpenType(currentOpenType.id, {
|
|
open_type: values.open_type,
|
|
type_name: values.type_name,
|
|
description: values.description,
|
|
thumb: thumbPath,
|
|
});
|
|
message.success('更新成功');
|
|
setUpdateModalVisible(false);
|
|
updateForm.resetFields();
|
|
setCurrentOpenType(null);
|
|
setUpdateThumbUrl('');
|
|
load();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '更新失败');
|
|
}
|
|
};
|
|
|
|
const handleDelete = (id: string) => {
|
|
Modal.confirm({
|
|
title: '确认删除',
|
|
content: '确定要删除这个开户方式吗?',
|
|
okText: '删除',
|
|
okType: 'danger',
|
|
cancelText: '取消',
|
|
onOk: async () => {
|
|
try {
|
|
await deleteOpenType(id);
|
|
message.success('删除成功');
|
|
load();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '删除失败');
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: 'ID',
|
|
dataIndex: 'id',
|
|
width: 100,
|
|
render: (v: string) => <Typography.Text>{v}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '开户类型',
|
|
dataIndex: 'openType',
|
|
width: 100,
|
|
render: (v: number) => <Typography.Text>{v}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '类型名称',
|
|
dataIndex: 'typeName',
|
|
width: 150,
|
|
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '描述',
|
|
dataIndex: 'description',
|
|
width: 250,
|
|
ellipsis: true,
|
|
render: (v: string) => (
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
{v}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{
|
|
title: '缩略图',
|
|
dataIndex: 'thumb',
|
|
width: 120,
|
|
render: (v: string) => {
|
|
if (!v) return '-';
|
|
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
|
const fullUrl = v.startsWith('http') ? v : `${baseUrl}${v}`;
|
|
return <img src={fullUrl} alt="thumb" style={{ width: 80, height: 60, objectFit: 'cover' }} />;
|
|
},
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'createdAt',
|
|
width: 160,
|
|
render: (v: string) => (
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
{formatDate(v)}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{
|
|
title: '更新时间',
|
|
dataIndex: 'updatedAt',
|
|
width: 160,
|
|
render: (v: string) => (
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
{formatDate(v)}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 240,
|
|
render: (_: any, record: OpenType) => (
|
|
<Space>
|
|
<Button
|
|
icon={<EyeOutlined />}
|
|
size="small"
|
|
onClick={() => handleDetail(record.id)}
|
|
>
|
|
详情
|
|
</Button>
|
|
<Button
|
|
icon={<EditOutlined />}
|
|
size="small"
|
|
onClick={() => handleUpdate(record.id)}
|
|
>
|
|
更新
|
|
</Button>
|
|
<Button
|
|
icon={<DeleteOutlined />}
|
|
size="small"
|
|
danger
|
|
onClick={() => handleDelete(record.id)}
|
|
>
|
|
删除
|
|
</Button>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
<Space>
|
|
<HistoryOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
|
<Typography.Text strong style={{ fontSize: 16 }}>平台开户方式管理</Typography.Text>
|
|
</Space>
|
|
<Space>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}>
|
|
新增
|
|
</Button>
|
|
<Button icon={<ReloadOutlined />} onClick={() => load()}>
|
|
刷新
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={openTypes}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
pageSize,
|
|
total,
|
|
showTotal: (t) => `共 ${t} 条记录`,
|
|
onChange: (p, ps) => {
|
|
setPage(p);
|
|
setPageSize(ps);
|
|
load(p, ps);
|
|
},
|
|
}}
|
|
scroll={{ x: 1000 }}
|
|
/>
|
|
</Card>
|
|
|
|
<Modal
|
|
title="新增开户方式"
|
|
open={createModalVisible}
|
|
onOk={handleCreate}
|
|
onCancel={() => {
|
|
setCreateModalVisible(false);
|
|
createForm.resetFields();
|
|
setCreateThumbUrl('');
|
|
}}
|
|
mask={{ closable: false }}
|
|
okText="创建"
|
|
cancelText="取消"
|
|
width={600}
|
|
>
|
|
<Form form={createForm} layout="vertical">
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<Form.Item
|
|
name="open_type"
|
|
label="开户方式Id"
|
|
style={{ flex: 1 }}
|
|
rules={[{ required: true, message: '请输入开户方式Id' }]}
|
|
>
|
|
<InputNumber style={{ width: '100%' }} min={1} placeholder="请输入开户方式Id" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="type_name"
|
|
label="类型名称"
|
|
style={{ flex: 1 }}
|
|
rules={[
|
|
{ required: true, message: '请输入类型名称' },
|
|
{ max: 100, message: '类型名称不能超过100个字符' },
|
|
]}
|
|
>
|
|
<Input style={{ width: '100%' }} placeholder="请输入类型名称" />
|
|
</Form.Item>
|
|
</div>
|
|
|
|
<Form.Item
|
|
name="description"
|
|
label="描述"
|
|
rules={[{ required: true, message: '请输入描述' }]}
|
|
>
|
|
<Input.TextArea placeholder="请输入描述" rows={4} />
|
|
</Form.Item>
|
|
<Form.Item label="缩略图">
|
|
<Upload
|
|
listType="picture-card"
|
|
fileList={createThumbUrl ? [{ uid: '1', name: 'thumb', status: 'done', url: createThumbUrl }] : []}
|
|
customRequest={async ({ file, onSuccess, onError }) => {
|
|
try {
|
|
const uploadFile = assertUploadFile(file);
|
|
const res = await uploadImage(uploadFile);
|
|
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
|
const fullUrl = res.url.startsWith('http') ? res.url : `${baseUrl}${res.url}`;
|
|
setCreateThumbUrl(fullUrl);
|
|
onSuccess?.({ url: fullUrl });
|
|
} catch (e) {
|
|
onError?.(normalizeUploadError(e));
|
|
}
|
|
}}
|
|
onRemove={() => setCreateThumbUrl('')}
|
|
>
|
|
{!createThumbUrl && (
|
|
<div>
|
|
<UploadOutlined style={{ fontSize: 24, color: '#999' }} />
|
|
<div style={{ marginTop: 8 }}>上传图片</div>
|
|
</div>
|
|
)}
|
|
</Upload>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
<Modal
|
|
title="开户方式详情"
|
|
open={detailModalVisible}
|
|
onOk={() => setDetailModalVisible(false)}
|
|
onCancel={() => {
|
|
setDetailModalVisible(false);
|
|
setCurrentOpenType(null);
|
|
}}
|
|
mask={{ closable: false }}
|
|
okText="关闭"
|
|
cancelText="取消"
|
|
width={520}
|
|
>
|
|
{currentOpenType && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 16, background: '#f8fafc', borderRadius: 8 }}>
|
|
<div style={{
|
|
width: 80, height: 60, borderRadius: 6, overflow: 'hidden',
|
|
background: currentOpenType.thumb ? 'transparent' : '#e2e8f0',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
{currentOpenType.thumb ? (
|
|
<img
|
|
src={currentOpenType.thumb.startsWith('http') ? currentOpenType.thumb : `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${currentOpenType.thumb}`}
|
|
alt="thumb"
|
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
/>
|
|
) : (
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>暂无图片</Typography.Text>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 16, fontWeight: 600 }}>{currentOpenType.typeName}</div>
|
|
<div style={{ fontSize: 12, color: '#64748b', marginTop: 4 }}>开户类型: {currentOpenType.openType}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<Typography.Text type="secondary" style={{ width: 80, flexShrink: 0 }}>ID</Typography.Text>
|
|
<Typography.Text>{currentOpenType.id}</Typography.Text>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<Typography.Text type="secondary" style={{ width: 80, flexShrink: 0 }}>开户类型</Typography.Text>
|
|
<Typography.Text>{currentOpenType.openType}</Typography.Text>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<Typography.Text type="secondary" style={{ width: 80, flexShrink: 0 }}>类型名称</Typography.Text>
|
|
<Typography.Text>{currentOpenType.typeName}</Typography.Text>
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
<Typography.Text type="secondary" style={{ width: 80 }}>描述</Typography.Text>
|
|
<div style={{ padding: 12, background: '#f8fafc', borderRadius: 6, fontSize: 13, lineHeight: 1.6 }}>
|
|
{currentOpenType.description || '-'}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<Typography.Text type="secondary" style={{ width: 80, flexShrink: 0 }}>创建时间</Typography.Text>
|
|
<Typography.Text>{formatDate(currentOpenType.createdAt)}</Typography.Text>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<Typography.Text type="secondary" style={{ width: 80, flexShrink: 0 }}>更新时间</Typography.Text>
|
|
<Typography.Text>{formatDate(currentOpenType.updatedAt)}</Typography.Text>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
|
|
<Modal
|
|
title="更新开户方式"
|
|
open={updateModalVisible}
|
|
onOk={handleSaveUpdate}
|
|
onCancel={() => {
|
|
setUpdateModalVisible(false);
|
|
updateForm.resetFields();
|
|
setCurrentOpenType(null);
|
|
}}
|
|
afterOpenChange={(open) => {
|
|
if (open && currentOpenType) {
|
|
updateForm.setFieldsValue({
|
|
open_type: currentOpenType.openType,
|
|
type_name: currentOpenType.typeName,
|
|
description: currentOpenType.description,
|
|
});
|
|
}
|
|
}}
|
|
mask={{ closable: false }}
|
|
okText="更新"
|
|
cancelText="取消"
|
|
width={600}
|
|
>
|
|
<Form form={updateForm} layout="vertical">
|
|
<Form.Item
|
|
name="open_type"
|
|
label="开户类型"
|
|
rules={[{ required: true, message: '请输入开户类型' }]}
|
|
>
|
|
<InputNumber min={1} placeholder="请输入开户类型编号" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="type_name"
|
|
label="类型名称"
|
|
rules={[
|
|
{ required: true, message: '请输入类型名称' },
|
|
{ max: 100, message: '类型名称不能超过100个字符' },
|
|
]}
|
|
>
|
|
<Input placeholder="请输入类型名称" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="description"
|
|
label="描述"
|
|
rules={[{ required: true, message: '请输入描述' }]}
|
|
>
|
|
<Input.TextArea placeholder="请输入描述" rows={4} />
|
|
</Form.Item>
|
|
<Form.Item label="缩略图">
|
|
<Upload
|
|
listType="picture-card"
|
|
fileList={updateThumbUrl ? [{ uid: '1', name: 'thumb', status: 'done', url: updateThumbUrl }] : []}
|
|
customRequest={async ({ file, onSuccess, onError }) => {
|
|
try {
|
|
const uploadFile = assertUploadFile(file);
|
|
const res = await uploadImage(uploadFile);
|
|
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
|
const fullUrl = res.url.startsWith('http') ? res.url : `${baseUrl}${res.url}`;
|
|
setUpdateThumbUrl(fullUrl);
|
|
onSuccess?.({ url: fullUrl });
|
|
} catch (e) {
|
|
onError?.(normalizeUploadError(e));
|
|
}
|
|
}}
|
|
onRemove={() => setUpdateThumbUrl('')}
|
|
>
|
|
{!updateThumbUrl && (
|
|
<div>
|
|
<UploadOutlined style={{ fontSize: 24, color: '#999' }} />
|
|
<div style={{ marginTop: 8 }}>上传图片</div>
|
|
</div>
|
|
)}
|
|
</Upload>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminPlatform; |