This commit is contained in:
2026-06-25 16:53:46 +08:00
39 changed files with 2943 additions and 866 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ import AdminAuthoriz from './pages/AdminAuthoriz';
import AdminConsume from './pages/AdminConsume';
import AdminLoginPage from './pages/AdminLoginPage';
import AdminDashboard from './pages/AdminDashboard';
import AdminPlatform from './pages/AdminPlatform';
import AdminPlatform from './pages/Adminplatform';
import AdminUsers from './pages/AdminUsers';
import AdminModels from './pages/AdminModels';
import AdminSettings from './pages/AdminSettings';
+80
View File
@@ -424,6 +424,49 @@ export async function deleteOauthApp(id: string): Promise<void> {
await api.get(`/admin/user-oauth-apps/delete/${id}`);
}
// ── Open Type ───────────────────────────────────────────────
export async function getOpenTypeList(params?: {
page?: number;
page_size?: number;
type_name?: string;
open_type?: number;
}): Promise<{ total: number; items: any[] }> {
const q = new URLSearchParams();
if (params?.page) q.set('page', String(params.page));
if (params?.page_size) q.set('page_size', String(params.page_size));
if (params?.type_name) q.set('type_name', params.type_name);
if (params?.open_type) q.set('open_type', String(params.open_type));
const qs = q.toString();
return api.get(`/open-type/list${qs ? `?${qs}` : ''}`);
}
export async function getOpenType(id: string): Promise<any> {
return api.get(`/open-type/select/${id}`);
}
export async function createOpenType(data: {
open_type: number;
description: string;
type_name: string;
thumb?: string;
}): Promise<any> {
return api.post('/open-type/create', data);
}
export async function updateOpenType(id: string, data: {
open_type?: number;
description?: string;
type_name?: string;
thumb?: string;
}): Promise<any> {
return api.put(`/open-type/update/${id}`, data);
}
export async function deleteOpenType(id: string): Promise<void> {
await api.delete(`/open-type/delete/${id}`);
}
// ── Generation Records (Admin) ─────────────────────────────
export async function getAdminGenerationRecords(params?: {
@@ -587,3 +630,40 @@ export async function getOAuthList(params: OAuthListParams): Promise<any> {
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
return api.get(`/user-oauth/oauth_list?${query.toString()}`);
}
export async function uploadImage(file: File): Promise<{ url: string; filename: string }> {
const form = new FormData();
form.append('file', file);
const token = localStorage.getItem('auth_token');
const res = await fetch(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/upload-image`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: form,
});
if (!res.ok) throw new Error('图片上传失败');
const data = await res.json();
return { url: data.url, filename: data.filename };
}
// 自定义表头字段
export async function getFields(): Promise<any> {
return api.get('/material-consumption/fields');
}
// 查询素材消耗列表
export interface MaterialConsumpParams {
advertiser_id?: string;
user_id?: string;
consume_date?: [string, string];
page?: number;
page_size?: number;
}
export async function getMaterialConsumpList(params: MaterialConsumpParams): Promise<any> {
const query = new URLSearchParams();
if (params.advertiser_id) query.set('advertiser_id', params.advertiser_id);
if (params.user_id) query.set('user_id', params.user_id);
if (params.consume_date !== undefined) query.set('consume_date', params.consume_date[0] + ',' + params.consume_date[1]);
if (params.page !== undefined) query.set('page', String(params.page));
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
return api.get(`/material-consumption/admin/list?${query.toString()}`);
}
+125 -54
View File
@@ -1,67 +1,81 @@
import React, { useState, useEffect } from 'react';
import { Card, Space, Table, Button, Typography, Modal, Checkbox, Input, message } from 'antd';
import { ArrowLeftOutlined, DollarOutlined, SettingOutlined } from '@ant-design/icons';
import { Card, Space, Table, Button, Typography, Modal, Checkbox, Input, message, DatePicker } from 'antd';
import { ArrowLeftOutlined, DollarOutlined, SettingOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getFields, getMaterialConsumpList } from '../api';
import dayjs from 'dayjs';
// 表头字段模拟数据
const mockColumnsData = [
{ name: 'id', description: 'id' },
{ name: 'star_id', description: '星图ID' },
{ name: 'demand_id', description: '任务ID' },
{ name: 'demand_name', description: '任务名称' },
{ name: 'demander_name', description: '客户名称' },
{ name: 'brand', description: '品牌' },
{ name: 'max_publish_count', description: '最大发布数量' },
{ name: 'product_name', description: '产品名称' },
{ name: 'product_information', description: '产品信息' },
];
const { RangePicker } = DatePicker;
const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
interface ConsumptionRecord {
id: string;
star_id?: string;
demand_id?: string;
demand_name?: string;
demander_name?: string;
brand?: string;
max_publish_count?: number;
product_name?: string;
product_information?: string;
advertiser_id?: string;
user_id?: string;
consume_date?: string;
[key: string]: any;
}
const mockConsumptionRecords: ConsumptionRecord[] = [
{ id: 'C001', star_id: 'ST001', demand_id: 'DM001', demand_name: '产品推广任务', demander_name: 'XX品牌方', brand: 'XX品牌', max_publish_count: 10, product_name: 'XX产品', product_information: '产品信息描述' },
{ id: 'C002', star_id: 'ST002', demand_id: 'DM002', demand_name: '新品发布任务', demander_name: 'YY品牌方', brand: 'YY品牌', max_publish_count: 5, product_name: 'YY产品', product_information: '新品信息描述' },
{ id: 'C003', star_id: 'ST003', demand_id: 'DM003', demand_name: '品牌宣传任务', demander_name: 'ZZ品牌方', brand: 'ZZ品牌', max_publish_count: 8, product_name: 'ZZ产品', product_information: '品牌信息描述' },
{ id: 'C004', star_id: 'ST001', demand_id: 'DM004', demand_name: '活动促销任务', demander_name: 'XX品牌方', brand: 'XX品牌', max_publish_count: 15, product_name: 'XX活动产品', product_information: '活动产品信息' },
{ id: 'C005', star_id: 'ST004', demand_id: 'DM005', demand_name: '节日营销任务', demander_name: 'AA品牌方', brand: 'AA品牌', max_publish_count: 20, product_name: 'AA节日产品', product_information: '节日产品信息' },
];
const ConsumePage: React.FC = () => {
const navigate = useNavigate();
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>(mockConsumptionRecords);
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [columnsData, setColumnsData] = useState<{ name: string; description: string }[]>([]);
const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
const [searchText, setSearchText] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [advertiserId, setAdvertiserId] = useState('');
const [userId, setUserId] = useState('');
const [consumeDateRange, setConsumeDateRange] = useState<[string, string] | undefined>();
useEffect(() => {
setLoading(true);
setTimeout(() => {
setConsumptionRecords(mockConsumptionRecords);
setLoading(false);
}, 500);
loadColumns();
loadData();
}, [page, pageSize]);
setTimeout(() => {
setColumnsData(mockColumnsData);
const loadData = async () => {
setLoading(true);
try {
const res = await getMaterialConsumpList({
advertiser_id: advertiserId || undefined,
user_id: userId || undefined,
consume_date: consumeDateRange,
page,
page_size: pageSize,
});
console.log(res);
setConsumptionRecords(res.data || []);
setTotal(res.pagination?.total || 0);
} catch (e: any) {
message.error('加载数据失败');
} finally {
setLoading(false);
}
};
const loadColumns = async () => {
try {
const res = await getFields();
const fields = res.data || [];
const formattedFields = fields.map((item: { field: string; description: string }) => ({
name: toCamelCase(item.field),
description: item.description,
}));
setColumnsData(formattedFields);
const saved = localStorage.getItem('consumeColumns');
if (saved) {
setSelectedColumns(JSON.parse(saved));
} else {
setSelectedColumns(mockColumnsData.map(item => item.name));
setSelectedColumns(formattedFields.map((item: { name: string }) => item.name));
}
}, 300);
}, []);
} catch (e: any) {
message.error('加载表头字段失败');
}
};
const tableData = consumptionRecords.map((item, index) => ({
...item,
@@ -73,8 +87,22 @@ const ConsumePage: React.FC = () => {
navigate('/authorization');
};
const dynamicColumns = columnsData
.filter(col => selectedColumns.includes(col.name))
const handleSearch = () => {
setPage(1);
loadData();
};
const handleReset = () => {
setAdvertiserId('');
setUserId('');
setConsumeDateRange(undefined);
setPage(1);
loadData();
};
const dynamicColumns = selectedColumns
.map(colName => columnsData.find(col => col.name === colName))
.filter((col): col is { name: string; description: string } => !!col)
.map(col => ({
title: col.description,
dataIndex: col.name,
@@ -84,15 +112,44 @@ const ConsumePage: React.FC = () => {
return (
<div style={{ minHeight: '94vh' }}>
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Space>
<Button type="text" icon={<ArrowLeftOutlined />} onClick={handleBack} />
{/* <Button icon={<ArrowLeftOutlined />} onClick={handleBack}>返回</Button> */}
<DollarOutlined style={{ fontSize: 18, color: '#6366f1' }} />
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</Space>
<Space>
<Button icon={<SettingOutlined />} onClick={() => setShowModal(true)}></Button>
<Button icon={<ArrowLeftOutlined />} onClick={handleBack}></Button>
</Space>
</div>
<div style={{ marginBottom: 16 }}>
<Space wrap>
<Input
placeholder="广告主ID"
value={advertiserId}
onChange={(e) => setAdvertiserId(e.target.value)}
style={{ width: 200 }}
/>
<Input
placeholder="用户ID"
value={userId}
onChange={(e) => setUserId(e.target.value)}
style={{ width: 200 }}
/>
<RangePicker
value={consumeDateRange ? [dayjs(consumeDateRange[0]), dayjs(consumeDateRange[1])] : undefined}
onChange={(dates) => {
if (dates && dates[0] && dates[1]) {
setConsumeDateRange([dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD')]);
} else {
setConsumeDateRange(undefined);
}
}}
/>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}></Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}></Button>
</Space>
</div>
<Table
@@ -103,15 +160,19 @@ const ConsumePage: React.FC = () => {
bordered={false}
scroll={{ x: 'max-content' }}
pagination={{
pageSize: 10,
total: consumptionRecords.length,
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `${t} 条记录`,
size: 'small',
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
}}
/>
</Card>
<Modal
title="自定义表头"
open={showModal}
@@ -127,13 +188,13 @@ const ConsumePage: React.FC = () => {
okText="确定"
cancelText="取消"
width={800}
bodyStyle={{ padding: 16 }}
>
<div style={{ display: 'flex', gap: 20, height: 400 }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Input
placeholder="搜索指标..."
value={searchText}
allowClear={true}
onChange={(e) => setSearchText(e.target.value)}
style={{ marginBottom: 8 }}
/>
@@ -197,11 +258,21 @@ const ConsumePage: React.FC = () => {
</Button>
<Button
size="small"
onClick={() => {
message.info('更新表头字段');
setTimeout(() => {
setColumnsData(mockColumnsData);
}, 300);
onClick={async () => {
try {
const res = await getFields();
const fields = res.data || [];
const formattedFields = fields.map((item: { field: string; description: string }) => ({
name: toCamelCase(item.field),
description: item.description,
}));
setColumnsData(formattedFields);
setSelectedColumns(formattedFields.map((item: { name: string }) => item.name));
localStorage.removeItem('consumeColumns');
message.success('表头字段已更新');
} catch (e: any) {
message.error('更新表头字段失败');
}
}}
>
+291 -71
View File
@@ -1,43 +1,49 @@
import React, { useEffect, useState } from 'react';
import {
Button, Card, Space, Table, Tag, Typography, message, Modal, Input, Upload,
Button, Card, Space, Table, Tag, Typography, message, Modal, Form, Input, InputNumber, Upload,
} from 'antd';
import {
HistoryOutlined, ReloadOutlined, PlusOutlined, UploadOutlined,
HistoryOutlined, ReloadOutlined, PlusOutlined, UploadOutlined, EyeOutlined, EditOutlined, DeleteOutlined,
} from '@ant-design/icons';
import { getOperationLogs } from '../api';
import { getOpenTypeList, getOpenType, createOpenType, updateOpenType, deleteOpenType, uploadImage } from '../api';
import { formatDate } from '../utils/formatDate';
interface OperationLog {
interface OpenType {
id: string;
userId: string;
username: string;
action: string;
method: string;
path: string;
detail?: string;
ip?: string;
open_type: number;
type_name: string;
description: string;
thumb?: string;
createdAt: string;
updatedAt: string;
}
const METHOD_COLORS: Record<string, string> = { POST: 'green', PUT: 'blue', DELETE: 'red' };
const AdminPlatform: React.FC = () => {
const [logs, setLogs] = useState<OperationLog[]>([]);
const [openTypes, setOpenTypes] = useState<OpenType[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [showModal, setShowModal] = useState(false);
const [formData, setFormData] = useState({ title: '', description: '', image: '' });
const [pageSize, setPageSize] = useState(20);
const load = async (p?: number) => {
const [createModalVisible, setCreateModalVisible] = useState(false);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [updateModalVisible, setUpdateModalVisible] = useState(false);
const [currentOpenType, setCurrentOpenType] = useState<OpenType | null>(null);
const [createForm] = Form.useForm();
const [updateForm] = Form.useForm();
const load = async (p?: number, ps?: number) => {
setLoading(true);
try {
const res = await getOperationLogs(p || page);
setLogs(res.items || []);
const res = await getOpenTypeList({
page: p || page,
page_size: ps || pageSize,
});
setOpenTypes(res.items || []);
setTotal(res.total || 0);
} catch {
message.error('加载平台管理失败');
message.error('加载开户方式列表失败');
} finally {
setLoading(false);
}
@@ -45,100 +51,227 @@ const AdminPlatform: React.FC = () => {
useEffect(() => { load(); }, []);
const handleCreate = async () => {
try {
const values = await createForm.validateFields();
console.log(values);
await createOpenType({
open_type: values.open_type,
type_name: values.type_name,
description: values.description,
thumb: values.thumb,
});
message.success('创建成功');
setCreateModalVisible(false);
createForm.resetFields();
load();
} catch (e: any) {
message.error(e?.message || '创建失败');
}
};
const handleDetail = async (id: string) => {
try {
const openType = await getOpenType(id);
setCurrentOpenType(openType);
setDetailModalVisible(true);
} catch (e: any) {
message.error(e?.message || '获取详情失败');
}
};
const handleUpdate = async (id: string) => {
try {
const openType = await getOpenType(id);
setCurrentOpenType(openType);
updateForm.setFieldsValue({
open_type: openType.open_type,
type_name: openType.type_name,
description: openType.description,
thumb: openType.thumb,
});
setUpdateModalVisible(true);
} catch (e: any) {
message.error(e?.message || '获取详情失败');
}
};
const handleSaveUpdate = async () => {
if (!currentOpenType) return;
try {
const values = await updateForm.validateFields();
await updateOpenType(currentOpenType.id, {
open_type: values.open_type,
type_name: values.type_name,
description: values.description,
thumb: values.thumb,
});
message.success('更新成功');
setUpdateModalVisible(false);
updateForm.resetFields();
setCurrentOpenType(null);
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: '操作人', dataIndex: 'username', width: 120,
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
},
{
title: '操作', dataIndex: 'action', width: 160,
title: 'ID', dataIndex: 'id', width: 100,
render: (v: string) => <Typography.Text>{v}</Typography.Text>,
},
{
title: '方法', dataIndex: 'method', width: 80,
render: (v: string) => <Tag color={METHOD_COLORS[v] || 'default'}>{v}</Tag>,
title: '开户类型', dataIndex: 'open_type', width: 100,
render: (v: number) => <Typography.Text>{v}</Typography.Text>,
},
{
title: '路径', dataIndex: 'path', width: 220, ellipsis: true,
title: '类型名称', dataIndex: 'type_name', 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: 'createdAt', width: 160,
title: '缩略图', dataIndex: 'thumb', width: 120,
render: (v: string) => v ? <img src={v} 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: 200,
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 bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<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>
<Typography.Text strong style={{ fontSize: 16 }}></Typography.Text>
</Space>
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}></Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}></Button>
<Button icon={<ReloadOutlined />} onClick={() => load()}></Button>
</Space>
</div>
<Table
columns={columns}
dataSource={logs}
dataSource={openTypes}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: 20,
pageSize,
total,
showTotal: (t) => `${t} 条记录`,
onChange: (p) => { setPage(p); load(p); },
onChange: (p, ps) => { setPage(p); setPageSize(ps); load(p, ps); },
}}
scroll={{ x: 800 }}
scroll={{ x: 1000 }}
/>
</Card>
<Modal
title="新增平台"
open={showModal}
onOk={() => {
message.success('新增成功');
setShowModal(false);
setFormData({ title: '', description: '', image: '' });
load();
}}
title="新增开户方式"
open={createModalVisible}
onOk={handleCreate}
onCancel={() => {
setShowModal(false);
setFormData({ title: '', description: '', image: '' });
setCreateModalVisible(false);
createForm.resetFields();
}}
okText="确认"
okText="创建"
cancelText="取消"
width={600}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<Typography.Text strong style={{ marginBottom: 8, display: 'block' }}></Typography.Text>
<Input
placeholder="请输入标题"
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
/>
<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>
<div>
<Typography.Text strong style={{ marginBottom: 8, display: 'block' }}></Typography.Text>
<Input.TextArea
placeholder="请输入描述"
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
rows={4}
/>
</div>
<div>
<Typography.Text strong style={{ marginBottom: 8, display: 'block' }}></Typography.Text>
<Form.Item
name="description"
label="描述"
rules={[{ required: true, message: '请输入描述' }]}
>
<Input.TextArea placeholder="请输入描述" rows={4} />
</Form.Item>
<Form.Item
name="thumb"
label="缩略图"
>
<Upload
action="/api/upload"
listType="picture-card"
onChange={(info) => {
if (info.file.status === 'done') {
setFormData(prev => ({ ...prev, image: info.file.response?.url || '' }));
customRequest={async ({ file, onSuccess, onError }) => {
try {
const res = await uploadImage(file as File);
console.log(res);
createForm.setFieldsValue({ thumb: res.url });
onSuccess(res);
} catch (e: any) {
onError(e);
}
}}
>
@@ -147,8 +280,95 @@ const AdminPlatform: React.FC = () => {
<div style={{ marginTop: 8 }}></div>
</div>
</Upload>
</Form.Item>
</Form>
</Modal>
<Modal
title="开户方式详情"
open={detailModalVisible}
onCancel={() => {
setDetailModalVisible(false);
setCurrentOpenType(null);
}}
okText="关闭"
cancelText="取消"
width={600}
>
{currentOpenType && (
<div style={{ lineHeight: '2' }}>
<p><strong>ID:</strong> {currentOpenType.id}</p>
<p><strong>:</strong> {currentOpenType.open_type}</p>
<p><strong>:</strong> {currentOpenType.type_name}</p>
<p><strong>:</strong> {currentOpenType.description}</p>
<p><strong>:</strong> {currentOpenType.thumb ? <img src={currentOpenType.thumb} alt="thumb" style={{ width: 120, height: 80, objectFit: 'cover' }} /> : '-'}</p>
<p><strong>:</strong> {formatDate(currentOpenType.createdAt)}</p>
<p><strong>:</strong> {formatDate(currentOpenType.updatedAt)}</p>
</div>
</div>
)}
</Modal>
<Modal
title="更新开户方式"
open={updateModalVisible}
onOk={handleSaveUpdate}
onCancel={() => {
setUpdateModalVisible(false);
updateForm.resetFields();
setCurrentOpenType(null);
}}
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
name="thumb"
label="缩略图"
>
<Upload
listType="picture-card"
defaultFileList={currentOpenType?.thumb ? [{ uid: '1', name: 'thumb', status: 'done', url: currentOpenType.thumb }] : []}
customRequest={async ({ file, onSuccess, onError }) => {
try {
const res = await uploadImage(file);
updateForm.setFieldsValue({ thumb: res.url });
onSuccess(res);
} catch (e: any) {
onError(e);
}
}}
>
{!updateForm.getFieldValue('thumb') && (
<div>
<UploadOutlined style={{ fontSize: 24, color: '#999' }} />
<div style={{ marginTop: 8 }}></div>
</div>
)}
</Upload>
</Form.Item>
</Form>
</Modal>
</div>
);