Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
Vendored
+91
-91
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DAaavCN6.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BLuHlLoF.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -31,6 +31,7 @@ import AdminShotReplications from './pages/AdminShotReplications';
|
||||
import AdminShotTaskSetDetail from './pages/AdminShotTaskSetDetail';
|
||||
import AdminReplicationProjectDetail from './pages/AdminReplicationProjectDetail';
|
||||
import AdminVideoPromptSchemaConfig from './pages/AdminVideoPromptSchemaConfig';
|
||||
import AdminContactRequests from './pages/AdminContactRequests';
|
||||
|
||||
import { useAdminStore } from './store';
|
||||
|
||||
@@ -103,6 +104,7 @@ const App = () => {
|
||||
<Route path="authoriza" element={<AdminAuthoriz />} />
|
||||
<Route path="consume" element={<AdminConsume />} />
|
||||
<Route path="platform" element={<AdminPlatform />} />
|
||||
<Route path="contact-requests" element={<AdminContactRequests />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -431,7 +431,10 @@ export async function getOpenTypeList(params?: {
|
||||
page_size?: number;
|
||||
type_name?: string;
|
||||
open_type?: number;
|
||||
}): Promise<{ total: number; items: any[] }> {
|
||||
}): Promise<{
|
||||
pagination: any;
|
||||
data: never[];
|
||||
}> {
|
||||
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));
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Button, Tag, Space, Typography, message, Modal, Card, Popconfirm, Empty } from 'antd';
|
||||
import { CheckOutlined, DeleteOutlined, EyeOutlined, FilterOutlined, MessageOutlined } from '@ant-design/icons';
|
||||
import { useAdminStore } from '../store';
|
||||
import { api } from '../api/client';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
|
||||
interface ContactRequest {
|
||||
id: string;
|
||||
userId: string;
|
||||
phone: string;
|
||||
companyName: string;
|
||||
industry: string;
|
||||
name: string;
|
||||
message: string | null;
|
||||
isHandled: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const AdminContactRequests: React.FC = () => {
|
||||
const [data, setData] = useState<ContactRequest[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [isHandledFilter, setIsHandledFilter] = useState<boolean | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<ContactRequest | null>(null);
|
||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||
|
||||
const { user } = useAdminStore();
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!user?.isAdmin) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const query = new URLSearchParams();
|
||||
query.set('page', String(page));
|
||||
query.set('page_size', String(pageSize));
|
||||
if (isHandledFilter !== null) {
|
||||
query.set('is_handled', String(isHandledFilter));
|
||||
}
|
||||
const res = await api.get<{ items: ContactRequest[]; total: number }>(`/contact/requests?${query.toString()}`);
|
||||
setData(res.items);
|
||||
setTotal(res.total);
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '获取失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [page, pageSize, isHandledFilter]);
|
||||
|
||||
const handleMarkHandled = async (id: string) => {
|
||||
try {
|
||||
await api.put(`/contact/requests/${id}/handle`);
|
||||
message.success('已标记为处理');
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await api.delete(`/contact/requests/${id}`);
|
||||
message.success('已删除');
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (item: ContactRequest) => {
|
||||
setSelectedItem(item);
|
||||
setDetailModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePageChange = (p: number, ps: number) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 100,
|
||||
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '公司名称',
|
||||
dataIndex: 'companyName',
|
||||
key: 'companyName',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '行业',
|
||||
dataIndex: 'industry',
|
||||
key: 'industry',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'isHandled',
|
||||
key: 'isHandled',
|
||||
width: 80,
|
||||
render: (isHandled: boolean) => (
|
||||
<Tag color={isHandled ? 'green' : 'orange'}>
|
||||
{isHandled ? '已处理' : '待处理'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
render: (date: string) => formatDate(date),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 180,
|
||||
render: (_: unknown, record: ContactRequest) => (
|
||||
<Space size={4}>
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
|
||||
查看
|
||||
</Button>
|
||||
{!record.isHandled && (
|
||||
<Button type="link" size="small" icon={<CheckOutlined />} onClick={() => handleMarkHandled(record.id)}>
|
||||
标记处理
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm title="确定删除该记录?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="link" danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Space>
|
||||
<MessageOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>联系请求管理</Typography.Text>
|
||||
<Tag color="purple">共 {total} 条记录</Tag>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
type={isHandledFilter === null ? 'primary' : 'default'}
|
||||
onClick={() => setIsHandledFilter(null)}
|
||||
icon={<FilterOutlined />}
|
||||
size="small"
|
||||
>
|
||||
全部
|
||||
</Button>
|
||||
<Button
|
||||
type={isHandledFilter === false ? 'primary' : 'default'}
|
||||
onClick={() => setIsHandledFilter(false)}
|
||||
size="small"
|
||||
>
|
||||
待处理
|
||||
</Button>
|
||||
<Button
|
||||
type={isHandledFilter === true ? 'primary' : 'default'}
|
||||
onClick={() => setIsHandledFilter(true)}
|
||||
size="small"
|
||||
>
|
||||
已处理
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: 40 }}>加载中...</div>
|
||||
) : data.length === 0 ? (
|
||||
<Empty description="暂无联系请求" style={{ padding: '40px 0' }} />
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
onChange: handlePageChange,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条记录`,
|
||||
}}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={<Space><EyeOutlined />联系请求详情</Space>}
|
||||
open={detailModalOpen}
|
||||
onCancel={() => setDetailModalOpen(false)}
|
||||
footer={null}
|
||||
width={500}
|
||||
>
|
||||
{selectedItem && (
|
||||
<div style={{ padding: 8 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Title level={4} style={{ marginBottom: 16 }}>
|
||||
{selectedItem.name}
|
||||
<Tag color={selectedItem.isHandled ? 'green' : 'orange'} style={{ marginLeft: 12 }}>
|
||||
{selectedItem.isHandled ? '已处理' : '待处理'}
|
||||
</Tag>
|
||||
</Typography.Title>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: 12 }}>
|
||||
<Typography.Text style={{ color: '#64748b' }}>手机号:</Typography.Text>
|
||||
<Typography.Text>{selectedItem.phone}</Typography.Text>
|
||||
<Typography.Text style={{ color: '#64748b' }}>公司名称:</Typography.Text>
|
||||
<Typography.Text>{selectedItem.companyName}</Typography.Text>
|
||||
<Typography.Text style={{ color: '#64748b' }}>行业:</Typography.Text>
|
||||
<Typography.Text>{selectedItem.industry}</Typography.Text>
|
||||
<Typography.Text style={{ color: '#64748b' }}>提交时间:</Typography.Text>
|
||||
<Typography.Text>{formatDate(selectedItem.createdAt)}</Typography.Text>
|
||||
{selectedItem.message && (
|
||||
<>
|
||||
<Typography.Text style={{ color: '#64748b' }}>留言:</Typography.Text>
|
||||
<Typography.Text>{selectedItem.message}</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{!selectedItem.isHandled && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
handleMarkHandled(selectedItem.id);
|
||||
setDetailModalOpen(false);
|
||||
}}
|
||||
icon={<CheckOutlined />}
|
||||
>
|
||||
标记为已处理
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => setDetailModalOpen(false)}>关闭</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminContactRequests;
|
||||
@@ -10,8 +10,8 @@ import { formatDate } from '../utils/formatDate';
|
||||
|
||||
interface OpenType {
|
||||
id: string;
|
||||
open_type: number;
|
||||
type_name: string;
|
||||
openType: number;
|
||||
typeName: string;
|
||||
description: string;
|
||||
thumb?: string;
|
||||
createdAt: string;
|
||||
@@ -49,6 +49,8 @@ const AdminPlatform: React.FC = () => {
|
||||
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();
|
||||
@@ -60,8 +62,8 @@ const AdminPlatform: React.FC = () => {
|
||||
page: p || page,
|
||||
page_size: ps || pageSize,
|
||||
});
|
||||
setOpenTypes(res.items || []);
|
||||
setTotal(res.total || 0);
|
||||
setOpenTypes(res.data || []);
|
||||
setTotal(res.pagination.total || 0);
|
||||
} catch {
|
||||
message.error('加载开户方式列表失败');
|
||||
} finally {
|
||||
@@ -76,16 +78,16 @@ const AdminPlatform: React.FC = () => {
|
||||
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,
|
||||
thumb: createThumbUrl,
|
||||
});
|
||||
message.success('创建成功');
|
||||
setCreateModalVisible(false);
|
||||
createForm.resetFields();
|
||||
setCreateThumbUrl('');
|
||||
load();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '创建失败');
|
||||
@@ -95,7 +97,7 @@ const AdminPlatform: React.FC = () => {
|
||||
const handleDetail = async (id: string) => {
|
||||
try {
|
||||
const openType = await getOpenType(id);
|
||||
setCurrentOpenType(openType);
|
||||
setCurrentOpenType(openType.data || {});
|
||||
setDetailModalVisible(true);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '获取详情失败');
|
||||
@@ -105,19 +107,14 @@ const AdminPlatform: React.FC = () => {
|
||||
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,
|
||||
});
|
||||
setCurrentOpenType(openType.data || {});
|
||||
setUpdateThumbUrl(openType.thumb || '');
|
||||
setUpdateModalVisible(true);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '获取详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSaveUpdate = async () => {
|
||||
if (!currentOpenType) return;
|
||||
try {
|
||||
@@ -126,12 +123,13 @@ const AdminPlatform: React.FC = () => {
|
||||
open_type: values.open_type,
|
||||
type_name: values.type_name,
|
||||
description: values.description,
|
||||
thumb: values.thumb,
|
||||
thumb: updateThumbUrl,
|
||||
});
|
||||
message.success('更新成功');
|
||||
setUpdateModalVisible(false);
|
||||
updateForm.resetFields();
|
||||
setCurrentOpenType(null);
|
||||
setUpdateThumbUrl('');
|
||||
load();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '更新失败');
|
||||
@@ -166,13 +164,13 @@ const AdminPlatform: React.FC = () => {
|
||||
},
|
||||
{
|
||||
title: '开户类型',
|
||||
dataIndex: 'open_type',
|
||||
dataIndex: 'openType',
|
||||
width: 100,
|
||||
render: (v: number) => <Typography.Text>{v}</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '类型名称',
|
||||
dataIndex: 'type_name',
|
||||
dataIndex: 'typeName',
|
||||
width: 150,
|
||||
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
||||
},
|
||||
@@ -191,9 +189,12 @@ const AdminPlatform: React.FC = () => {
|
||||
title: '缩略图',
|
||||
dataIndex: 'thumb',
|
||||
width: 120,
|
||||
render: (v: string) => (
|
||||
v ? <img src={v} alt="thumb" style={{ width: 80, height: 60, objectFit: 'cover' }} /> : '-'
|
||||
),
|
||||
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: '创建时间',
|
||||
@@ -291,7 +292,9 @@ const AdminPlatform: React.FC = () => {
|
||||
onCancel={() => {
|
||||
setCreateModalVisible(false);
|
||||
createForm.resetFields();
|
||||
setCreateThumbUrl('');
|
||||
}}
|
||||
mask={{ closable: false }}
|
||||
okText="创建"
|
||||
cancelText="取消"
|
||||
width={600}
|
||||
@@ -326,28 +329,28 @@ const AdminPlatform: React.FC = () => {
|
||||
>
|
||||
<Input.TextArea placeholder="请输入描述" rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="thumb"
|
||||
label="缩略图"
|
||||
>
|
||||
<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);
|
||||
console.log(res);
|
||||
createForm.setFieldsValue({ thumb: res.url });
|
||||
onSuccess?.(res);
|
||||
setCreateThumbUrl(res.url);
|
||||
onSuccess?.({ url: res.url });
|
||||
} catch (e) {
|
||||
onError?.(normalizeUploadError(e));
|
||||
}
|
||||
}}
|
||||
onRemove={() => setCreateThumbUrl('')}
|
||||
>
|
||||
<div>
|
||||
<UploadOutlined style={{ fontSize: 24, color: '#999' }} />
|
||||
<div style={{ marginTop: 8 }}>上传图片</div>
|
||||
</div>
|
||||
{!createThumbUrl && (
|
||||
<div>
|
||||
<UploadOutlined style={{ fontSize: 24, color: '#999' }} />
|
||||
<div style={{ marginTop: 8 }}>上传图片</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -356,32 +359,68 @@ const AdminPlatform: React.FC = () => {
|
||||
<Modal
|
||||
title="开户方式详情"
|
||||
open={detailModalVisible}
|
||||
onOk={() => setDetailModalVisible(false)}
|
||||
onCancel={() => {
|
||||
setDetailModalVisible(false);
|
||||
setCurrentOpenType(null);
|
||||
}}
|
||||
mask={{ closable: false }}
|
||||
okText="关闭"
|
||||
cancelText="取消"
|
||||
width={600}
|
||||
width={520}
|
||||
>
|
||||
{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 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>
|
||||
@@ -395,6 +434,16 @@ const AdminPlatform: React.FC = () => {
|
||||
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}
|
||||
@@ -424,29 +473,23 @@ const AdminPlatform: React.FC = () => {
|
||||
>
|
||||
<Input.TextArea placeholder="请输入描述" rows={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="thumb"
|
||||
label="缩略图"
|
||||
>
|
||||
<Form.Item label="缩略图">
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
defaultFileList={
|
||||
currentOpenType?.thumb
|
||||
? [{ uid: '1', name: 'thumb', status: 'done', url: currentOpenType.thumb }]
|
||||
: []
|
||||
}
|
||||
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);
|
||||
updateForm.setFieldsValue({ thumb: res.url });
|
||||
onSuccess?.(res);
|
||||
setUpdateThumbUrl(res.url);
|
||||
onSuccess?.({ url: res.url });
|
||||
} catch (e) {
|
||||
onError?.(normalizeUploadError(e));
|
||||
}
|
||||
}}
|
||||
onRemove={() => setUpdateThumbUrl('')}
|
||||
>
|
||||
{!updateForm.getFieldValue('thumb') && (
|
||||
{!updateThumbUrl && (
|
||||
<div>
|
||||
<UploadOutlined style={{ fontSize: 24, color: '#999' }} />
|
||||
<div style={{ marginTop: 8 }}>上传图片</div>
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/pages/adminauthoriz.tsx","./src/pages/adminconsume.tsx","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminhotopeningreplicationdetail.tsx","./src/pages/adminhotopeningreplications.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoauthapplist.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminpaymentstats.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminreplicationprojectdetail.tsx","./src/pages/adminsettings.tsx","./src/pages/adminshotreplications.tsx","./src/pages/adminshottasksetdetail.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/pages/adminvideopromptschemaconfig.tsx","./src/pages/adminplatform.tsx","./src/pages/adminreplication/components/jsoncollapse.tsx","./src/pages/adminreplication/components/mediapreview.tsx","./src/pages/adminreplication/components/statustag.tsx","./src/pages/adminreplication/components/videopromptschemaviewer.tsx","./src/store/index.ts","./src/types/index.ts","./src/types/xlsx-js-style.d.ts","./src/utils/excelexport.ts","./src/utils/formatdate.ts","./src/utils/videopromptschema.ts"],"version":"6.0.3"}
|
||||
{"root":["./src/app.tsx","./src/env.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/crypto.ts","./src/api/index.ts","./src/pages/adminauthoriz.tsx","./src/pages/adminconsume.tsx","./src/pages/admincontactrequests.tsx","./src/pages/admincreditratios.tsx","./src/pages/admincreditrecords.tsx","./src/pages/admindashboard.tsx","./src/pages/admingenerationairecords.tsx","./src/pages/admingenerationrecords.tsx","./src/pages/adminhotopeningreplicationdetail.tsx","./src/pages/adminhotopeningreplications.tsx","./src/pages/adminimageengines.tsx","./src/pages/adminindustries.tsx","./src/pages/adminlayout.tsx","./src/pages/adminloginpage.tsx","./src/pages/adminmenuconfig.tsx","./src/pages/adminmodels.tsx","./src/pages/adminnotificationmanager.tsx","./src/pages/adminoauthapplist.tsx","./src/pages/adminoperationlogs.tsx","./src/pages/adminpaymentconfig.tsx","./src/pages/adminpaymentstats.tsx","./src/pages/adminrechargepackages.tsx","./src/pages/adminreplicationprojectdetail.tsx","./src/pages/adminsettings.tsx","./src/pages/adminshotreplications.tsx","./src/pages/adminshottasksetdetail.tsx","./src/pages/adminusers.tsx","./src/pages/adminvideoengines.tsx","./src/pages/adminvideopromptschemaconfig.tsx","./src/pages/adminplatform.tsx","./src/pages/adminreplication/components/jsoncollapse.tsx","./src/pages/adminreplication/components/mediapreview.tsx","./src/pages/adminreplication/components/statustag.tsx","./src/pages/adminreplication/components/videopromptschemaviewer.tsx","./src/store/index.ts","./src/types/index.ts","./src/types/xlsx-js-style.d.ts","./src/utils/excelexport.ts","./src/utils/formatdate.ts","./src/utils/videopromptschema.ts"],"version":"6.0.3"}
|
||||
@@ -25,6 +25,7 @@ from app.api.v1.pre_test_template import router as pre_test_template_router
|
||||
from app.api.v1.material_consumption import router as material_consumption_router
|
||||
from app.api.v1.open_type import router as open_type_router
|
||||
from app.api.v1.resources_material import router as resources_material_router
|
||||
from app.api.v1.contact import router as contact_router
|
||||
from app.api.admin import router as admin_module_router
|
||||
|
||||
api_router = APIRouter()
|
||||
@@ -53,4 +54,5 @@ api_router.include_router(pre_test_template_router)
|
||||
api_router.include_router(material_consumption_router)
|
||||
api_router.include_router(open_type_router)
|
||||
api_router.include_router(resources_material_router)
|
||||
api_router.include_router(contact_router)
|
||||
api_router.include_router(admin_module_router)
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.dependencies import (
|
||||
get_current_user_allow_password_pending,
|
||||
get_db,
|
||||
)
|
||||
from app.models.credit_record import CreditRecord
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.models.user import User
|
||||
from app.schemas.auth import (
|
||||
@@ -86,6 +87,20 @@ async def _get_register_credits(db: AsyncSession) -> int:
|
||||
return int(value) if value else 100
|
||||
|
||||
|
||||
async def _add_register_credit_record(db: AsyncSession, user: User, credits: int) -> None:
|
||||
if credits <= 0:
|
||||
return
|
||||
record = CreditRecord(
|
||||
id=generate_id(),
|
||||
user_id=user.id,
|
||||
type="recharge",
|
||||
amount=credits,
|
||||
balance_after=user.credits,
|
||||
description=f"注册赠送 {credits} 积分",
|
||||
)
|
||||
db.add(record)
|
||||
|
||||
|
||||
async def _handle_daily_login_credits(db: AsyncSession, user: User) -> None:
|
||||
enabled_result = await db.execute(
|
||||
select(SystemConfig.value).where(SystemConfig.key == "user_login_credits_enabled").limit(1)
|
||||
@@ -108,6 +123,16 @@ async def _handle_daily_login_credits(db: AsyncSession, user: User) -> None:
|
||||
return
|
||||
|
||||
user.credits += credits
|
||||
|
||||
record = CreditRecord(
|
||||
id=generate_id(),
|
||||
user_id=user.id,
|
||||
type="recharge",
|
||||
amount=credits,
|
||||
balance_after=user.credits,
|
||||
description=f"每日登录赠送 {credits} 积分",
|
||||
)
|
||||
db.add(record)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -204,6 +229,7 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
||||
db.add(user)
|
||||
await db.flush()
|
||||
|
||||
await _add_register_credit_record(db, user, register_credits)
|
||||
await _assign_default_frontend_menus(db, user)
|
||||
|
||||
user.credits = round(user.credits, 2)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.contact_request import ContactRequest
|
||||
from app.models.user import User
|
||||
from app.schemas.contact import ContactRequestCreate, ContactRequestListOut, ContactRequestOut
|
||||
from app.utils.id_gen import generate_id
|
||||
|
||||
router = APIRouter(prefix="/contact", tags=["contact"])
|
||||
|
||||
|
||||
@router.post("/request", summary="提交联系请求", status_code=status.HTTP_201_CREATED)
|
||||
async def create_contact_request(
|
||||
request: ContactRequestCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
today_end = today_start + timedelta(days=1)
|
||||
|
||||
count = await db.execute(
|
||||
select(func.count(ContactRequest.id))
|
||||
.where(ContactRequest.user_id == user.id)
|
||||
.where(ContactRequest.created_at >= today_start)
|
||||
.where(ContactRequest.created_at < today_end)
|
||||
)
|
||||
daily_count = count.scalar_one()
|
||||
|
||||
if daily_count >= 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail="每个账号每天只能提交一次联系我们"
|
||||
)
|
||||
|
||||
contact_request = ContactRequest(
|
||||
id=generate_id(),
|
||||
user_id=user.id,
|
||||
phone=request.phone,
|
||||
company_name=request.company_name,
|
||||
industry=request.industry,
|
||||
name=request.name,
|
||||
message=request.message,
|
||||
)
|
||||
|
||||
db.add(contact_request)
|
||||
await db.commit()
|
||||
await db.refresh(contact_request)
|
||||
|
||||
return {"message": "提交成功,我们会尽快与您联系"}
|
||||
|
||||
|
||||
@router.get("/requests", summary="获取联系请求列表", response_model=ContactRequestListOut)
|
||||
async def get_contact_requests(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
is_handled: bool | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
|
||||
|
||||
query = select(ContactRequest).order_by(ContactRequest.created_at.desc())
|
||||
|
||||
if is_handled is not None:
|
||||
query = query.where(ContactRequest.is_handled == is_handled)
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
result = await db.execute(query.offset(offset).limit(page_size))
|
||||
items = result.scalars().all()
|
||||
|
||||
count_result = await db.execute(select(func.count(ContactRequest.id)))
|
||||
total = count_result.scalar_one()
|
||||
|
||||
return {"items": items, "total": total}
|
||||
|
||||
|
||||
@router.get("/requests/{request_id}", summary="获取联系请求详情", response_model=ContactRequestOut)
|
||||
async def get_contact_request(
|
||||
request_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
|
||||
|
||||
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
|
||||
contact_request = result.scalar_one_or_none()
|
||||
|
||||
if not contact_request:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
|
||||
|
||||
return contact_request
|
||||
|
||||
|
||||
@router.put("/requests/{request_id}/handle", summary="标记为已处理")
|
||||
async def mark_as_handled(
|
||||
request_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
|
||||
|
||||
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
|
||||
contact_request = result.scalar_one_or_none()
|
||||
|
||||
if not contact_request:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
|
||||
|
||||
contact_request.is_handled = True
|
||||
await db.commit()
|
||||
await db.refresh(contact_request)
|
||||
|
||||
return {"message": "已标记为处理"}
|
||||
|
||||
|
||||
@router.delete("/requests/{request_id}", summary="删除联系请求")
|
||||
async def delete_contact_request(
|
||||
request_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权限")
|
||||
|
||||
result = await db.execute(select(ContactRequest).where(ContactRequest.id == request_id))
|
||||
contact_request = result.scalar_one_or_none()
|
||||
|
||||
if not contact_request:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="记录不存在")
|
||||
|
||||
await db.delete(contact_request)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "删除成功"}
|
||||
@@ -55,144 +55,152 @@ async def async_batch_upload_material(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Any | dict:
|
||||
if not req.tasks:
|
||||
try:
|
||||
if not req.tasks:
|
||||
return {
|
||||
"code": 0,
|
||||
"message": "上传任务列表不能为空",
|
||||
"task_ids": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
task_ids = []
|
||||
errors = []
|
||||
|
||||
source_model_map = {
|
||||
"generation_records": "GenerationRecord",
|
||||
"generated_resources": None,
|
||||
"chat_generation_tasks": "ChatGenerationTask",
|
||||
}
|
||||
|
||||
for task_index, task in enumerate(req.tasks, 1):
|
||||
if not task.advertiser_ids:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "广告主id数组不能为空",
|
||||
})
|
||||
continue
|
||||
|
||||
if not task.resource_ids:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "资源id数组不能为空",
|
||||
})
|
||||
continue
|
||||
|
||||
if task.is_pre_test == "1" and not task.pre_test_template:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "开启前测功能时,必须指定前测模板id",
|
||||
})
|
||||
continue
|
||||
|
||||
if task.is_pre_test == "1":
|
||||
template = await db.execute(
|
||||
select(PreTestTemplate).where(PreTestTemplate.id == task.pre_test_template).
|
||||
where(PreTestTemplate.deleted_at.is_(None)).
|
||||
where(PreTestTemplate.user_id == current_user.id)
|
||||
)
|
||||
template = template.scalar_one_or_none()
|
||||
if not template:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "前测模板id不存在",
|
||||
})
|
||||
continue
|
||||
|
||||
target_source_model = source_model_map.get(task.source_model)
|
||||
|
||||
# 检查资源id是否存在,非资源id
|
||||
if target_source_model:
|
||||
query = (
|
||||
select(GeneratedResource.id)
|
||||
.where(GeneratedResource.source_model == target_source_model)
|
||||
.where(GeneratedResource.source_id.in_(task.resource_ids))
|
||||
.where(GeneratedResource.user_id == current_user.id)
|
||||
.where(GeneratedResource.deleted_at.is_(None))
|
||||
)
|
||||
result = await db.execute(query)
|
||||
valid_resource_ids = [row[0] for row in result.all()]
|
||||
|
||||
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
|
||||
|
||||
if invalid_ids:
|
||||
invalid_ids_str = ", ".join(invalid_ids)
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
|
||||
})
|
||||
continue
|
||||
|
||||
resource_ids_to_upload = valid_resource_ids
|
||||
else:
|
||||
#用户提交的直接是资源id
|
||||
query = (
|
||||
select(GeneratedResource.id)
|
||||
.where(GeneratedResource.id.in_(task.resource_ids))
|
||||
.where(GeneratedResource.user_id == current_user.id)
|
||||
.where(GeneratedResource.deleted_at.is_(None))
|
||||
)
|
||||
result = await db.execute(query)
|
||||
valid_resource_ids = [row[0] for row in result.all()]
|
||||
|
||||
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
|
||||
|
||||
if invalid_ids:
|
||||
invalid_ids_str = ", ".join(invalid_ids)
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
|
||||
})
|
||||
continue
|
||||
|
||||
resource_ids_to_upload = valid_resource_ids
|
||||
|
||||
for advertiser_id in task.advertiser_ids:
|
||||
for resource_id in resource_ids_to_upload:
|
||||
other_info = {}
|
||||
if task.is_pre_test == "1":
|
||||
other_info["is_pre_test"] = task.is_pre_test
|
||||
other_info["pre_test_template"] = task.pre_test_template
|
||||
|
||||
task_id = generate_id()
|
||||
upload_task = UploadTask(
|
||||
id=task_id,
|
||||
user_id=current_user.id,
|
||||
oauth_id=task.oauth_id,
|
||||
advertiser_id=advertiser_id,
|
||||
resource_id=resource_id,
|
||||
status=1,
|
||||
note=None,
|
||||
other_info=json.dumps(other_info) if other_info else None,
|
||||
)
|
||||
|
||||
db.add(upload_task)
|
||||
await upload_queue.enqueue(task_id)
|
||||
task_ids.append(task_id)
|
||||
|
||||
await db.commit()
|
||||
|
||||
message = "上传任务已提交"
|
||||
if errors:
|
||||
message = f"部分任务提交成功,{len(errors)} 个任务失败"
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"message": "上传任务列表不能为空",
|
||||
"message": message,
|
||||
"task_ids": task_ids,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"code": 1,
|
||||
"message": str(e),
|
||||
"task_ids": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
task_ids = []
|
||||
errors = []
|
||||
|
||||
source_model_map = {
|
||||
"generation_records": "GenerationRecord",
|
||||
"generated_resources": None,
|
||||
"chat_generation_tasks": "ChatGenerationTask",
|
||||
}
|
||||
|
||||
for task_index, task in enumerate(req.tasks, 1):
|
||||
if not task.advertiser_ids:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "广告主id数组不能为空",
|
||||
})
|
||||
continue
|
||||
|
||||
if not task.resource_ids:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "资源id数组不能为空",
|
||||
})
|
||||
continue
|
||||
|
||||
if task.is_pre_test == "1" and not task.pre_test_template:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "开启前测功能时,必须指定前测模板id",
|
||||
})
|
||||
continue
|
||||
|
||||
if task.is_pre_test == "1":
|
||||
template = await db.execute(
|
||||
select(PreTestTemplate).where(PreTestTemplate.id == task.pre_test_template).
|
||||
where(PreTestTemplate.deleted_at.is_(None)).
|
||||
where(PreTestTemplate.user_id == current_user.id)
|
||||
)
|
||||
template = template.scalar_one_or_none()
|
||||
if not template:
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": "前测模板id不存在",
|
||||
})
|
||||
continue
|
||||
|
||||
target_source_model = source_model_map.get(task.source_model)
|
||||
|
||||
# 检查资源id是否存在,非资源id
|
||||
if target_source_model:
|
||||
query = (
|
||||
select(GeneratedResource.id)
|
||||
.where(GeneratedResource.source_model == target_source_model)
|
||||
.where(GeneratedResource.source_id.in_(task.resource_ids))
|
||||
.where(GeneratedResource.user_id == current_user.id)
|
||||
.where(GeneratedResource.deleted_at.is_(None))
|
||||
)
|
||||
result = await db.execute(query)
|
||||
valid_resource_ids = [row[0] for row in result.all()]
|
||||
|
||||
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
|
||||
|
||||
if invalid_ids:
|
||||
invalid_ids_str = ", ".join(invalid_ids)
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
|
||||
})
|
||||
continue
|
||||
|
||||
resource_ids_to_upload = valid_resource_ids
|
||||
else:
|
||||
#用户提交的直接是资源id
|
||||
query = (
|
||||
select(GeneratedResource.id)
|
||||
.where(GeneratedResource.id.in_(task.resource_ids))
|
||||
.where(GeneratedResource.user_id == current_user.id)
|
||||
.where(GeneratedResource.deleted_at.is_(None))
|
||||
)
|
||||
result = await db.execute(query)
|
||||
valid_resource_ids = [row[0] for row in result.all()]
|
||||
|
||||
invalid_ids = set(task.resource_ids) - set(valid_resource_ids)
|
||||
|
||||
if invalid_ids:
|
||||
invalid_ids_str = ", ".join(invalid_ids)
|
||||
errors.append({
|
||||
"task_index": task_index,
|
||||
"error": f"资源id [{invalid_ids_str}] 不可用或已删除",
|
||||
})
|
||||
continue
|
||||
|
||||
resource_ids_to_upload = valid_resource_ids
|
||||
|
||||
for advertiser_id in task.advertiser_ids:
|
||||
for resource_id in resource_ids_to_upload:
|
||||
other_info = {}
|
||||
if task.is_pre_test == "1":
|
||||
other_info["is_pre_test"] = task.is_pre_test
|
||||
other_info["pre_test_template"] = task.pre_test_template
|
||||
|
||||
task_id = generate_id()
|
||||
upload_task = UploadTask(
|
||||
id=task_id,
|
||||
user_id=current_user.id,
|
||||
oauth_id=task.oauth_id,
|
||||
advertiser_id=advertiser_id,
|
||||
resource_id=resource_id,
|
||||
status=1,
|
||||
note=None,
|
||||
other_info=json.dumps(other_info) if other_info else None,
|
||||
)
|
||||
|
||||
db.add(upload_task)
|
||||
await upload_queue.enqueue(task_id)
|
||||
task_ids.append(task_id)
|
||||
|
||||
await db.commit()
|
||||
|
||||
message = "上传任务已提交"
|
||||
if errors:
|
||||
message = f"部分任务提交成功,{len(errors)} 个任务失败"
|
||||
|
||||
return {
|
||||
"code": 0,
|
||||
"message": message,
|
||||
"task_ids": task_ids,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/batch-update-filename",
|
||||
summary="批量修改资源文件名",
|
||||
|
||||
+60
-113
@@ -125,7 +125,7 @@ async def _seed_data():
|
||||
from app.models.model_config import ModelConfig
|
||||
from app.services.auth import hash_password
|
||||
from app.utils.id_gen import generate_id
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, func
|
||||
|
||||
async with async_session() as db:
|
||||
# Check if admin exists
|
||||
@@ -354,91 +354,42 @@ async def _seed_data():
|
||||
# Seed menu configs
|
||||
from app.models.menu_config import MenuConfig
|
||||
|
||||
frontend_groups = [
|
||||
("AI项目行业生成", "HomeOutlined", 0),
|
||||
("AI对话生成", "HomeOutlined", 1),
|
||||
("AI视频创作", "HomeOutlined", 2),
|
||||
("资产管理", "HomeOutlined", 3),
|
||||
("媒体关联", "HomeOutlined", 4),
|
||||
]
|
||||
frontend_group_ids: dict[str, str] = {}
|
||||
for label, icon, order in frontend_groups:
|
||||
existing = await db.execute(
|
||||
select(MenuConfig).where(
|
||||
MenuConfig.label == label,
|
||||
MenuConfig.menu_type == "group",
|
||||
MenuConfig.menu_target == "frontend",
|
||||
).limit(1)
|
||||
)
|
||||
group = existing.scalar_one_or_none()
|
||||
if group:
|
||||
frontend_group_ids[label] = group.id
|
||||
else:
|
||||
gid = generate_id()
|
||||
frontend_group_ids[label] = gid
|
||||
db.add(
|
||||
MenuConfig(
|
||||
id=gid,
|
||||
path="",
|
||||
label=label,
|
||||
icon=icon,
|
||||
sort_order=order,
|
||||
is_active=True,
|
||||
parent_id=None,
|
||||
menu_type="group",
|
||||
menu_target="frontend",
|
||||
is_default=True,
|
||||
)
|
||||
)
|
||||
menu_count = await db.execute(select(func.count(MenuConfig.id)))
|
||||
menu_count_result = menu_count.scalar_one()
|
||||
|
||||
logging.info(f"Menu config count: {menu_count_result}")
|
||||
|
||||
if menu_count_result == 0:
|
||||
logging.info("Inserting default menu configs...")
|
||||
frontend_menus = [
|
||||
{"id": "0019eca2549ba477069", "label": "制作素材", "path": "", "icon": "PlayCircleOutlined", "sort_order": 1, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019f01914a66279ec0", "label": "灵感参考", "path": "", "icon": "HomeOutlined", "sort_order": 2, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019eca2735b9f3d944", "label": "我的资产", "path": "", "icon": "HomeOutlined", "sort_order": 3, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019eca27ec59922048", "label": "广告素材管理", "path": "", "icon": "HomeOutlined", "sort_order": 4, "is_active": True, "parent_id": None, "menu_type": "group", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019f01b5717017792f", "label": "首页", "path": "/home", "icon": "HomeOutlined", "sort_order": 0, "is_active": True, "parent_id": None, "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019f01b445f51dee00", "label": "我的项目", "path": "/projects", "icon": "AppstoreOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019f01b445f854a14a", "label": "AI创作", "path": "/conversation", "icon": "StarOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019e49af896982b070", "label": "爆款开头复刻", "path": "/initial", "icon": "CodeOutlined", "sort_order": 2, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019e4f26a8c4c0de5a", "label": "拆镜复刻", "path": "/removelens", "icon": "CameraOutlined", "sort_order": 3, "is_active": True, "parent_id": "0019eca2549ba477069", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019f01b7bb6e147445", "label": "爆款榜单", "path": "/popular", "icon": "FireOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019f01914a66279ec0", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019f019267a4de06a0", "label": "创意广场", "path": "/creativeplaza", "icon": "BulbOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019f01914a66279ec0", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019e80aff6d0ea5843", "label": "素材云", "path": "/generated", "icon": "CloudOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019eca2735b9f3d944", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019f01b44606cc4c81", "label": "投放平台授权", "path": "/authorization", "icon": "UserOutlined", "sort_order": 0, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019ef924a3521924ab", "label": "素材ID列表", "path": "/materials", "icon": "FileTextOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019eb61d6fc2c14ebd", "label": "消耗列表", "path": "/consume", "icon": "FileTextOutlined", "sort_order": 1, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
{"id": "0019ed36864f343d347", "label": "素材前测", "path": "/pretest", "icon": "DatabaseOutlined", "sort_order": 2, "is_active": True, "parent_id": "0019eca27ec59922048", "menu_type": "page", "menu_target": "frontend", "is_default": True},
|
||||
]
|
||||
|
||||
for menu in frontend_menus:
|
||||
db.add(MenuConfig(**menu))
|
||||
|
||||
frontend_pages = [
|
||||
("/projects", "我的项目", "HomeOutlined", 0, "AI项目行业生成", True),
|
||||
("/conversation", "AI创作", "StarOutlined", 0, "AI对话生成", True),
|
||||
("/initial", "爆款开头复刻", "CodeOutlined", 0, "AI视频创作", False),
|
||||
("/removelens", "拆镜复刻", "CameraOutlined", 1, "AI视频创作", False),
|
||||
("/records", "项目记录", "PlayCircleOutlined", 0, "资产管理", True),
|
||||
("/generated", "素材云", "CloudOutlined", 1, "资产管理", True),
|
||||
("/authorization", "授权管理", "UserOutlined", 0, "媒体关联", False),
|
||||
("/consume", "消耗列表", "FileTextOutlined", 1, "媒体关联", False),
|
||||
]
|
||||
for path, label, icon, order, group_label, is_default in frontend_pages:
|
||||
existing = await db.execute(
|
||||
select(MenuConfig).where(MenuConfig.path == path).limit(1)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
db.add(
|
||||
MenuConfig(
|
||||
id=generate_id(),
|
||||
path=path,
|
||||
label=label,
|
||||
icon=icon,
|
||||
sort_order=order,
|
||||
is_active=True,
|
||||
parent_id=frontend_group_ids.get(group_label),
|
||||
menu_type="page",
|
||||
menu_target="frontend",
|
||||
is_default=is_default,
|
||||
)
|
||||
)
|
||||
|
||||
admin_groups = [
|
||||
("模型设置", "RobotOutlined", 98),
|
||||
("模型配置", "RobotOutlined", 6),
|
||||
("系统设置", "SettingOutlined", 99),
|
||||
]
|
||||
admin_group_ids: dict[str, str] = {}
|
||||
for label, icon, order in admin_groups:
|
||||
existing = await db.execute(
|
||||
select(MenuConfig).where(
|
||||
MenuConfig.label == label,
|
||||
MenuConfig.menu_type == "group",
|
||||
MenuConfig.menu_target == "admin",
|
||||
).limit(1)
|
||||
)
|
||||
group = existing.scalar_one_or_none()
|
||||
if group:
|
||||
admin_group_ids[label] = group.id
|
||||
else:
|
||||
admin_groups = [
|
||||
("模型设置", "RobotOutlined", 98),
|
||||
("模型配置", "RobotOutlined", 6),
|
||||
("系统设置", "SettingOutlined", 99),
|
||||
]
|
||||
admin_group_ids: dict[str, str] = {}
|
||||
for label, icon, order in admin_groups:
|
||||
gid = generate_id()
|
||||
admin_group_ids[label] = gid
|
||||
db.add(
|
||||
@@ -454,34 +405,28 @@ async def _seed_data():
|
||||
)
|
||||
)
|
||||
|
||||
admin_pages = [
|
||||
("/", "数据概览", "DashboardOutlined", 0, None),
|
||||
("/users", "用户管理", "UserOutlined", 1, None),
|
||||
("/credit-records", "交易流水", "WalletOutlined", 2, None),
|
||||
("/generation-ai", "创作记录", "BulbOutlined", 3, None),
|
||||
("/generation-records", "项目记录", "VideoCameraOutlined", 3, None),
|
||||
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
|
||||
("/notifications", "消息推送", "BellOutlined", 5, None),
|
||||
("/payment-stats", "支付统计", "LineChartOutlined", 6, None),
|
||||
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型设置"),
|
||||
("/models", "模型配置", "RobotOutlined", 1, "模型设置"),
|
||||
("/image-engines", "图片模型", "PictureOutlined", 2, "模型设置"),
|
||||
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型设置"),
|
||||
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
|
||||
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
|
||||
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
|
||||
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
|
||||
("/operation-logs", "操作日志", "DatabaseOutlined", 5, "系统设置"),
|
||||
("/oauthapp-list", "授权应用", "MenuOutlined", 28, "系统设置"),
|
||||
]
|
||||
for path, label, icon, order, parent_group in admin_pages:
|
||||
existing = await db.execute(
|
||||
select(MenuConfig).where(
|
||||
MenuConfig.path == path,
|
||||
MenuConfig.menu_target == "admin",
|
||||
).limit(1)
|
||||
)
|
||||
if not existing.scalar_one_or_none():
|
||||
admin_pages = [
|
||||
("/", "数据概览", "DashboardOutlined", 0, None),
|
||||
("/users", "用户管理", "UserOutlined", 1, None),
|
||||
("/credit-records", "交易流水", "WalletOutlined", 2, None),
|
||||
("/generation-ai", "创作记录", "BulbOutlined", 3, None),
|
||||
("/generation-records", "项目记录", "VideoCameraOutlined", 3, None),
|
||||
("/recharge-packages", "充值套餐", "GiftOutlined", 4, None),
|
||||
("/notifications", "消息推送", "BellOutlined", 5, None),
|
||||
("/payment-stats", "支付统计", "LineChartOutlined", 6, None),
|
||||
("/video-engines", "视频引擎", "PlayCircleOutlined", 0, "模型设置"),
|
||||
("/models", "模型配置", "RobotOutlined", 1, "模型设置"),
|
||||
("/image-engines", "图片模型", "PictureOutlined", 2, "模型设置"),
|
||||
("/credit-ratios", "积分比例", "CalculatorOutlined", 3, "模型设置"),
|
||||
("/payment", "支付配置", "DollarOutlined", 1, "系统设置"),
|
||||
("/industries", "行业配置", "AppstoreOutlined", 2, "系统设置"),
|
||||
("/menu-configs", "菜单配置", "SettingOutlined", 3, "系统设置"),
|
||||
("/settings", "系统设置", "SettingOutlined", 4, "系统设置"),
|
||||
("/operation-logs", "操作日志", "DatabaseOutlined", 5, "系统设置"),
|
||||
("/oauthapp-list", "授权应用", "MenuOutlined", 28, "系统设置"),
|
||||
("/contact-requests", "联系请求", "MessageCircleOutlined", 29, "系统设置"),
|
||||
]
|
||||
for path, label, icon, order, parent_group in admin_pages:
|
||||
db.add(
|
||||
MenuConfig(
|
||||
id=generate_id(),
|
||||
@@ -495,6 +440,8 @@ async def _seed_data():
|
||||
parent_id=admin_group_ids.get(parent_group),
|
||||
)
|
||||
)
|
||||
|
||||
logging.info("Default menu configs inserted successfully")
|
||||
|
||||
# Seed recharge packages
|
||||
from app.models.recharge_package import RechargePackage
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Boolean, ForeignKey, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.models.base import Base, TimestampMixin
|
||||
|
||||
|
||||
class ContactRequest(Base, TimestampMixin):
|
||||
__tablename__ = "contact_requests"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id"), index=True)
|
||||
phone: Mapped[str] = mapped_column(String(20), index=True)
|
||||
company_name: Mapped[str] = mapped_column(String(128))
|
||||
industry: Mapped[str] = mapped_column(String(64))
|
||||
name: Mapped[str] = mapped_column(String(64))
|
||||
message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
is_handled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
@@ -0,0 +1,31 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ContactRequestCreate(BaseModel):
|
||||
phone: str = Field(..., description="手机号")
|
||||
company_name: str = Field(..., description="公司名称")
|
||||
industry: str = Field(..., description="行业")
|
||||
name: str = Field(..., description="姓名")
|
||||
message: str | None = Field(None, description="留言")
|
||||
|
||||
|
||||
class ContactRequestOut(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
phone: str
|
||||
company_name: str
|
||||
industry: str
|
||||
name: str
|
||||
message: str | None
|
||||
is_handled: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ContactRequestListOut(BaseModel):
|
||||
items: list[ContactRequestOut]
|
||||
total: int
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select, update
|
||||
@@ -15,42 +14,12 @@ from app.models.user_oauth_account import UserOAuthAccount
|
||||
from app.models.pre_test_template import PreTestTemplate
|
||||
from app.utils.id_gen import generate_id
|
||||
from app.utils.douyinApi import DouyinApi
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
|
||||
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
logger = logging.getLogger("upload_queue")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class DailyRotatingFileHandler(logging.FileHandler):
|
||||
def __init__(self, directory, encoding=None):
|
||||
self.directory = directory
|
||||
filename = self._get_log_filename()
|
||||
super().__init__(filename, encoding=encoding)
|
||||
|
||||
def _get_log_filename(self):
|
||||
return os.path.join(self.directory, f"upload_queue-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.log")
|
||||
|
||||
def emit(self, record):
|
||||
current_filename = self._get_log_filename()
|
||||
if self.baseFilename != current_filename:
|
||||
self.close()
|
||||
self.baseFilename = current_filename
|
||||
self.stream = self._open()
|
||||
super().emit(record)
|
||||
|
||||
|
||||
if not logger.handlers:
|
||||
handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8")
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
|
||||
logger.addHandler(handler)
|
||||
|
||||
logger = get_logger("upload_queue", "upload_queue")
|
||||
douyin_api = DouyinApi()
|
||||
|
||||
#上传素材队列,处理上传素材的任务
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from datetime import datetime, timezone
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
|
||||
from sqlalchemy import select, update
|
||||
@@ -11,38 +9,9 @@ from app.models.base import async_session
|
||||
from app.models.resources_material import ResourcesMaterial
|
||||
from app.models.user_oauth import UserOAuth
|
||||
from app.utils.douyinApi import DouyinApi
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
#前测结果和上传素材属于一种任务,放到一起日志里边
|
||||
logger = logging.getLogger("upload_queue")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class DailyRotatingFileHandler(logging.FileHandler):
|
||||
def __init__(self, directory, encoding=None):
|
||||
self.directory = directory
|
||||
filename = self._get_log_filename()
|
||||
super().__init__(filename, encoding=encoding)
|
||||
|
||||
def _get_log_filename(self):
|
||||
return os.path.join(self.directory, f"pre_test_result_task-{datetime.now(timezone.utc).strftime('%Y-%m-%d')}.log")
|
||||
|
||||
def emit(self, record):
|
||||
current_filename = self._get_log_filename()
|
||||
if self.baseFilename != current_filename:
|
||||
self.close()
|
||||
self.baseFilename = current_filename
|
||||
self.stream = self._open()
|
||||
super().emit(record)
|
||||
|
||||
|
||||
if not logger.handlers:
|
||||
handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8")
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
|
||||
logger.addHandler(handler)
|
||||
|
||||
logger = get_logger("pre_test_result_task", "pre_test_result_task")
|
||||
douyin_api = DouyinApi()
|
||||
|
||||
#获取前测结果并更新数据库,计划任务,每2分钟执行一次
|
||||
|
||||
@@ -2,9 +2,6 @@ from datetime import datetime, timedelta, timezone
|
||||
import asyncio
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -14,37 +11,10 @@ from app.models.user_oauth_app import UserOAuthApp
|
||||
from app.models.base import async_session
|
||||
from app.config import settings
|
||||
from app.utils.redis import get_redis
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
REDIS_KEY = "douyin:tokens"
|
||||
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
|
||||
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
logger = logging.getLogger("token_refresh")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
def get_log_filename():
|
||||
return os.path.join(LOG_DIR, f"token_refresh-{datetime.now().strftime('%Y-%m-%d')}.log")
|
||||
|
||||
class DailyRotatingFileHandler(logging.FileHandler):
|
||||
def __init__(self, directory, encoding=None):
|
||||
self.directory = directory
|
||||
filename = get_log_filename()
|
||||
super().__init__(filename, encoding=encoding)
|
||||
|
||||
def emit(self, record):
|
||||
current_filename = get_log_filename()
|
||||
if self.baseFilename != current_filename:
|
||||
self.close()
|
||||
self.baseFilename = current_filename
|
||||
self.stream = self._open()
|
||||
super().emit(record)
|
||||
|
||||
handler = DailyRotatingFileHandler(LOG_DIR, encoding="utf-8")
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
logger.addHandler(handler)
|
||||
logger = get_logger("token_refresh", "token_refresh")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ class DouyinApi:
|
||||
oauth_id,
|
||||
url,
|
||||
'POST',
|
||||
options
|
||||
options,
|
||||
request_count = 3
|
||||
)
|
||||
|
||||
#上传视频素材
|
||||
@@ -52,7 +53,8 @@ class DouyinApi:
|
||||
oauth_id,
|
||||
url,
|
||||
'POST',
|
||||
options
|
||||
options,
|
||||
request_count = 3
|
||||
)
|
||||
|
||||
#获取区域信息
|
||||
|
||||
@@ -11,6 +11,9 @@ from app.models.user_oauth import UserOAuth
|
||||
from app.models.user_oauth_app import UserOAuthApp
|
||||
from app.models.base import async_session
|
||||
from app.utils.redis import get_redis
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("douyin_request", "douyin_request")
|
||||
|
||||
|
||||
class DouyinRequest:
|
||||
@@ -244,8 +247,6 @@ class DouyinRequest:
|
||||
await asyncio.sleep(i * 10)
|
||||
continue
|
||||
|
||||
res = json.dumps(data) if 'data' in locals() else ''
|
||||
|
||||
options_log = {}
|
||||
if options:
|
||||
for key, value in options.items():
|
||||
@@ -254,10 +255,31 @@ class DouyinRequest:
|
||||
else:
|
||||
options_log[key] = value
|
||||
|
||||
raise RuntimeError(
|
||||
f'DouYin API request failed after 5 retries. '
|
||||
f'url:{url};oauthId:{oauth_id};options:{json.dumps(options_log)};response:{res}'
|
||||
res = json.dumps(data) if 'data' in locals() else ''
|
||||
|
||||
logger.error(
|
||||
f'DouYin API request failed after {request_count} retries. '
|
||||
f'url:{url};method:{method};oauth_id:{oauth_id};options:{json.dumps(options_log)};response:{res}'
|
||||
)
|
||||
|
||||
if 'data' in locals() and data.get('code', 0) != 0:
|
||||
raise ValueError(f'接口返回错误[code:{data.get("code", "接口编码")}]{data.get("message", "接口返回错误")}')
|
||||
else:
|
||||
raise ValueError('网络错误,稍后重试。')
|
||||
|
||||
# options_log = {}
|
||||
# if options:
|
||||
# for key, value in options.items():
|
||||
# if key == 'files':
|
||||
# options_log[key] = {k: (v[0], 'bytes_content', v[2]) for k, v in value.items()}
|
||||
# else:
|
||||
# options_log[key] = value
|
||||
# raise RuntimeError(
|
||||
# f'DouYin API request failed after 5 retries. '
|
||||
# f'url:{url};oauthId:{oauth_id};options:{json.dumps(options_log)};response:{res}'
|
||||
# )
|
||||
# if code != 0:
|
||||
# raise ValueError(f'response:{res}')
|
||||
|
||||
# 无token请求
|
||||
async def request_with_context(
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def get_logger(name: str, log_filename: str) -> logging.Logger:
|
||||
"""
|
||||
创建并配置一个每日滚动的日志记录器
|
||||
|
||||
Args:
|
||||
name: 日志记录器名称
|
||||
log_filename: 日志文件名(不含日期和扩展名)
|
||||
|
||||
Returns:
|
||||
配置好的日志记录器对象
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs")
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
class DailyRotatingFileHandler(logging.FileHandler):
|
||||
def __init__(self, directory, filename_prefix, encoding=None):
|
||||
self.directory = directory
|
||||
self.filename_prefix = filename_prefix
|
||||
filename = self._get_log_filename()
|
||||
super().__init__(filename, encoding=encoding)
|
||||
|
||||
def _get_log_filename(self):
|
||||
return os.path.join(self.directory, f"{self.filename_prefix}-{datetime.now().strftime('%Y-%m-%d')}.log")
|
||||
|
||||
def emit(self, record):
|
||||
current_filename = self._get_log_filename()
|
||||
if self.baseFilename != current_filename:
|
||||
self.close()
|
||||
self.baseFilename = current_filename
|
||||
self.stream = self._open()
|
||||
super().emit(record)
|
||||
|
||||
handler = DailyRotatingFileHandler(LOG_DIR, log_filename, encoding="utf-8")
|
||||
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logger
|
||||
@@ -11,7 +11,7 @@ Requires-Dist: alembic>=1.14.0
|
||||
Requires-Dist: pydantic>=2.10.0
|
||||
Requires-Dist: pydantic-settings>=2.6.0
|
||||
Requires-Dist: pyjwt>=2.10.0
|
||||
Requires-Dist: passlib[bcrypt]>=1.7.4
|
||||
Requires-Dist: bcrypt>=4.0.0
|
||||
Requires-Dist: httpx>=0.28.0
|
||||
Requires-Dist: python-multipart>=0.0.17
|
||||
Requires-Dist: cryptography>=44.0.0
|
||||
@@ -22,6 +22,12 @@ Requires-Dist: redis>=5.2.0; extra == "redis"
|
||||
Provides-Extra: celery
|
||||
Requires-Dist: celery>=5.4.0; extra == "celery"
|
||||
Requires-Dist: redis>=5.2.0; extra == "celery"
|
||||
Provides-Extra: alipay
|
||||
Requires-Dist: alipay-sdk-python>=3.7.1160; extra == "alipay"
|
||||
Provides-Extra: wxpay
|
||||
Requires-Dist: wechatpayv3>=2.0.2; extra == "wxpay"
|
||||
Provides-Extra: volc
|
||||
Requires-Dist: volcengine-python-sdk>=1.1.0; extra == "volc"
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
||||
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
||||
|
||||
@@ -4,56 +4,196 @@ app/config.py
|
||||
app/dependencies.py
|
||||
app/main.py
|
||||
app/api/__init__.py
|
||||
app/api/admin/__init__.py
|
||||
app/api/admin/video_prompt_schema_config.py
|
||||
app/api/v1/__init__.py
|
||||
app/api/v1/admin.py
|
||||
app/api/v1/auth.py
|
||||
app/api/v1/captcha.py
|
||||
app/api/v1/contact.py
|
||||
app/api/v1/credits.py
|
||||
app/api/v1/generation.py
|
||||
app/api/v1/generation_ai.py
|
||||
app/api/v1/hot_opening_replicate.py
|
||||
app/api/v1/image_engines.py
|
||||
app/api/v1/industries.py
|
||||
app/api/v1/material_consumption.py
|
||||
app/api/v1/menu_configs.py
|
||||
app/api/v1/notifications.py
|
||||
app/api/v1/open_type.py
|
||||
app/api/v1/payments.py
|
||||
app/api/v1/pre_test_template.py
|
||||
app/api/v1/projects.py
|
||||
app/api/v1/recharge_packages.py
|
||||
app/api/v1/resources_material.py
|
||||
app/api/v1/shot_replicate.py
|
||||
app/api/v1/sms.py
|
||||
app/api/v1/test.py
|
||||
app/api/v1/upload_material.py
|
||||
app/api/v1/user_oauth.py
|
||||
app/api/v1/user_oauth_app.py
|
||||
app/api/v1/video_engines.py
|
||||
app/enums/__init__.py
|
||||
app/enums/common.py
|
||||
app/enums/credit_record.py
|
||||
app/enums/hot_opening_replicate.py
|
||||
app/enums/module_generation_flow.py
|
||||
app/enums/shot_replicate.py
|
||||
app/enums/token_usage.py
|
||||
app/enums/user.py
|
||||
app/enums/video_prompt_schema.py
|
||||
app/middleware/__init__.py
|
||||
app/middleware/anti_crawler.py
|
||||
app/middleware/logging.py
|
||||
app/middleware/rate_limit.py
|
||||
app/middleware/request_encrypt.py
|
||||
app/models/__init__.py
|
||||
app/models/base.py
|
||||
app/models/chat_generation_task.py
|
||||
app/models/chat_generation_task_event.py
|
||||
app/models/chat_provider_call_log.py
|
||||
app/models/contact_request.py
|
||||
app/models/credit_ratio.py
|
||||
app/models/credit_record.py
|
||||
app/models/generated_resource.py
|
||||
app/models/generation_record.py
|
||||
app/models/image_engine.py
|
||||
app/models/industry_config.py
|
||||
app/models/material_cost.py
|
||||
app/models/menu_config.py
|
||||
app/models/model_config.py
|
||||
app/models/module_generation_project.py
|
||||
app/models/module_generation_step.py
|
||||
app/models/notification.py
|
||||
app/models/notification_read.py
|
||||
app/models/open_type.py
|
||||
app/models/operation_log.py
|
||||
app/models/payment_order.py
|
||||
app/models/pre_test_template.py
|
||||
app/models/project.py
|
||||
app/models/recharge_package.py
|
||||
app/models/resources_material.py
|
||||
app/models/shot_replicate_segment.py
|
||||
app/models/shot_replicate_task_set.py
|
||||
app/models/system_config.py
|
||||
app/models/token_usage.py
|
||||
app/models/upload_task.py
|
||||
app/models/user.py
|
||||
app/models/user_oauth.py
|
||||
app/models/user_oauth_account.py
|
||||
app/models/user_oauth_app.py
|
||||
app/models/user_resource_month_stat.py
|
||||
app/models/user_resource_total_stat.py
|
||||
app/models/video_engine.py
|
||||
app/schemas/__init__.py
|
||||
app/schemas/admin.py
|
||||
app/schemas/auth.py
|
||||
app/schemas/captcha.py
|
||||
app/schemas/common.py
|
||||
app/schemas/contact.py
|
||||
app/schemas/credit.py
|
||||
app/schemas/credit_ratio.py
|
||||
app/schemas/generation.py
|
||||
app/schemas/generation_ai.py
|
||||
app/schemas/hot_opening_replicate.py
|
||||
app/schemas/image_engine.py
|
||||
app/schemas/industry.py
|
||||
app/schemas/menu.py
|
||||
app/schemas/notification.py
|
||||
app/schemas/open_type.py
|
||||
app/schemas/payment.py
|
||||
app/schemas/pre_test_template.py
|
||||
app/schemas/project.py
|
||||
app/schemas/recharge_package.py
|
||||
app/schemas/resources_material.py
|
||||
app/schemas/shot_replicate.py
|
||||
app/schemas/sms.py
|
||||
app/schemas/user.py
|
||||
app/schemas/user_oauth.py
|
||||
app/schemas/user_oauth_app.py
|
||||
app/schemas/video_engine.py
|
||||
app/schemas/video_prompt_schema_config.py
|
||||
app/services/__init__.py
|
||||
app/services/admin_credit_record_service.py
|
||||
app/services/auth.py
|
||||
app/services/captcha.py
|
||||
app/services/celery_download_recovery_service.py
|
||||
app/services/credit_ratio_service.py
|
||||
app/services/credit_record_meta_service.py
|
||||
app/services/credits.py
|
||||
app/services/error_codes.py
|
||||
app/services/generation_ai_service.py
|
||||
app/services/generation_billing_service.py
|
||||
app/services/generation_download_service.py
|
||||
app/services/generation_log_service.py
|
||||
app/services/generation_module_hook_service.py
|
||||
app/services/generation_prompt_service.py
|
||||
app/services/generation_provider_service.py
|
||||
app/services/generation_provider_types.py
|
||||
app/services/generation_recovery_service.py
|
||||
app/services/generation_refund_service.py
|
||||
app/services/generation_task_factory_service.py
|
||||
app/services/hot_opening_replicate_service.py
|
||||
app/services/hot_opening_video_prompt_service.py
|
||||
app/services/image_gen.py
|
||||
app/services/llm.py
|
||||
app/services/log_config.py
|
||||
app/services/material_consumption_queue.py
|
||||
app/services/material_consumption_service.py
|
||||
app/services/module_async_recovery_service.py
|
||||
app/services/module_generation_flow_base_service.py
|
||||
app/services/module_generation_log_service.py
|
||||
app/services/module_generation_step_common_service.py
|
||||
app/services/module_generation_step_update_service.py
|
||||
app/services/notification.py
|
||||
app/services/operation_log.py
|
||||
app/services/payment.py
|
||||
app/services/pre_test_template_service.py
|
||||
app/services/provider_limit.py
|
||||
app/services/redis_registry_service.py
|
||||
app/services/resource_accounting_service.py
|
||||
app/services/resource_signed_url_service.py
|
||||
app/services/resources_material_service.py
|
||||
app/services/shot_replicate_flow_service.py
|
||||
app/services/shot_replicate_recovery_service.py
|
||||
app/services/shot_replicate_taskset_service.py
|
||||
app/services/shot_video_analysis_service.py
|
||||
app/services/shot_video_split_service.py
|
||||
app/services/sms.py
|
||||
app/services/upload_material_service.py
|
||||
app/services/upload_queue.py
|
||||
app/services/upload_video_asset_service.py
|
||||
app/services/user_oauth_app_service.py
|
||||
app/services/user_oauth_service.py
|
||||
app/services/video_cover_service.py
|
||||
app/services/video_gen.py
|
||||
app/services/video_prompt_schema_config_service.py
|
||||
app/services/video_queue.py
|
||||
app/services/video_url.py
|
||||
app/tasks/__init__.py
|
||||
app/tasks/async_runner.py
|
||||
app/tasks/celery_app.py
|
||||
app/tasks/cleanup.py
|
||||
app/tasks/generation_create_tasks.py
|
||||
app/tasks/generation_download_tasks.py
|
||||
app/tasks/generation_poll_tasks.py
|
||||
app/tasks/generation_recovery_tasks.py
|
||||
app/tasks/hot_opening_replicate_tasks.py
|
||||
app/tasks/material_consumption_task.py
|
||||
app/tasks/module_async_recovery_tasks.py
|
||||
app/tasks/pre_test_result_task.py
|
||||
app/tasks/shot_replicate_flow_tasks.py
|
||||
app/tasks/shot_replicate_tasks.py
|
||||
app/tasks/token_refresh_task.py
|
||||
app/tasks/user_oauth_tasks.py
|
||||
app/tasks/video_generation.py
|
||||
app/utils/__init__.py
|
||||
app/utils/area.py
|
||||
app/utils/douyinApi.py
|
||||
app/utils/douyinRequest.py
|
||||
app/utils/exceptions.py
|
||||
app/utils/id_gen.py
|
||||
app/utils/logger.py
|
||||
app/utils/redis.py
|
||||
app/utils/security.py
|
||||
videogen_api.egg-info/PKG-INFO
|
||||
|
||||
@@ -6,11 +6,14 @@ alembic>=1.14.0
|
||||
pydantic>=2.10.0
|
||||
pydantic-settings>=2.6.0
|
||||
pyjwt>=2.10.0
|
||||
passlib[bcrypt]>=1.7.4
|
||||
bcrypt>=4.0.0
|
||||
httpx>=0.28.0
|
||||
python-multipart>=0.0.17
|
||||
cryptography>=44.0.0
|
||||
|
||||
[alipay]
|
||||
alipay-sdk-python>=3.7.1160
|
||||
|
||||
[celery]
|
||||
celery>=5.4.0
|
||||
redis>=5.2.0
|
||||
@@ -25,3 +28,9 @@ asyncpg>=0.30.0
|
||||
|
||||
[redis]
|
||||
redis>=5.2.0
|
||||
|
||||
[volc]
|
||||
volcengine-python-sdk>=1.1.0
|
||||
|
||||
[wxpay]
|
||||
wechatpayv3>=2.0.2
|
||||
|
||||
+95
-95
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-D9QoFfGP.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-b7j6q-om.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-eLRg4pQk.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -666,6 +666,19 @@ export async function getMaterialConsumptionFields(): Promise<any> {
|
||||
return api.get('/material-consumption/fields');
|
||||
}
|
||||
|
||||
// ── Contact ────────────────────────────────────────────────
|
||||
export interface ContactRequestParams {
|
||||
phone: string;
|
||||
company_name: string;
|
||||
industry: string;
|
||||
name: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function createContactRequest(params: ContactRequestParams): Promise<any> {
|
||||
return api.post('/contact/request', params);
|
||||
}
|
||||
|
||||
// 查询上传素材列表
|
||||
export interface ResourcesMaterialListParams {
|
||||
advertiser_id?: string;
|
||||
|
||||
@@ -53,10 +53,13 @@ import {
|
||||
ApiOutlined,
|
||||
DatabaseOutlined,
|
||||
CloudServerOutlined,
|
||||
MessageOutlined,
|
||||
DownOutlined,
|
||||
InfoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount } from '../../api';
|
||||
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest } from '../../api';
|
||||
import NotificationPopup from '../NotificationPopup';
|
||||
|
||||
interface MenuConfig {
|
||||
@@ -167,6 +170,10 @@ const AppLayout: React.FC = () => {
|
||||
const countdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const currentOrderNoRef = useRef<string | null>(null);
|
||||
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
|
||||
const [contactModalOpen, setContactModalOpen] = useState(false);
|
||||
const [contactForm] = Form.useForm();
|
||||
const [contactHovered, setContactHovered] = useState(false);
|
||||
const [submittingContact, setSubmittingContact] = useState(false);
|
||||
|
||||
// LocalStorage keys
|
||||
const PENDING_ORDER_KEY = 'pending_payment_order';
|
||||
@@ -316,6 +323,31 @@ const AppLayout: React.FC = () => {
|
||||
setRechargeModalOpen(true);
|
||||
};
|
||||
|
||||
const handleContactSubmit = async () => {
|
||||
if (!user) {
|
||||
message.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const values = await contactForm.validateFields();
|
||||
setSubmittingContact(true);
|
||||
await createContactRequest({
|
||||
phone: values.phone,
|
||||
company_name: values.companyName,
|
||||
industry: values.industry,
|
||||
name: values.name,
|
||||
message: values.message,
|
||||
});
|
||||
message.success('提交成功,我们会尽快与您联系');
|
||||
setContactModalOpen(false);
|
||||
contactForm.resetFields();
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '提交失败');
|
||||
} finally {
|
||||
setSubmittingContact(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollingTimerRef.current) {
|
||||
clearInterval(pollingTimerRef.current);
|
||||
@@ -463,9 +495,9 @@ const AppLayout: React.FC = () => {
|
||||
<div key={item.id} onClick={() => item.path && navigate(item.path)} style={{
|
||||
display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
gap: 12,
|
||||
padding: depth > 0 ? '8px 14px 8px 36px' : '10px 16px',
|
||||
borderRadius: 12, margin: '2px 6px', cursor: 'pointer',
|
||||
gap: 10,
|
||||
padding: depth > 0 ? '6px 12px 6px 32px' : '6px 14px',
|
||||
borderRadius: 12, margin: '1px 4px', cursor: 'pointer',
|
||||
fontSize: depth > 0 ? 13 : 14, fontWeight: isActive ? 600 : 400,
|
||||
color: isActive ? '#4f46e5' : '#475569',
|
||||
background: isActive ? 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.08) 100%)' : 'transparent',
|
||||
@@ -495,11 +527,11 @@ const AppLayout: React.FC = () => {
|
||||
items.push(
|
||||
<div key={item.id}>
|
||||
<div style={{
|
||||
color: '#94a3b8', fontSize: 12, fontWeight: 600,
|
||||
padding: '12px 16px 6px', letterSpacing: 0.5, textTransform: 'uppercase',
|
||||
}}>
|
||||
{item.label}
|
||||
</div>
|
||||
color: '#94a3b8', fontSize: 12, fontWeight: 600,
|
||||
padding: '10px 14px 4px', letterSpacing: 0.5, textTransform: 'uppercase',
|
||||
}}>
|
||||
{item.label}
|
||||
</div>
|
||||
{children.map(c => renderMenuItem(c, 1))}
|
||||
</div>
|
||||
);
|
||||
@@ -674,6 +706,20 @@ const AppLayout: React.FC = () => {
|
||||
<Typography.Text style={{ color: '#64748b', letterSpacing: 0 }}>当前积分余额</Typography.Text>
|
||||
<Typography.Text strong style={{ color: '#6366f1', fontSize: 20, fontWeight: 600 }}>{user?.credits ?? 0}</Typography.Text>
|
||||
</Space>
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
background: 'rgba(99, 102, 241, 0.06)',
|
||||
borderRadius: 10,
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<InfoOutlined style={{ color: '#6366f1', fontSize: 14 }} />
|
||||
<Typography.Text style={{ color: '#ff0000ff', fontSize: 13 }}>
|
||||
当前平台仅支持支付宝/微信扫码充值,如需转账支付请联系我们
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
{rechargeOptions.map((opt, idx) => {
|
||||
const g = GRADIENTS[idx % GRADIENTS.length];
|
||||
@@ -958,6 +1004,142 @@ const AppLayout: React.FC = () => {
|
||||
</Modal>
|
||||
|
||||
<NotificationPopup />
|
||||
|
||||
{/* Contact Button */}
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
right: 24,
|
||||
bottom: 24,
|
||||
zIndex: 1000,
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 64,
|
||||
bottom: 8,
|
||||
padding: '8px 16px',
|
||||
background: '#1e293b',
|
||||
color: '#ffffff',
|
||||
borderRadius: 8,
|
||||
fontSize: 13,
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: contactHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s ease',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
联系我们
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setContactModalOpen(true)}
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
color: '#ffffff',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 4px 20px rgba(99, 102, 241, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1.05)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 24px rgba(99, 102, 241, 0.5)';
|
||||
setContactHovered(true);
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 20px rgba(99, 102, 241, 0.4)';
|
||||
setContactHovered(false);
|
||||
}}
|
||||
>
|
||||
<MessageOutlined style={{ fontSize: 20 }} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Modal */}
|
||||
<Modal
|
||||
title={<Space><MessageOutlined />联系我们</Space>}
|
||||
open={contactModalOpen}
|
||||
onCancel={() => { setContactModalOpen(false); contactForm.resetFields(); }}
|
||||
footer={null}
|
||||
width={480}
|
||||
>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Form form={contactForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="姓名"
|
||||
rules={[{ required: true, message: '请输入姓名' }]}
|
||||
>
|
||||
<Input placeholder="请输入您的姓名" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="手机号"
|
||||
rules={[
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入您的手机号" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="companyName"
|
||||
label="公司名称"
|
||||
rules={[{ required: true, message: '请输入公司名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入公司名称" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="industry"
|
||||
label="您的行业"
|
||||
rules={[{ required: true, message: '请输入您的行业' }]}
|
||||
>
|
||||
<Input placeholder="请输入您的行业" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="message" label="留言(选填)">
|
||||
<Input.TextArea
|
||||
placeholder="请输入您的需求或问题"
|
||||
rows={3}
|
||||
style={{ borderRadius: 10 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<div style={{ marginTop: 16, display: 'flex', gap: 12 }}>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={() => { setContactModalOpen(false); contactForm.resetFields(); }}
|
||||
style={{ borderRadius: 10, flex: 1 }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleContactSubmit}
|
||||
loading={submittingContact}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Popover, Tag, List, Descriptions, Typography } from 'antd';
|
||||
|
||||
interface PreResultData {
|
||||
video_id: string; //视频id
|
||||
advertiser_id: number; //广告主id
|
||||
material_id: string; //素材id
|
||||
is_ad_high_quality_material: string; //是否优质素材
|
||||
is_ecp_high_quality_material: string; //是否千川优质素材
|
||||
is_inefficient_material: string; //是否低效素材
|
||||
is_first_publish_material: string; //是否首发素材
|
||||
not_ad_high_quality_reason: string[] | null; //AD非优质原因
|
||||
not_ecp_high_quality_reason: string[] | null; //千川非优质原因
|
||||
is_local_high_quality_material: string; //是否本地推优质素材
|
||||
}
|
||||
|
||||
interface PreResultDisplayProps {
|
||||
preResult: string;
|
||||
}
|
||||
|
||||
const qualityConfig: Record<string, { color: string; label: string }> = {
|
||||
YES: { color: 'green', label: '是' },
|
||||
NO: { color: 'red', label: '否' },
|
||||
UNKNOWN: { color: 'default', label: '未知' },
|
||||
};
|
||||
|
||||
const PreResultDisplay: React.FC<PreResultDisplayProps> = ({ preResult }) => {
|
||||
const [parsedData, setParsedData] = useState<PreResultData | null>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!preResult) {
|
||||
setParsedData(null);
|
||||
setHasError(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(preResult);
|
||||
setParsedData(data);
|
||||
setHasError(false);
|
||||
} catch {
|
||||
setParsedData(null);
|
||||
setHasError(true);
|
||||
}
|
||||
}, [preResult]);
|
||||
|
||||
if (!preResult || hasError || !parsedData) {
|
||||
return <span style={{ color: '#64748b' }}>-</span>;
|
||||
}
|
||||
|
||||
const allQualityFields = [
|
||||
parsedData.is_ad_high_quality_material,
|
||||
parsedData.is_ecp_high_quality_material,
|
||||
parsedData.is_local_high_quality_material,
|
||||
];
|
||||
|
||||
const hasNoQuality = allQualityFields.some((val) => val === 'NO');
|
||||
const allYes = allQualityFields.every((val) => val === 'YES');
|
||||
|
||||
let statusTag;
|
||||
if (hasNoQuality) {
|
||||
statusTag = <Tag color="red">非优质</Tag>;
|
||||
} else if (allYes) {
|
||||
statusTag = <Tag color="green">优质</Tag>;
|
||||
} else {
|
||||
statusTag = <Tag color="default">待评估</Tag>;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div style={{ maxWidth: 500 }}>
|
||||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12 }}>前测结果详情</Typography.Text>
|
||||
|
||||
<Descriptions column={1} size="small" style={{ marginBottom: 12 }}>
|
||||
<Descriptions.Item label="视频ID">{parsedData.video_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="广告主ID">{parsedData.advertiser_id}</Descriptions.Item>
|
||||
<Descriptions.Item label="素材ID">{parsedData.material_id}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Typography.Text strong style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 8 }}>质量评估</Typography.Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 12 }}>
|
||||
<Tag color={qualityConfig[parsedData.is_ad_high_quality_material]?.color}>
|
||||
AD优质素材: {qualityConfig[parsedData.is_ad_high_quality_material]?.label}
|
||||
</Tag>
|
||||
<Tag color={qualityConfig[parsedData.is_ecp_high_quality_material]?.color}>
|
||||
千川优质素材: {qualityConfig[parsedData.is_ecp_high_quality_material]?.label}
|
||||
</Tag>
|
||||
<Tag color={qualityConfig[parsedData.is_local_high_quality_material]?.color}>
|
||||
本地推优质素材: {qualityConfig[parsedData.is_local_high_quality_material]?.label}
|
||||
</Tag>
|
||||
<Tag color={qualityConfig[parsedData.is_inefficient_material]?.color}>
|
||||
低效素材: {qualityConfig[parsedData.is_inefficient_material]?.label}
|
||||
</Tag>
|
||||
<Tag color={qualityConfig[parsedData.is_first_publish_material]?.color}>
|
||||
首发素材: {qualityConfig[parsedData.is_first_publish_material]?.label}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{parsedData.not_ad_high_quality_reason && parsedData.not_ad_high_quality_reason.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Typography.Text strong style={{ fontSize: 12, color: '#ef4444', display: 'block', marginBottom: 8 }}>
|
||||
AD非优质原因
|
||||
</Typography.Text>
|
||||
<List
|
||||
dataSource={parsedData.not_ad_high_quality_reason}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item key={index} style={{ padding: '4px 0', fontSize: 12, color: '#64748b' }}>
|
||||
{index + 1}. {item}
|
||||
</List.Item>
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{parsedData.not_ecp_high_quality_reason && parsedData.not_ecp_high_quality_reason.length > 0 && (
|
||||
<div>
|
||||
<Typography.Text strong style={{ fontSize: 12, color: '#ef4444', display: 'block', marginBottom: 8 }}>
|
||||
千川非优质原因
|
||||
</Typography.Text>
|
||||
<List
|
||||
dataSource={parsedData.not_ecp_high_quality_reason}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item key={index} style={{ padding: '4px 0', fontSize: 12, color: '#64748b' }}>
|
||||
{index + 1}. {item}
|
||||
</List.Item>
|
||||
)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} title={null} trigger="hover">
|
||||
{statusTag}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreResultDisplay;
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
StarOutlined,
|
||||
LockOutlined,
|
||||
GiftOutlined,
|
||||
SparklesOutlined,
|
||||
ThunderboltOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
HeartOutlined,
|
||||
@@ -12,75 +12,13 @@ import {
|
||||
|
||||
const CreativePlazaPage: React.FC = () => {
|
||||
const handleUnlock = () => {
|
||||
message.info('请联系客服开通VIP会员,客服热线:400-888-8888');
|
||||
message.info('请联系客服开通');
|
||||
};
|
||||
|
||||
const caseStudies = [
|
||||
{
|
||||
id: 1,
|
||||
title: '品牌宣传视频',
|
||||
category: '商业广告',
|
||||
description: '为知名品牌打造的创意宣传视频,展现品牌理念与产品特色',
|
||||
tags: ['AI生成', '品牌推广', '创意设计'],
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=professional%20brand%20promotional%20video%20advertisement%20modern&image_size=landscape_16_9',
|
||||
duration: '0:45',
|
||||
quality: '4K',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '产品展示动画',
|
||||
category: '电商展示',
|
||||
description: '3D产品展示动画,让产品细节完美呈现',
|
||||
tags: ['3D渲染', '产品展示', '电商'],
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=3D%20product%20showcase%20animation%20ecommerce&image_size=landscape_16_9',
|
||||
duration: '0:30',
|
||||
quality: '4K',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '教育培训课程',
|
||||
category: '知识分享',
|
||||
description: '生动有趣的教育内容,让学习更加轻松愉快',
|
||||
tags: ['教育', '知识', '在线课程'],
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=educational%20video%20learning%20classroom%20modern&image_size=landscape_16_9',
|
||||
duration: '15:20',
|
||||
quality: '1080P',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '短视频创意',
|
||||
category: '社交媒体',
|
||||
description: '适合各大社交平台的创意短视频内容',
|
||||
tags: ['短视频', '社交', '创意'],
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=creative%20short%20video%20social%20media%20vibrant&image_size=landscape_16_9',
|
||||
duration: '0:15',
|
||||
quality: '1080P',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '企业宣传片',
|
||||
category: '企业形象',
|
||||
description: '全方位展示企业实力与文化的专业宣传片',
|
||||
tags: ['企业', '宣传片', '品牌'],
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=corporate%20video%20company%20profile%20professional&image_size=landscape_16_9',
|
||||
duration: '2:30',
|
||||
quality: '4K',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '动画短片',
|
||||
category: '创意动画',
|
||||
description: '精美的AI生成动画短片,展现无限创意',
|
||||
tags: ['动画', 'AI艺术', '创意'],
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=creative%20animated%20short%20film%20colorful%20artistic&image_size=landscape_16_9',
|
||||
duration: '1:30',
|
||||
quality: '4K',
|
||||
},
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <SparklesOutlined style={{ fontSize: 28, color: '#8b5cf6' }} />,
|
||||
icon: <ThunderboltOutlined style={{ fontSize: 28, color: '#8b5cf6' }} />,
|
||||
title: 'AI智能生成',
|
||||
description: '先进的AI技术,一键生成高质量视频内容',
|
||||
},
|
||||
@@ -176,89 +114,19 @@ const CreativePlazaPage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
{caseStudies.map((caseItem) => (
|
||||
<Card
|
||||
key={caseItem.id}
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.06)',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={caseItem.thumbnail}
|
||||
alt={caseItem.title}
|
||||
style={{ width: '100%', height: 180, objectFit: 'cover' }}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: 12,
|
||||
padding: '4px 12px',
|
||||
background: 'rgba(139,92,246,0.9)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
}}>
|
||||
{caseItem.category}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
padding: '4px 10px',
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
borderRadius: 6,
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
}}>
|
||||
{caseItem.duration} | {caseItem.quality}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Typography.Title level={5} style={{ marginBottom: 8, color: '#1e293b' }}>
|
||||
{caseItem.title}
|
||||
</Typography.Title>
|
||||
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 12, display: 'block' }}>
|
||||
{caseItem.description}
|
||||
</Typography.Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 12 }}>
|
||||
{caseItem.tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
style={{
|
||||
padding: '4px 12px',
|
||||
background: 'rgba(99,102,241,0.08)',
|
||||
borderRadius: 20,
|
||||
color: '#6366f1',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="small"
|
||||
onClick={handleUnlock}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%)',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<SparklesOutlined style={{ marginRight: 4 }} />
|
||||
生成同款
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
background: '#fafafa',
|
||||
padding: 40,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#94a3b8', fontSize: 14 }}>
|
||||
敬请期待更多创意案例
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,21 +39,21 @@ const GeneratedRecord: React.FC = () => {
|
||||
|
||||
// 上传配置弹窗相关状态
|
||||
const [uploadConfigModalVisible, setUploadConfigModalVisible] = useState(false);
|
||||
const [accountIdList, setAccountIdList] = useState<{
|
||||
const [accountIdLists, setAccountIdLists] = useState<{
|
||||
accountId: string;
|
||||
}[]>([]);
|
||||
const [accountIdInput, setAccountIdInput] = useState('');
|
||||
}[][]>([[]]);
|
||||
const [accountIdInputs, setAccountIdInputs] = useState<string[]>(['']);
|
||||
|
||||
const [oauthList, setOauthList] = useState<any[]>([]);
|
||||
const [oauthLoading, setOauthLoading] = useState(false);
|
||||
const [oauthTotal, setOauthTotal] = useState(0);
|
||||
const [selectedOauthItems, setSelectedOauthItems] = useState<{ value: string; label: string } | undefined>(undefined);
|
||||
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 [oauthSelectOpens, setOauthSelectOpens] = useState<boolean[]>([false]);
|
||||
|
||||
// 上传任务历史弹窗相关状态
|
||||
const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false);
|
||||
@@ -682,8 +682,21 @@ const GeneratedRecord: React.FC = () => {
|
||||
message.warning('请先选择要上传的媒体');
|
||||
return;
|
||||
}
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setAccountIdLists([[]]);
|
||||
setAccountIdInputs(['']);
|
||||
setSelectedOauthItems([undefined]);
|
||||
setUploadConfigModalVisible(true);
|
||||
};
|
||||
|
||||
const handleSinglePushToMedia = () => {
|
||||
if (!previewItem) return;
|
||||
const resourceId = getItemResourceId(previewItem);
|
||||
setSelectedItems(new Set([resourceId]));
|
||||
setAccountIdLists([[]]);
|
||||
setAccountIdInputs(['']);
|
||||
setSelectedOauthItems([undefined]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
setUploadConfigModalVisible(true);
|
||||
};
|
||||
|
||||
@@ -743,7 +756,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
|
||||
// 批量上传素材
|
||||
const handleStartBatchUpload = async () => {
|
||||
if (!selectedOauthItems) {
|
||||
const validOauthItems = selectedOauthItems.filter(item => item !== undefined);
|
||||
if (validOauthItems.length === 0) {
|
||||
message.warning('请先选择授权账户');
|
||||
return;
|
||||
}
|
||||
@@ -753,14 +767,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
}
|
||||
setUploading(true);
|
||||
try {
|
||||
const tasks: {
|
||||
advertiser_ids: string[];
|
||||
resource_ids: string[];
|
||||
oauth_id: string;
|
||||
source_model: string;
|
||||
}[] = [];
|
||||
const advertiserIds = accountIdList.map(account => account.accountId);
|
||||
// 创建itemId到item对象的映射
|
||||
const itemMap = new Map<string, any>();
|
||||
recordlist.forEach((group: any) => {
|
||||
group.items.forEach((item: any) => {
|
||||
@@ -769,32 +775,64 @@ const GeneratedRecord: React.FC = () => {
|
||||
});
|
||||
});
|
||||
|
||||
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';
|
||||
const tasks: {
|
||||
advertiser_ids: string[];
|
||||
resource_ids: string[];
|
||||
oauth_id: string;
|
||||
source_model: string;
|
||||
}[] = [];
|
||||
|
||||
for (let i = 0; i < validOauthItems.length; i++) {
|
||||
const oauthItem = validOauthItems[i];
|
||||
const advertiserIds = accountIdLists[i]?.map(account => account.accountId) || [];
|
||||
|
||||
if (advertiserIds.length === 0) {
|
||||
message.warning(`第 ${i + 1} 组授权账户未设置账户ID,已跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
tasks.push({
|
||||
advertiser_ids: advertiserIds,
|
||||
resource_ids: [itemId],
|
||||
oauth_id: selectedOauthItems.value,
|
||||
source_model: sourceModel,
|
||||
const sourceModelMap = new Map<string, string[]>();
|
||||
|
||||
for (const itemId of selectedItems) {
|
||||
const item = itemMap.get(itemId);
|
||||
let sourceModel: string;
|
||||
if (item && hasGeneratedResourceId(item)) {
|
||||
sourceModel = 'generated_resources';
|
||||
} else {
|
||||
sourceModel = filterType === 'project' ? 'generation_records' : 'chat_generation_tasks';
|
||||
}
|
||||
|
||||
if (!sourceModelMap.has(sourceModel)) {
|
||||
sourceModelMap.set(sourceModel, []);
|
||||
}
|
||||
sourceModelMap.get(sourceModel)!.push(itemId);
|
||||
}
|
||||
|
||||
sourceModelMap.forEach((resourceIds, sourceModel) => {
|
||||
tasks.push({
|
||||
advertiser_ids: advertiserIds,
|
||||
resource_ids: resourceIds,
|
||||
oauth_id: oauthItem.value,
|
||||
source_model: sourceModel,
|
||||
});
|
||||
});
|
||||
}
|
||||
await asyncBatchUploadMaterial({ tasks });
|
||||
message.success(`已提交 ${tasks.length} 个上传任务,后台异步处理中`);
|
||||
|
||||
const res = await asyncBatchUploadMaterial({ tasks });
|
||||
if (res.errors?.length > 0) {
|
||||
message.warning(res.message);
|
||||
} else if (res.code === 0) {
|
||||
message.success(res.message);
|
||||
} else {
|
||||
message.error(res.message);
|
||||
}
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
// 关闭弹窗并清理状态
|
||||
setUploadConfigModalVisible(false);
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setSelectedOauthItems(undefined);
|
||||
setAccountIdLists([[]]);
|
||||
setAccountIdInputs(['']);
|
||||
setSelectedOauthItems([undefined]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
} catch (error: any) {
|
||||
@@ -1323,13 +1361,13 @@ const GeneratedRecord: React.FC = () => {
|
||||
|
||||
{/* 上传配置弹窗 */}
|
||||
<Modal
|
||||
title="批量上传配置"
|
||||
title={selectedItems.size === 1 ? '上传配置' : '批量上传配置'}
|
||||
open={uploadConfigModalVisible}
|
||||
onCancel={() => {
|
||||
setUploadConfigModalVisible(false);
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setSelectedOauthItems(undefined);
|
||||
setAccountIdLists([[]]);
|
||||
setAccountIdInputs(['']);
|
||||
setSelectedOauthItems([undefined]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
}}
|
||||
@@ -1433,7 +1471,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
value={materialFileNames.get(itemId) || item?.fileName || ''}
|
||||
value={materialFileNames.has(itemId) ? materialFileNames.get(itemId)! : item?.fileName || ''}
|
||||
onChange={(e) => {
|
||||
const newName = e.target.value;
|
||||
const newNames = new Map(materialFileNames);
|
||||
@@ -1457,147 +1495,196 @@ const GeneratedRecord: React.FC = () => {
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block' }}>
|
||||
选择授权账户
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={selectedOauthItems}
|
||||
onChange={(value) => {
|
||||
setSelectedOauthItems(value as { value: string; label: string } | undefined);
|
||||
}}
|
||||
placeholder="点击选择授权账户"
|
||||
style={{ width: '100%', marginBottom: 16, borderRadius: 8 }}
|
||||
popupRender={() => (
|
||||
<div style={{ padding: 8, width: 800, maxHeight: 500, overflow: 'auto' }}>
|
||||
<Table
|
||||
dataSource={oauthList}
|
||||
columns={[
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '授权账户名称',
|
||||
dataIndex: 'accountName',
|
||||
key: 'accountName',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '授权应用ID',
|
||||
dataIndex: 'appid',
|
||||
key: 'appid',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '授权用户ID',
|
||||
dataIndex: 'accountUserid',
|
||||
key: 'accountUserid',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '授权账户角色',
|
||||
dataIndex: 'accountRole',
|
||||
key: 'accountRole',
|
||||
width: 200,
|
||||
render: (role: string) => {
|
||||
const roleMap: Record<string, string> = {
|
||||
ADVERTISER: '客户',
|
||||
CUSTOMER_ADMIN: '普通版工作台-管理员',
|
||||
CUSTOMER_OPERATOR: '普通版工作台-协作者',
|
||||
AGENT: '代理商',
|
||||
CHILD_AGENT: '二级代理商',
|
||||
PLATFORM_ROLE_STAR: '星图账户',
|
||||
PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户',
|
||||
PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商',
|
||||
PLATFORM_ROLE_STAR_AGENT: '星图代理商',
|
||||
PLATFORM_ROLE_AWEME: '抖音号',
|
||||
PLATFORM_ROLE_STAR_MCN: '星图MCN机构',
|
||||
PLATFORM_ROLE_STAR_ISV: '星图服务商',
|
||||
AGENT_SYSTEM_ACCOUNT: '代理商系统账户',
|
||||
PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商',
|
||||
PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员',
|
||||
PLATFORM_ROLE_LIFE: '抖音来客账户',
|
||||
PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员',
|
||||
PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者',
|
||||
};
|
||||
return roleMap[role] || role;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '授权账户用户名',
|
||||
dataIndex: 'accountUsername',
|
||||
key: 'accountUsername',
|
||||
width: 120,
|
||||
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
|
||||
},
|
||||
]}
|
||||
loading={oauthLoading}
|
||||
pagination={{
|
||||
current: oauthPage,
|
||||
pageSize: oauthPageSize,
|
||||
total: oauthTotal,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
onChange: (page, size) => {
|
||||
setOauthPage(page);
|
||||
setOauthPageSize(size);
|
||||
loadOAuthList(page, size);
|
||||
},
|
||||
}}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
const id = String(record.id);
|
||||
setSelectedOauthItems({ value: id, label: String(record.accountUserid) });
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: selectedOauthItems?.value === String(record.id) ? '#e6f7ff' : undefined,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{selectedOauthItems.map((oauthItem, index) => (
|
||||
<div key={index} style={{ marginBottom: 16, padding: 12, border: '1px solid #e2e8f0', borderRadius: 8 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569' }}>
|
||||
授权账户 {index + 1}
|
||||
</Typography.Text>
|
||||
{selectedOauthItems.length > 1 && (
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => {
|
||||
const newOauthItems = [...selectedOauthItems];
|
||||
const newAccountIdInputs = [...accountIdInputs];
|
||||
const newAccountIdLists = [...accountIdLists];
|
||||
const newOauthSelectOpens = [...oauthSelectOpens];
|
||||
newOauthItems.splice(index, 1);
|
||||
newAccountIdInputs.splice(index, 1);
|
||||
newAccountIdLists.splice(index, 1);
|
||||
newOauthSelectOpens.splice(index, 1);
|
||||
setSelectedOauthItems(newOauthItems);
|
||||
setAccountIdInputs(newAccountIdInputs);
|
||||
setAccountIdLists(newAccountIdLists);
|
||||
setOauthSelectOpens(newOauthSelectOpens);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
open={oauthSelectOpen}
|
||||
onOpenChange={(open) => {
|
||||
setOauthSelectOpen(open);
|
||||
if (open) {
|
||||
loadOAuthList(1, oauthPageSize);
|
||||
}
|
||||
}}
|
||||
labelInValue
|
||||
fieldNames={{ label: 'accountUserid', value: 'id' }}
|
||||
/>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block', marginTop: 16 }}>
|
||||
粘贴账户ID(每行一个或用逗号分隔)
|
||||
</Typography.Text>
|
||||
<Input.TextArea
|
||||
value={accountIdInput}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setAccountIdInput(value);
|
||||
const ids = value.split(/[\n,]/)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
const uniqueIds = [...new Set(ids)];
|
||||
const textAccounts = uniqueIds.map(id => ({ accountId: id }));
|
||||
const seen = new Set<string>();
|
||||
const finalAccounts = textAccounts.filter(a => {
|
||||
if (seen.has(a.accountId)) return false;
|
||||
seen.add(a.accountId);
|
||||
return true;
|
||||
});
|
||||
setAccountIdList(finalAccounts);
|
||||
}}
|
||||
placeholder="粘贴账户ID,每行一个或用逗号分隔,例如:
|
||||
<Select
|
||||
value={oauthItem}
|
||||
onChange={(value) => {
|
||||
const newOauthItems = [...selectedOauthItems];
|
||||
newOauthItems[index] = value as { value: string; label: string } | undefined;
|
||||
setSelectedOauthItems(newOauthItems);
|
||||
}}
|
||||
placeholder="点击选择授权账户"
|
||||
style={{ width: '100%', marginBottom: 12, borderRadius: 8 }}
|
||||
popupRender={() => (
|
||||
<div style={{ padding: 8, width: 800, maxHeight: 500, overflow: 'auto' }}>
|
||||
<Table
|
||||
dataSource={oauthList}
|
||||
columns={[
|
||||
{
|
||||
title: '授权账户ID',
|
||||
dataIndex: 'accountId',
|
||||
key: 'accountId',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '授权账户名称',
|
||||
dataIndex: 'accountName',
|
||||
key: 'accountName',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '授权账户角色',
|
||||
dataIndex: 'accountRole',
|
||||
key: 'accountRole',
|
||||
width: 160,
|
||||
render: (role: string) => {
|
||||
const roleMap: Record<string, string> = {
|
||||
ADVERTISER: '客户',
|
||||
CUSTOMER_ADMIN: '普通版工作台-管理员',
|
||||
CUSTOMER_OPERATOR: '普通版工作台-协作者',
|
||||
AGENT: '代理商',
|
||||
CHILD_AGENT: '二级代理商',
|
||||
PLATFORM_ROLE_STAR: '星图账户',
|
||||
PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户',
|
||||
PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商',
|
||||
PLATFORM_ROLE_STAR_AGENT: '星图代理商',
|
||||
PLATFORM_ROLE_AWEME: '抖音号',
|
||||
PLATFORM_ROLE_STAR_MCN: '星图MCN机构',
|
||||
PLATFORM_ROLE_STAR_ISV: '星图服务商',
|
||||
AGENT_SYSTEM_ACCOUNT: '代理商系统账户',
|
||||
PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商',
|
||||
PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员',
|
||||
PLATFORM_ROLE_LIFE: '抖音来客账户',
|
||||
PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员',
|
||||
PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者',
|
||||
};
|
||||
return roleMap[role] || role;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '授权账户用户名',
|
||||
dataIndex: 'accountUsername',
|
||||
key: 'accountUsername',
|
||||
width: 120,
|
||||
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '授权用户ID',
|
||||
dataIndex: 'accountUserid',
|
||||
key: 'accountUserid',
|
||||
width: 120,
|
||||
},
|
||||
]}
|
||||
loading={oauthLoading}
|
||||
pagination={{
|
||||
current: oauthPage,
|
||||
pageSize: oauthPageSize,
|
||||
total: oauthTotal,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
onChange: (page, size) => {
|
||||
setOauthPage(page);
|
||||
setOauthPageSize(size);
|
||||
loadOAuthList(page, size);
|
||||
},
|
||||
}}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
scroll={{ x: 'max-content' }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
const id = String(record.id);
|
||||
const newOauthItems = [...selectedOauthItems];
|
||||
newOauthItems[index] = { value: id, label: String(record.accountId)+'-'+(record.accountName || '-') };
|
||||
setSelectedOauthItems(newOauthItems);
|
||||
const newOauthSelectOpens = [...oauthSelectOpens];
|
||||
newOauthSelectOpens[index] = false;
|
||||
setOauthSelectOpens(newOauthSelectOpens);
|
||||
},
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: oauthItem?.value === String(record.id) ? '#e6f7ff' : undefined,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
open={oauthSelectOpens[index]}
|
||||
onOpenChange={(open) => {
|
||||
const newOauthSelectOpens = [...oauthSelectOpens];
|
||||
newOauthSelectOpens[index] = open;
|
||||
setOauthSelectOpens(newOauthSelectOpens);
|
||||
if (open) {
|
||||
loadOAuthList(1, oauthPageSize);
|
||||
}
|
||||
}}
|
||||
labelInValue
|
||||
fieldNames={{ label: 'accountUserid', value: 'id' }}
|
||||
/>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569', marginBottom: 8, display: 'block' }}>
|
||||
粘贴账户ID(每行一个或用逗号分隔)
|
||||
</Typography.Text>
|
||||
<Input.TextArea
|
||||
value={accountIdInputs[index]}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const newAccountIdInputs = [...accountIdInputs];
|
||||
newAccountIdInputs[index] = value;
|
||||
setAccountIdInputs(newAccountIdInputs);
|
||||
const ids = value.split(/[\n,]/)
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
const uniqueIds = [...new Set(ids)];
|
||||
const textAccounts = uniqueIds.map(id => ({ accountId: id }));
|
||||
const seen = new Set<string>();
|
||||
const finalAccounts = textAccounts.filter(a => {
|
||||
if (seen.has(a.accountId)) return false;
|
||||
seen.add(a.accountId);
|
||||
return true;
|
||||
});
|
||||
const newAccountIdLists = [...accountIdLists];
|
||||
newAccountIdLists[index] = finalAccounts;
|
||||
setAccountIdLists(newAccountIdLists);
|
||||
}}
|
||||
placeholder="粘贴账户ID,每行一个或用逗号分隔,例如:
|
||||
10001,10002,10003
|
||||
10004"
|
||||
rows={4}
|
||||
rows={3}
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
onClick={() => {
|
||||
setSelectedOauthItems([...selectedOauthItems, undefined]);
|
||||
setAccountIdInputs([...accountIdInputs, '']);
|
||||
setAccountIdLists([...accountIdLists, []]);
|
||||
setOauthSelectOpens([...oauthSelectOpens, false]);
|
||||
}}
|
||||
style={{ borderRadius: 8, marginBottom: 16 }}
|
||||
/>
|
||||
>
|
||||
+ 新增授权账户组
|
||||
</Button>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div style={{
|
||||
@@ -1608,9 +1695,9 @@ const GeneratedRecord: React.FC = () => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
setUploadConfigModalVisible(false);
|
||||
setAccountIdList([]);
|
||||
setAccountIdInput('');
|
||||
setSelectedOauthItems(undefined);
|
||||
setAccountIdLists([[]]);
|
||||
setAccountIdInputs(['']);
|
||||
setSelectedOauthItems([undefined]);
|
||||
setMaterialFileNames(new Map());
|
||||
setUnifiedFileName('');
|
||||
}}
|
||||
@@ -1622,7 +1709,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
type="primary"
|
||||
onClick={handleStartBatchUpload}
|
||||
loading={uploading}
|
||||
disabled={uploading || accountIdList.length === 0}
|
||||
disabled={uploading || accountIdLists.every(list => list.length === 0)}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
{uploading ? '上传中...' : '开始上传'}
|
||||
@@ -1678,25 +1765,6 @@ const GeneratedRecord: React.FC = () => {
|
||||
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',
|
||||
@@ -1726,7 +1794,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
title: '备注',
|
||||
dataIndex: 'note',
|
||||
key: 'note',
|
||||
width: 250,
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
render: (note: string) => (
|
||||
<span style={{ color: '#94a3b8' }}>
|
||||
@@ -1736,10 +1804,17 @@ const GeneratedRecord: React.FC = () => {
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180,
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
render: (text: string) => formatDateTime(text),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updatedAt',
|
||||
key: 'updatedAt',
|
||||
width: 180,
|
||||
render: (text: string) => formatDateTime(text),
|
||||
},
|
||||
|
||||
]}
|
||||
@@ -2070,15 +2145,16 @@ const GeneratedRecord: React.FC = () => {
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
{/* <div>
|
||||
<div>
|
||||
<Button
|
||||
|
||||
style={{ width: '100%', borderRadius: 8, marginTop: 20, color: '#4c49cc' }}
|
||||
type="primary"
|
||||
onClick={handleSinglePushToMedia}
|
||||
style={{ width: '100%', borderRadius: 8, marginTop: 20 }}
|
||||
disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)}
|
||||
>
|
||||
|
||||
推送媒体后台
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button, Table, Tag, Input, Pagination, Typography, Select, App } from '
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FolderOpenOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { getResourcesMaterialList } from '../api';
|
||||
import PreResultDisplay from '../components/PreResultDisplay';
|
||||
|
||||
// 格式化时间 2026-06-12T03:47:28.542988Z -> 2026-06-12 03:47:28
|
||||
const formatDateTime = (dateStr: string) => {
|
||||
@@ -34,10 +35,9 @@ const resourceTypeConfig: Record<string, { label: string; color: string }> = {
|
||||
|
||||
// 状态配置
|
||||
const statusConfig: Record<string, { label: string; color: string }> = {
|
||||
'1': { label: '待上传', color: 'orange' },
|
||||
'2': { label: '上传中', color: 'processing' },
|
||||
'3': { label: '上传成功', color: 'success' },
|
||||
'4': { label: '上传失败', color: 'error' },
|
||||
'FAILED': { label: '失败', color: 'error' },
|
||||
'PENDING': { label: '处理中', color: 'processing' },
|
||||
'SUCCESS': { label: '成功', color: 'success' },
|
||||
};
|
||||
|
||||
interface MaterialResource {
|
||||
@@ -232,18 +232,17 @@ const MaterialListPage: React.FC = () => {
|
||||
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>;
|
||||
// },
|
||||
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>,
|
||||
render: (text: string) => <PreResultDisplay preResult={text} />,
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
|
||||
@@ -6,76 +6,16 @@ import {
|
||||
LockOutlined,
|
||||
ThunderboltOutlined,
|
||||
GiftOutlined,
|
||||
TrendingUpOutlined,
|
||||
FlameOutlined,
|
||||
AwardOutlined,
|
||||
BarChartOutlined,
|
||||
FireOutlined,
|
||||
TrophyOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const PopularPage: React.FC = () => {
|
||||
const handleUnlock = () => {
|
||||
message.info('请联系客服开通VIP会员,客服热线:400-888-8888');
|
||||
message.info('请联系客服开通');
|
||||
};
|
||||
|
||||
const industries = [
|
||||
{ name: '美妆护肤', hot: 'HOT', trend: '+125%', color: '#ec4899' },
|
||||
{ name: '美食餐饮', hot: '', trend: '+89%', color: '#f59e0b' },
|
||||
{ name: '服饰穿搭', hot: 'NEW', trend: '+76%', color: '#10b981' },
|
||||
{ name: '数码科技', hot: '', trend: '+64%', color: '#06b6d4' },
|
||||
{ name: '家居生活', hot: '', trend: '+52%', color: '#8b5cf6' },
|
||||
{ name: '运动健身', hot: 'HOT', trend: '+48%', color: '#6366f1' },
|
||||
];
|
||||
|
||||
const hotMaterials = [
|
||||
{
|
||||
id: 1,
|
||||
title: '夏日清爽护肤教程',
|
||||
industry: '美妆护肤',
|
||||
views: '2.3M',
|
||||
likes: '156K',
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=beautiful%20skincare%20product%20advertisement%20with%20fresh%20summer%20vibes&image_size=landscape_16_9',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '网红美食探店vlog',
|
||||
industry: '美食餐饮',
|
||||
views: '1.8M',
|
||||
likes: '128K',
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=delicious%20food%20foodie%20vlog%20style%20restaurant&image_size=landscape_16_9',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '秋季穿搭灵感分享',
|
||||
industry: '服饰穿搭',
|
||||
views: '1.5M',
|
||||
likes: '98K',
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=fashion%20autumn%20outfit%20inspiration%20stylish&image_size=landscape_16_9',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '新品手机开箱评测',
|
||||
industry: '数码科技',
|
||||
views: '1.2M',
|
||||
likes: '87K',
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=smartphone%20unboxing%20review%20tech%20gadget&image_size=landscape_16_9',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '家居改造前后对比',
|
||||
industry: '家居生活',
|
||||
views: '980K',
|
||||
likes: '76K',
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=home%20makeover%20before%20after%20interior%20design&image_size=landscape_16_9',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '健身房训练日常',
|
||||
industry: '运动健身',
|
||||
views: '850K',
|
||||
likes: '65K',
|
||||
thumbnail: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=gym%20workout%20fitness%20training%20motivation&image_size=landscape_16_9',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px 0' }}>
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
@@ -90,7 +30,7 @@ const PopularPage: React.FC = () => {
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 4px 16px rgba(245,158,11,0.3)',
|
||||
}}>
|
||||
<TrendingUpOutlined style={{ fontSize: 22, color: '#fff' }} />
|
||||
<BarChartOutlined style={{ fontSize: 22, color: '#fff' }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={2} style={{ margin: 0, color: '#1e293b', fontWeight: 700 }}>
|
||||
@@ -112,60 +52,32 @@ const PopularPage: React.FC = () => {
|
||||
borderRadius: 20,
|
||||
background: 'linear-gradient(135deg, rgba(245,158,11,0.04) 0%, rgba(217,119,6,0.04) 100%)',
|
||||
border: '1px solid rgba(245,158,11,0.1)',
|
||||
padding: 24,
|
||||
padding: 10,
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4} style={{ marginBottom: 20, color: '#1e293b' }}>
|
||||
<FlameOutlined style={{ marginRight: 8, color: '#f59e0b' }} />
|
||||
<FireOutlined style={{ marginRight: 8, color: '#f59e0b' }} />
|
||||
行业热度排行
|
||||
</Typography.Title>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: 16 }}>
|
||||
{industries.map((industry, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
textAlign: 'center',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4, marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 20, fontWeight: 700, color: industry.color }}>{index + 1}</span>
|
||||
{industry.hot && (
|
||||
<span style={{
|
||||
fontSize: 10,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 10,
|
||||
background: industry.hot === 'HOT' ? 'rgba(239,68,68,0.1)' : 'rgba(16,185,129,0.1)',
|
||||
color: industry.hot === 'HOT' ? '#ef4444' : '#10b981',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{industry.hot}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text strong style={{ color: '#1e293b', fontSize: 14 }}>
|
||||
{industry.name}
|
||||
</Typography.Text>
|
||||
<div style={{
|
||||
marginTop: 8,
|
||||
fontSize: 12,
|
||||
color: '#10b981',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{industry.trend}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
background: '#fafafa',
|
||||
padding: 40,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#94a3b8', fontSize: 14 }}>
|
||||
敬请期待行业热度排行
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<Typography.Title level={4} style={{ margin: 0, color: '#1e293b' }}>
|
||||
<AwardOutlined style={{ marginRight: 8, color: '#6366f1' }} />
|
||||
<TrophyOutlined style={{ marginRight: 8, color: '#6366f1' }} />
|
||||
爆款素材榜单
|
||||
</Typography.Title>
|
||||
<Button
|
||||
@@ -183,63 +95,19 @@ const PopularPage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||||
{hotMaterials.map((material) => (
|
||||
<Card
|
||||
key={material.id}
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.06)',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={material.thumbnail}
|
||||
alt={material.title}
|
||||
style={{ width: '100%', height: 180, objectFit: 'cover' }}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
left: 12,
|
||||
padding: '4px 12px',
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
borderRadius: 8,
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
}}>
|
||||
{material.industry}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Typography.Title level={5} style={{ marginBottom: 8, color: '#1e293b' }}>
|
||||
{material.title}
|
||||
</Typography.Title>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, color: '#64748b', fontSize: 13 }}>
|
||||
<span><StarOutlined style={{ marginRight: 4 }} />{material.views} 播放</span>
|
||||
<span><CrownOutlined style={{ marginRight: 4 }} />{material.likes} 点赞</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="small"
|
||||
onClick={handleUnlock}
|
||||
style={{
|
||||
marginTop: 12,
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
border: 'none',
|
||||
}}
|
||||
>
|
||||
<ThunderboltOutlined style={{ marginRight: 4 }} />
|
||||
一键生成同款
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card
|
||||
bordered={false}
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
background: '#fafafa',
|
||||
padding: 40,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#94a3b8', fontSize: 14 }}>
|
||||
敬请期待更多爆款素材
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user