对接素材列表解决
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
VITE_API_BASE=http://192.168.120.17:8000
|
||||
# VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||
# VITE_API_BASE=http://192.168.120.17:8000
|
||||
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||
VITE_USE_MOCK=false
|
||||
# Encryption disabled for dev — enable in production
|
||||
VITE_ENCRYPTION_KEY=
|
||||
@@ -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';
|
||||
|
||||
@@ -403,6 +403,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?: {
|
||||
@@ -566,3 +609,17 @@ 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 };
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
VITE_API_BASE=http://192.168.120.17:8000
|
||||
# VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||
# VITE_API_BASE=http://192.168.120.17:8000
|
||||
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||
VITE_USE_MOCK=false
|
||||
# Encryption disabled for dev — enable in production
|
||||
VITE_ENCRYPTION_KEY=
|
||||
|
||||
+425
File diff suppressed because one or more lines are too long
@@ -19,6 +19,7 @@ import RemoveLens from './pages/RemoveLens';
|
||||
import GeneratedRecord from './pages/GeneratedRecord';
|
||||
import PreTest from './pages/PreTest';
|
||||
import AuthorizationPage from './pages/AuthorizationPage';
|
||||
import MaterialListPage from './pages/MaterialListPage';
|
||||
import RemoveInfo from './pages/RemoveInfo';
|
||||
import RemoveRw from './pages/RemoveRw';
|
||||
// import RemoveFenbu from './pages/RemoveFenbu';
|
||||
@@ -108,6 +109,7 @@ const App = () => {
|
||||
<Route path="generated" element={<GeneratedRecord />} />
|
||||
<Route path="pretest" element={<PreTest />} />
|
||||
<Route path="authorization" element={<AuthorizationPage />} />
|
||||
<Route path="materials" element={<MaterialListPage />} />
|
||||
<Route path="consume" element={<ConsumePage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/projects" replace />} />
|
||||
|
||||
@@ -616,3 +616,53 @@ export async function getUploadHistory(params: UploadHistoryParams): Promise<any
|
||||
const query = searchParams.toString();
|
||||
return api.get(`/upload-material/upload-history${query ? `?${query}` : ''}`);
|
||||
}
|
||||
|
||||
export interface UploadFilenames {
|
||||
source_id: string;
|
||||
file_name: string;
|
||||
}
|
||||
export interface UpdateFilenameParams {
|
||||
filenames: UploadFilenames[];
|
||||
}
|
||||
// 上传文件名
|
||||
export async function updateFilename(params: UpdateFilenameParams): Promise<any> {
|
||||
return api.post('/upload-material/batch-update-filename', params);
|
||||
}
|
||||
|
||||
// 查询素材消耗列表
|
||||
export interface MaterialConsumpListParams {
|
||||
advertiser_id?: string;
|
||||
consume_date?: [string, string];
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
export async function getMaterialConsumpList(params: MaterialConsumpListParams): Promise<any> {
|
||||
const query = new URLSearchParams();
|
||||
if (params.advertiser_id) query.set('advertiser_id', params.advertiser_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/list?${query.toString()}`);
|
||||
}
|
||||
|
||||
// 查询素材消耗列表
|
||||
export interface ResourcesMaterialListParams {
|
||||
advertiser_id?: string;
|
||||
material_id?: string;
|
||||
upload_id?: string;
|
||||
file_name?: string;
|
||||
resource_type?: string; // image或者video
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
export async function getResourcesMaterialList(params: ResourcesMaterialListParams): Promise<any> {
|
||||
const query = new URLSearchParams();
|
||||
if (params.advertiser_id) query.set('advertiser_id', params.advertiser_id);
|
||||
if (params.material_id) query.set('material_id', params.material_id);
|
||||
if (params.upload_id) query.set('upload_id', params.upload_id);
|
||||
if (params.file_name) query.set('file_name', params.file_name);
|
||||
if (params.resource_type) query.set('resource_type', params.resource_type);
|
||||
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(`/resources-material/list?${query.toString()}`);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Table, Tag, Modal, Select, App, Input, Pagination, Typography } from 'antd';
|
||||
import { PlusOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { getOAuthList, juliang_callback, requestOAuth } from '../api';
|
||||
import { getOAuthList, requestOAuth } from '../api';
|
||||
|
||||
const OPEN_TYPE_MAP: Record<number, string> = {
|
||||
1: '千川',
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Tag, Button, Pagination, Typography } from 'antd';
|
||||
import { Table, Tag, Button, Pagination, Typography, DatePicker, App, Input } from 'antd';
|
||||
import { ArrowLeftOutlined, DollarOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getMaterialConsumpList } from '../api';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
// 消耗类型配置
|
||||
const consumptionTypeConfig: Record<string, { label: string; color: string }> = {
|
||||
video: { label: '视频生成', color: 'blue' },
|
||||
audio: { label: '音频转换', color: 'purple' },
|
||||
image: { label: '图片处理', color: 'green' },
|
||||
};
|
||||
|
||||
// 消耗记录数据类型
|
||||
interface ConsumptionRecord {
|
||||
id: string;
|
||||
authorizationId: string;
|
||||
@@ -13,62 +22,110 @@ interface ConsumptionRecord {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 模拟消耗记录数据
|
||||
const mockConsumptionRecords: ConsumptionRecord[] = [
|
||||
{ id: 'C001', authorizationId: '1867060028363785', amount: 100, type: 'video', description: '视频生成消耗', createdAt: '2024-01-15 10:30:00' },
|
||||
{ id: 'C002', authorizationId: '1867060028363785', amount: 50, type: 'audio', description: '音频转换消耗', createdAt: '2024-01-15 11:20:00' },
|
||||
{ id: 'C003', authorizationId: '1867059808785418', amount: 200, type: 'video', description: '视频生成消耗', createdAt: '2024-01-14 14:45:00' },
|
||||
{ id: 'C004', authorizationId: '1867060028363785', amount: 75, type: 'image', description: '图片处理消耗', createdAt: '2024-01-14 09:15:00' },
|
||||
{ id: 'C005', authorizationId: '1867059757929740', amount: 150, type: 'video', description: '视频生成消耗', createdAt: '2024-01-13 16:00:00' },
|
||||
];
|
||||
|
||||
// 消耗类型配置
|
||||
const consumptionTypeConfig = {
|
||||
video: { label: '视频生成', color: 'blue' },
|
||||
audio: { label: '音频转换', color: 'purple' },
|
||||
image: { label: '图片处理', color: 'green' },
|
||||
};
|
||||
|
||||
// 表头配置
|
||||
const columns = [
|
||||
{ title: '序号', dataIndex: 'index', key: 'index', width: 80, fixed: 'left' as const, render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span> },
|
||||
{ title: '消耗ID', dataIndex: 'id', key: 'id', ellipsis: true, render: (text: string) => <span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span> },
|
||||
{ title: '授权ID', dataIndex: 'authorizationId', key: 'authorizationId', ellipsis: true },
|
||||
{
|
||||
title: '消耗类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120,
|
||||
render: (text: string) => {
|
||||
const config = consumptionTypeConfig[text as keyof typeof consumptionTypeConfig];
|
||||
return <Tag color={config?.color}>{config?.label}</Tag>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '消耗金额',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
width: 120,
|
||||
render: (text: number) => <span style={{ color: '#ef4444', fontWeight: 500 }}>{text} 元</span>
|
||||
},
|
||||
{ title: '消耗描述', dataIndex: 'description', key: 'description', ellipsis: true },
|
||||
{ title: '消耗时间', dataIndex: 'createdAt', key: 'createdAt', width: 160, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const ConsumePage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>(mockConsumptionRecords);
|
||||
const { message } = App.useApp();
|
||||
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [listLoading, setListLoading] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
advertiser_id: '',
|
||||
consume_date: undefined as [string, string] | undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
// 模拟异步获取数据
|
||||
setTimeout(() => {
|
||||
setConsumptionRecords(mockConsumptionRecords);
|
||||
setLoading(false);
|
||||
}, 500);
|
||||
loadConsumptionList();
|
||||
}, []);
|
||||
|
||||
const loadConsumptionList = async (page = 1, pageSizeNum = 10, params = searchParams) => {
|
||||
setListLoading(true);
|
||||
try {
|
||||
const response = await getMaterialConsumpList({
|
||||
page,
|
||||
page_size: pageSizeNum,
|
||||
advertiser_id: params.advertiser_id || undefined,
|
||||
consume_date: params.consume_date,
|
||||
});
|
||||
if (response) {
|
||||
if (response.data) {
|
||||
setConsumptionRecords(response.data.data || response.data);
|
||||
setTotal(response.pagination?.total || 0);
|
||||
setCurrentPage(response.pagination?.page || 1);
|
||||
setPageSize(response.pagination?.pageSize || 10);
|
||||
} else {
|
||||
setConsumptionRecords(response.data || response || []);
|
||||
setTotal(Array.isArray(response) ? response.length : 0);
|
||||
}
|
||||
} else {
|
||||
setConsumptionRecords([]);
|
||||
setTotal(0);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取消耗列表失败');
|
||||
console.error('获取消耗列表失败:', error);
|
||||
} finally {
|
||||
setListLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 表头配置
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
key: 'index',
|
||||
width: 80,
|
||||
fixed: 'left' as const,
|
||||
render: (text: number) => <span style={{ color: '#94a3b8' }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: '消耗ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ fontWeight: 500, color: '#1e293b' }}>{text}</span>,
|
||||
},
|
||||
{
|
||||
title: '授权ID',
|
||||
dataIndex: 'authorizationId',
|
||||
key: 'authorizationId',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '消耗类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120,
|
||||
render: (text: string) => {
|
||||
const config = consumptionTypeConfig[text];
|
||||
return <Tag color={config?.color}>{config?.label || text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '消耗金额',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
width: 120,
|
||||
render: (text: number) => <span style={{ color: '#ef4444', fontWeight: 500 }}>{text} 元</span>,
|
||||
},
|
||||
{
|
||||
title: '消耗描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '消耗时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = consumptionRecords.map((item, index) => ({
|
||||
...item,
|
||||
index: index + 1,
|
||||
@@ -83,16 +140,54 @@ const ConsumePage: React.FC = () => {
|
||||
<div style={{ minHeight: '94vh' }}>
|
||||
{/* 页面标题 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={handleBack} />
|
||||
<DollarOutlined style={{ color: '#6366f1', fontSize: 16 }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>消耗记录</Typography.Text>
|
||||
</div>
|
||||
|
||||
{/* 搜索筛选 */}
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="授权ID"
|
||||
value={searchParams.advertiser_id}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
|
||||
style={{ width: 200 }}
|
||||
onPressEnter={() => { setCurrentPage(1); loadConsumptionList(1, pageSize); }}
|
||||
/>
|
||||
<RangePicker
|
||||
value={searchParams.consume_date ? [undefined, undefined] : undefined}
|
||||
onChange={(dates, dateStrings) => {
|
||||
if (dates && dateStrings[0] && dateStrings[1]) {
|
||||
setSearchParams(prev => ({ ...prev, consume_date: [dateStrings[0], dateStrings[1]] }));
|
||||
} else {
|
||||
setSearchParams(prev => ({ ...prev, consume_date: undefined }));
|
||||
}
|
||||
}}
|
||||
style={{ width: 280 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => { setCurrentPage(1); loadConsumptionList(1, pageSize); }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSearchParams({ advertiser_id: '', consume_date: undefined });
|
||||
setCurrentPage(1);
|
||||
loadConsumptionList(1, pageSize);
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
|
||||
<Table
|
||||
dataSource={tableData}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
loading={listLoading}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
bordered={false}
|
||||
@@ -100,10 +195,16 @@ const ConsumePage: React.FC = () => {
|
||||
/>
|
||||
<div style={{ padding: '16px', textAlign: 'right' }}>
|
||||
<Pagination
|
||||
pageSize={10}
|
||||
total={consumptionRecords.length}
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
showSizeChanger
|
||||
showTotal={(total) => `共 ${total} 条记录`}
|
||||
onChange={(page, size) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
loadConsumptionList(page, size);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
@@ -112,4 +213,4 @@ const ConsumePage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsumePage;
|
||||
export default ConsumePage;
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress, Table, DatePicker } from 'antd';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
VideoCameraOutlined,
|
||||
PictureOutlined,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
UploadOutlined,
|
||||
|
||||
} from '@ant-design/icons';
|
||||
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, getUploadHistory } from '../api';
|
||||
import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory } from '../api';
|
||||
|
||||
const { Search } = Input;
|
||||
const { Text } = Typography;
|
||||
@@ -48,16 +47,13 @@ const GeneratedRecord: React.FC = () => {
|
||||
const [oauthLoading, setOauthLoading] = useState(false);
|
||||
const [oauthTotal, setOauthTotal] = useState(0);
|
||||
const [selectedOauthItems, setSelectedOauthItems] = useState<{ value: string; label: string } | undefined>(undefined);
|
||||
const [materialFileNames, setMaterialFileNames] = useState<Map<string, string>>(new Map());
|
||||
const [unifiedFileName, setUnifiedFileName] = useState('');
|
||||
const updateFilenameDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [oauthPage, setOauthPage] = useState(1);
|
||||
const [oauthPageSize, setOauthPageSize] = useState(10);
|
||||
const [oauthSelectOpen, setOauthSelectOpen] = useState(false);
|
||||
|
||||
const [batchUploadProgress, setBatchUploadProgress] = useState<{
|
||||
itemId: string;
|
||||
status: 'pending' | 'uploading' | 'success' | 'error';
|
||||
message: string;
|
||||
}[]>([]);
|
||||
|
||||
// 上传任务历史弹窗相关状态
|
||||
const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false);
|
||||
const [uploadHistoryList, setUploadHistoryList] = useState<any[]>([]);
|
||||
@@ -501,7 +497,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleSelect?.(item.id);
|
||||
onToggleSelect?.(item.generatedResourceId || item.id);
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1.1)';
|
||||
@@ -571,6 +567,16 @@ const GeneratedRecord: React.FC = () => {
|
||||
setPreviewVisible(false);
|
||||
};
|
||||
|
||||
// 获取item的资源ID(优先使用generatedResourceId,否则使用id)
|
||||
const getItemResourceId = (item: any): string => {
|
||||
return item.generatedResourceId || item.id;
|
||||
};
|
||||
|
||||
// 判断item是否有generatedResourceId
|
||||
const hasGeneratedResourceId = (item: any): boolean => {
|
||||
return Boolean(item.generatedResourceId);
|
||||
};
|
||||
|
||||
// 多选相关函数
|
||||
const handleToggleSelect = (itemId: string) => {
|
||||
setSelectedItems(prev => {
|
||||
@@ -586,7 +592,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const allItemIds = recordlist.flatMap((group: any) =>
|
||||
group.items.map((item: any) => item.id)
|
||||
group.items.map((item: any) => getItemResourceId(item))
|
||||
);
|
||||
if (selectedItems.size === allItemIds.length) {
|
||||
setSelectedItems(new Set());
|
||||
@@ -602,7 +608,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
}
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setBatchUploadProgress([]);
|
||||
setUploadConfigModalVisible(true);
|
||||
};
|
||||
|
||||
@@ -660,6 +665,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
loadUploadHistory();
|
||||
};
|
||||
|
||||
// 批量上传素材
|
||||
const handleStartBatchUpload = async () => {
|
||||
if (!selectedOauthItems) {
|
||||
message.warning('请先选择授权账户');
|
||||
@@ -675,23 +681,46 @@ const GeneratedRecord: React.FC = () => {
|
||||
advertiser_ids: string[];
|
||||
resource_ids: string[];
|
||||
oauth_id: string;
|
||||
type: string;
|
||||
source_model: string;
|
||||
}[] = [];
|
||||
const advertiserIds = accountIdList.map(account => account.accountId);
|
||||
const type = filterType === 'project' ? 'generation_record' : 'chat_task';
|
||||
// 创建itemId到item对象的映射
|
||||
const itemMap = new Map<string, any>();
|
||||
recordlist.forEach((group: any) => {
|
||||
group.items.forEach((item: any) => {
|
||||
const resourceId = getItemResourceId(item);
|
||||
itemMap.set(resourceId, item);
|
||||
});
|
||||
});
|
||||
|
||||
for (const itemId of selectedItems) {
|
||||
const item = itemMap.get(itemId);
|
||||
// 根据item是否有generatedResourceId来决定source_model
|
||||
let sourceModel: string;
|
||||
if (item && hasGeneratedResourceId(item)) {
|
||||
sourceModel = 'generated_resources';
|
||||
} else {
|
||||
sourceModel = filterType === 'project' ? 'generation_records' : 'chat_generation_tasks';
|
||||
}
|
||||
|
||||
tasks.push({
|
||||
advertiser_ids: advertiserIds,
|
||||
resource_ids: [itemId],
|
||||
oauth_id: selectedOauthItems.value,
|
||||
type,
|
||||
source_model: sourceModel,
|
||||
});
|
||||
}
|
||||
// console.log(tasks);
|
||||
await asyncBatchUploadMaterial({ tasks });
|
||||
message.success(`已提交 ${tasks.length} 个上传任务,后台异步处理中`);
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
// 关闭弹窗并清理状态
|
||||
setUploadConfigModalVisible(false);
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setSelectedOauthItems(undefined);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
} catch (error: any) {
|
||||
console.error('批量上传失败:', error);
|
||||
message.error(error.message || '批量上传失败');
|
||||
@@ -700,6 +729,73 @@ const GeneratedRecord: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 更新文件名函数
|
||||
const handleUpdateFileName = async (sourceId: string, newFileName: string) => {
|
||||
if (!newFileName.trim()) return;
|
||||
try {
|
||||
const response = await updateFilename({
|
||||
filenames: [{ source_id: sourceId, file_name: newFileName }],
|
||||
});
|
||||
// 更新 recordlist 中的文件名,使用 API 返回的 new_file_name
|
||||
const result = response?.results?.find((r: any) => r.source_id === sourceId);
|
||||
const actualFileName = result?.new_file_name || newFileName;
|
||||
setRecordList(prevList => {
|
||||
return prevList.map(group => ({
|
||||
...group,
|
||||
items: group.items.map((item: any) => {
|
||||
const resourceId = getItemResourceId(item);
|
||||
if (resourceId === sourceId) {
|
||||
return { ...item, fileName: actualFileName };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
}));
|
||||
});
|
||||
message.success('文件名更新成功');
|
||||
} catch (error: any) {
|
||||
console.error('文件名更新失败:', error);
|
||||
message.error(error.message || '文件名更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 批量更新文件名函数
|
||||
const handleBatchUpdateFileName = async (sourceIds: string[], newFileName: string) => {
|
||||
if (!newFileName.trim() || sourceIds.length === 0) return;
|
||||
try {
|
||||
const filenames = sourceIds.map(sourceId => ({
|
||||
source_id: sourceId,
|
||||
file_name: newFileName,
|
||||
}));
|
||||
const response = await updateFilename({ filenames });
|
||||
// 批量更新 recordlist 中的文件名,使用 API 返回的 new_file_name
|
||||
const resultsMap = new Map<string, string>();
|
||||
response?.results?.forEach((r: any) => {
|
||||
if (r.success && r.new_file_name) {
|
||||
resultsMap.set(r.source_id, r.new_file_name);
|
||||
}
|
||||
});
|
||||
setRecordList(prevList => {
|
||||
const sourceIdSet = new Set(sourceIds);
|
||||
return prevList.map(group => ({
|
||||
...group,
|
||||
items: group.items.map((item: any) => {
|
||||
const resourceId = getItemResourceId(item);
|
||||
if (sourceIdSet.has(resourceId)) {
|
||||
const actualFileName = resultsMap.get(resourceId) || newFileName;
|
||||
return { ...item, fileName: actualFileName };
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
}));
|
||||
});
|
||||
const successCount = response?.success_count || 0;
|
||||
message.success(`已更新 ${successCount} 个文件名`);
|
||||
} catch (error: any) {
|
||||
console.error('文件名更新失败:', error);
|
||||
message.error(error.message || '文件名更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 日期选择器变化处理函数
|
||||
const handleDateChange = (dateString: string) => {
|
||||
setSelectedDate(dateString);
|
||||
@@ -891,7 +987,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
borderRadius: 8,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#64748b',
|
||||
color: '#222222ff',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
@@ -919,8 +1015,9 @@ const GeneratedRecord: React.FC = () => {
|
||||
disabled={uploading || selectedItems.size === 0}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #10b981, #059669)',
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
@@ -934,7 +1031,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
onClick={() => setIsSelectionMode(true)}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #10b981, #059669)',
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
@@ -949,6 +1046,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
{/* Second row filter: 视频 / 图片 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 24,
|
||||
@@ -1017,7 +1115,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
onClick={handleOpenUploadHistory}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
fontWeight: 600,
|
||||
@@ -1066,11 +1164,11 @@ const GeneratedRecord: React.FC = () => {
|
||||
}}>
|
||||
{group.items.map((item: any) => (
|
||||
<LazyMedia
|
||||
key={item.id}
|
||||
key={getItemResourceId(item)}
|
||||
item={item}
|
||||
mediaType={filterMedia}
|
||||
onClick={() => isSelectionMode ? handleToggleSelect(item.id) : handlePreview(item)}
|
||||
isSelected={selectedItems.has(item.id)}
|
||||
onClick={() => isSelectionMode ? handleToggleSelect(getItemResourceId(item)) : handlePreview(item)}
|
||||
isSelected={selectedItems.has(getItemResourceId(item))}
|
||||
onToggleSelect={handleToggleSelect}
|
||||
isSelectionMode={isSelectionMode}
|
||||
/>
|
||||
@@ -1129,12 +1227,133 @@ const GeneratedRecord: React.FC = () => {
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setSelectedOauthItems(undefined);
|
||||
setBatchUploadProgress([]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
}}
|
||||
footer={null}
|
||||
width={900}
|
||||
mask={{ closable: false }}
|
||||
>
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block' }}>
|
||||
选中素材 ({selectedItems.size}个)
|
||||
</Typography.Text>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
marginBottom: 12,
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b' }}>统一修改名称:</Typography.Text>
|
||||
<Input
|
||||
value={unifiedFileName}
|
||||
onChange={(e) => setUnifiedFileName(e.target.value)}
|
||||
placeholder="输入名称后点击应用"
|
||||
style={{ flex: 1, borderRadius: 8 }}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (unifiedFileName.trim() && selectedItems.size > 0) {
|
||||
handleBatchUpdateFileName(Array.from(selectedItems), unifiedFileName);
|
||||
}
|
||||
}}
|
||||
disabled={!unifiedFileName.trim() || selectedItems.size === 0}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
应用
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{
|
||||
maxHeight: 300,
|
||||
overflow: 'auto',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
}}>
|
||||
{(() => {
|
||||
const itemMap = new Map<string, any>();
|
||||
recordlist.forEach((group: any) => {
|
||||
group.items.forEach((item: any) => {
|
||||
const resourceId = getItemResourceId(item);
|
||||
itemMap.set(resourceId, item);
|
||||
});
|
||||
});
|
||||
return Array.from(selectedItems).map((itemId) => {
|
||||
const item = itemMap.get(itemId);
|
||||
return (
|
||||
<div
|
||||
key={itemId}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '8px 0',
|
||||
borderBottom: '1px solid #f5f5f5',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 60,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#f5f5f5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{(() => {
|
||||
const coverField = filterMedia === 'video' ? item?.videoCoverUrl : item?.imageUrl;
|
||||
if (!coverField) {
|
||||
return <Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>预览</Typography.Text>;
|
||||
}
|
||||
const baseUrl = import.meta.env.VITE_API_BASE || "http://localhost:8000";
|
||||
const cleanPath = coverField.startsWith('/') ? coverField.slice(1) : coverField;
|
||||
const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
||||
const coverUrl = `${cleanBase}/static/${cleanPath}&w=300&q=50`;
|
||||
return (
|
||||
<img
|
||||
src={coverUrl}
|
||||
alt=""
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#1e293b' }}>
|
||||
{item?.fileName || `素材 ${item.id}`}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
value={materialFileNames.get(itemId) || item?.fileName || ''}
|
||||
onChange={(e) => {
|
||||
const newName = e.target.value;
|
||||
const newNames = new Map(materialFileNames);
|
||||
newNames.set(itemId, newName);
|
||||
setMaterialFileNames(newNames);
|
||||
// 防抖调用API
|
||||
if (updateFilenameDebounceRef.current) {
|
||||
clearTimeout(updateFilenameDebounceRef.current);
|
||||
}
|
||||
updateFilenameDebounceRef.current = setTimeout(() => {
|
||||
handleUpdateFileName(itemId, newName);
|
||||
}, 800);
|
||||
}}
|
||||
placeholder="输入新名称"
|
||||
style={{ width: 200, borderRadius: 4 }}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block' }}>
|
||||
选择授权账户
|
||||
</Typography.Text>
|
||||
@@ -1281,9 +1500,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
marginTop: 16,
|
||||
paddingTop: 16,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
justifyContent: 'flex-end',
|
||||
}}>
|
||||
<Button
|
||||
@@ -1292,7 +1508,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setSelectedOauthItems(undefined);
|
||||
setBatchUploadProgress([]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
}}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
@@ -1317,8 +1534,9 @@ const GeneratedRecord: React.FC = () => {
|
||||
open={uploadHistoryModalVisible}
|
||||
onCancel={() => setUploadHistoryModalVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
width={ 800 }
|
||||
style={{ borderRadius: 8 }}
|
||||
mask={{ closable: false }}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Select
|
||||
@@ -1346,23 +1564,36 @@ const GeneratedRecord: React.FC = () => {
|
||||
dataSource={uploadHistoryList}
|
||||
columns={[
|
||||
{
|
||||
title: '任务ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '资源ID',
|
||||
dataIndex: 'resource_id',
|
||||
key: 'resource_id',
|
||||
width: 150,
|
||||
title: '素材名称',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '账户ID',
|
||||
dataIndex: 'advertiser_id',
|
||||
key: 'advertiser_id',
|
||||
width: 120,
|
||||
dataIndex: 'advertiserId',
|
||||
key: 'advertiserId',
|
||||
width: 180,
|
||||
},
|
||||
// {
|
||||
// title: '状态',
|
||||
// dataIndex: 'status',
|
||||
// key: 'status',
|
||||
// width: 100,
|
||||
// render: (status: number, record: any) => {
|
||||
// const statusColorMap: Record<number, string> = {
|
||||
// 1: '#f59e0b',
|
||||
// 2: '#6366f1',
|
||||
// 3: '#10b981',
|
||||
// 4: '#ef4444',
|
||||
// };
|
||||
// return (
|
||||
// <Tag color={statusColorMap[status] || '#64748b'} style={{ borderRadius: 4 }}>
|
||||
// {record.status_text || status}
|
||||
// </Tag>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -1388,6 +1619,18 @@ const GeneratedRecord: React.FC = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'note',
|
||||
key: 'note',
|
||||
width: 250,
|
||||
ellipsis: true,
|
||||
render: (note: string) => (
|
||||
<span style={{ color: '#94a3b8' }}>
|
||||
{note || '-'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
@@ -1395,15 +1638,10 @@ const GeneratedRecord: React.FC = () => {
|
||||
width: 180,
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updated_at',
|
||||
key: 'updated_at',
|
||||
width: 180,
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
|
||||
]}
|
||||
loading={uploadHistoryLoading}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
current: uploadHistoryPage,
|
||||
pageSize: uploadHistoryPageSize,
|
||||
@@ -1412,7 +1650,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
onChange: handleUploadHistoryPageChange,
|
||||
}}
|
||||
rowKey={(record, index) => record.id || record.resource_id || index}
|
||||
rowKey={(record, index) => record.task_id || record.resource_id || index}
|
||||
size="small"
|
||||
/>
|
||||
</Modal>
|
||||
@@ -1729,7 +1967,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{/* <div>
|
||||
<Button
|
||||
|
||||
style={{ width: '100%', borderRadius: 8, marginTop: 20, color: '#4c49cc' }}
|
||||
@@ -1737,9 +1975,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
|
||||
推送媒体后台
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Table, Tag, Input, Pagination, Typography, Select, App, Image } from 'antd';
|
||||
import { FolderOpenOutlined } from '@ant-design/icons';
|
||||
import { getResourcesMaterialList } from '../api';
|
||||
|
||||
// 格式化时间 2026-06-12T03:47:28.542988Z -> 2026-06-12 03:47:28
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 安全拼接URL
|
||||
const buildUrl = (path: string): string => {
|
||||
if (!path) return '';
|
||||
const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||||
const cleanPath = path.startsWith('/') ? path.slice(1) : path;
|
||||
const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
||||
return `${cleanBase}/${cleanPath}`;
|
||||
};
|
||||
|
||||
// 资源类型配置
|
||||
const resourceTypeConfig: Record<string, { label: string; color: string }> = {
|
||||
image: { label: '图片', color: 'green' },
|
||||
video: { label: '视频', color: 'blue' },
|
||||
};
|
||||
|
||||
// 状态配置
|
||||
const statusConfig: Record<string, { label: string; color: string }> = {
|
||||
'1': { label: '待上传', color: 'orange' },
|
||||
'2': { label: '上传中', color: 'processing' },
|
||||
'3': { label: '上传成功', color: 'success' },
|
||||
'4': { label: '上传失败', color: 'error' },
|
||||
};
|
||||
|
||||
interface MaterialResource {
|
||||
fileName: string;
|
||||
resourceUrl: string;
|
||||
remoteUrl: string;
|
||||
storageType: string;
|
||||
storagePath: string;
|
||||
fileSizeBytes: number;
|
||||
sourceModel: string;
|
||||
sourceModelModule: string;
|
||||
sourceId: string;
|
||||
engineId: string;
|
||||
engineType: string;
|
||||
provider: string;
|
||||
modelName: string;
|
||||
generatedAt: string;
|
||||
resourceMonth: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface MaterialData {
|
||||
id: string;
|
||||
oauth_idId: string;
|
||||
advertiserId: string;
|
||||
targetTable: string;
|
||||
targetId: string;
|
||||
materialId: string;
|
||||
uploadId: string;
|
||||
resourceType: string;
|
||||
userId: string;
|
||||
taskId: string;
|
||||
note: string;
|
||||
status: string;
|
||||
preResult: string;
|
||||
preTestTemplateId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
resource: MaterialResource;
|
||||
}
|
||||
|
||||
const MaterialListPage: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
const [materials, setMaterials] = useState<MaterialData[]>([]);
|
||||
const [listLoading, setListLoading] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [searchParams, setSearchParams] = useState({
|
||||
advertiser_id: '',
|
||||
material_id: '',
|
||||
upload_id: '',
|
||||
file_name: '',
|
||||
resource_type: undefined as string | undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadMaterialList();
|
||||
}, []);
|
||||
|
||||
const loadMaterialList = async (page = 1, pageSizeNum = 10, params = searchParams) => {
|
||||
setListLoading(true);
|
||||
try {
|
||||
const response = await getResourcesMaterialList({
|
||||
advertiser_id: params.advertiser_id || undefined,
|
||||
material_id: params.material_id || undefined,
|
||||
upload_id: params.upload_id || undefined,
|
||||
file_name: params.file_name || undefined,
|
||||
resource_type: params.resource_type,
|
||||
page,
|
||||
page_size: pageSizeNum,
|
||||
});
|
||||
if (response?.code === 0) {
|
||||
setMaterials(response.data || []);
|
||||
setTotal(response.total || 0);
|
||||
} else {
|
||||
setMaterials([]);
|
||||
setTotal(0);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取素材列表失败');
|
||||
console.error('获取素材列表失败:', error);
|
||||
} finally {
|
||||
setListLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
// {
|
||||
// title: 'ID',
|
||||
// dataIndex: 'id',
|
||||
// key: 'id',
|
||||
// width: 100,
|
||||
// },
|
||||
{
|
||||
title: '广告主ID',
|
||||
dataIndex: 'advertiserId',
|
||||
key: 'advertiserId',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '素材ID',
|
||||
dataIndex: 'materialId',
|
||||
key: 'materialId',
|
||||
width: 120,
|
||||
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '上传平台Id',
|
||||
dataIndex: 'uploadId',
|
||||
key: 'uploadId',
|
||||
width: 120,
|
||||
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '资源类型',
|
||||
dataIndex: 'resourceType',
|
||||
key: 'resourceType',
|
||||
width: 100,
|
||||
render: (text: string) => {
|
||||
const config = resourceTypeConfig[text];
|
||||
return <Tag color={config?.color}>{config?.label || text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '预览',
|
||||
key: 'preview',
|
||||
width: 80,
|
||||
render: (_: unknown, record: MaterialData) => {
|
||||
if (record.resourceType == 'image') {
|
||||
const imageUrl = record.resource?.resourceUrl ? buildUrl(record.resource.resourceUrl) : '';
|
||||
if (imageUrl) {
|
||||
return <Image width={40} height={40} src={imageUrl} style={{ objectFit: 'cover', borderRadius: 4 }} />;
|
||||
}
|
||||
}
|
||||
return <Tag>-</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: ['resource', 'fileName'],
|
||||
key: 'fileName',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: '#1e293b' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '来源模型',
|
||||
dataIndex: ['resource', 'sourceModel'],
|
||||
key: 'sourceModel',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '供应商',
|
||||
dataIndex: ['resource', 'provider'],
|
||||
key: 'provider',
|
||||
width: 100,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '模型名称',
|
||||
dataIndex: ['resource', 'modelName'],
|
||||
key: 'modelName',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '前测状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
|
||||
// render: (text: string) => {
|
||||
// const config = statusConfig[text];
|
||||
// return <Tag color={config?.color}>{config?.label || text}</Tag>;
|
||||
// },
|
||||
},
|
||||
{
|
||||
title: '前测结果',
|
||||
dataIndex: 'preResult',
|
||||
key: 'preResult',
|
||||
width: 120,
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'note',
|
||||
key: 'note',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: '#94a3b8' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
width: 120,
|
||||
// ellipsis: true,
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
|
||||
},
|
||||
// {
|
||||
// title: '更新时间',
|
||||
// dataIndex: 'updatedAt',
|
||||
// key: 'updatedAt',
|
||||
// width: 160,
|
||||
// render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
|
||||
// },
|
||||
];
|
||||
|
||||
const tableData = materials.map((item, index) => ({
|
||||
...item,
|
||||
index: index + 1,
|
||||
key: item.id,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '94vh' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<FolderOpenOutlined style={{ color: '#6366f1', fontSize: 16 }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>素材列表</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="广告主ID"
|
||||
value={searchParams.advertiser_id}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
|
||||
style={{ width: 160 }}
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="素材ID"
|
||||
value={searchParams.material_id}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, material_id: e.target.value }))}
|
||||
style={{ width: 160 }}
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="上传ID"
|
||||
value={searchParams.upload_id}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, upload_id: e.target.value }))}
|
||||
style={{ width: 160 }}
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="文件名"
|
||||
value={searchParams.file_name}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, file_name: e.target.value }))}
|
||||
style={{ width: 140 }}
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="资源类型"
|
||||
value={searchParams.resource_type}
|
||||
onChange={(value) => setSearchParams(prev => ({ ...prev, resource_type: value }))}
|
||||
style={{ width: 120 }}
|
||||
allowClear
|
||||
options={[
|
||||
{ value: 'image', label: '图片' },
|
||||
{ value: 'video', label: '视频' },
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSearchParams({ advertiser_id: '', material_id: '', upload_id: '', file_name: '', resource_type: undefined });
|
||||
setCurrentPage(1);
|
||||
loadMaterialList(1, pageSize);
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
|
||||
<Table
|
||||
dataSource={tableData}
|
||||
columns={columns}
|
||||
loading={listLoading}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
bordered={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
<div style={{ padding: '16px', textAlign: 'right' }}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
showSizeChanger
|
||||
showTotal={(total) => `共 ${total} 条记录`}
|
||||
onChange={(page, size) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
loadMaterialList(page, size);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaterialListPage;
|
||||
Reference in New Issue
Block a user