解决冲突

This commit is contained in:
Lrd
2026-07-03 11:25:31 +08:00
16 changed files with 448 additions and 197 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-CE6vN0iM.js"></script> <script type="module" crossorigin src="/assets/index-875Agalq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css"> <link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head> </head>
<body> <body>
+3
View File
@@ -611,6 +611,9 @@ export async function getAdminGenerationAiTasks(params?: GenerationAITaskQueryPa
if (params?.pageSize) q.set('page_size', String(params.pageSize)); if (params?.pageSize) q.set('page_size', String(params.pageSize));
if (params?.userId) q.set('user_id', params.userId); if (params?.userId) q.set('user_id', params.userId);
if (params?.userName) q.set('user_name', params.userName); if (params?.userName) q.set('user_name', params.userName);
if (params?.engineId) q.set('engine_id', params.engineId);
if (params?.createdStart) q.set('created_start', params.createdStart);
if (params?.createdEnd) q.set('created_end', params.createdEnd);
const qs = q.toString(); const qs = q.toString();
return api.get<GenerationAITaskListOut>(`/generation-ai/tasks${qs ? `?${qs}` : ''}`); return api.get<GenerationAITaskListOut>(`/generation-ai/tasks${qs ? `?${qs}` : ''}`);
} }
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { import {
Button, Button,
Card, Card,
DatePicker,
Empty, Empty,
Input, Input,
message, message,
@@ -19,21 +20,35 @@ import {
CloseCircleOutlined, CloseCircleOutlined,
EyeOutlined, EyeOutlined,
FileImageOutlined, FileImageOutlined,
LinkOutlined,
LoadingOutlined, LoadingOutlined,
PlayCircleOutlined, PlayCircleOutlined,
RobotOutlined,
SearchOutlined, SearchOutlined,
VideoCameraOutlined, VideoCameraOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import dayjs from 'dayjs';
import { getAdminGenerationAiTasks } from '../api'; import { getAdminGenerationAiTasks } from '../api';
import type { GenerationAIMediaReference, GenerationAITaskOut } from '../types'; import type { GenerationAIMediaReference, GenerationAITaskOut } from '../types';
import { formatDate } from '../utils/formatDate'; import { formatDate } from '../utils/formatDate';
const { RangePicker } = DatePicker;
const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
// 资源 URL 由后端返回相对路径,例如 /generate/images/xxx.png?exp=xxx&sign=xxx。 // 资源 URL 由后端返回相对路径,例如 /generate/images/xxx.png?exp=xxx&sign=xxx。
// 如果 VITE_API_BASE 被配置成 http://host/api 或 /api,这里会去掉 /api,避免拼成 /api/generate/xxx 导致 404。 // 如果 VITE_API_BASE 被配置成 http://host/api 或 /api,这里会去掉 /api,避免拼成 /api/generate/xxx 导致 404。
const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, ''); const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, '');
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
/** 从 engineSnapshot 中提取引擎展示名称。 */
const getEngineName = (snapshot: Record<string, unknown> | null | undefined): string | null => {
if (!snapshot) return null;
return (snapshot.name as string)
|| (snapshot.modelName as string)
|| (snapshot.id as string)
|| null;
};
type ResourceState = 'empty' | 'checking' | 'valid' | 'invalid'; type ResourceState = 'empty' | 'checking' | 'valid' | 'invalid';
interface PreviewResourceState { interface PreviewResourceState {
@@ -200,6 +215,10 @@ const InfoItem: React.FC<{ label: string; value?: React.ReactNode }> = ({ label,
</div> </div>
); );
const todayStart = () => dayjs().startOf('day');
const todayEnd = () => dayjs().endOf('day');
const AdminGenerationAiRecords: React.FC = () => { const AdminGenerationAiRecords: React.FC = () => {
const [records, setRecords] = useState<GenerationAITaskOut[]>([]); const [records, setRecords] = useState<GenerationAITaskOut[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -208,10 +227,14 @@ const AdminGenerationAiRecords: React.FC = () => {
const [filterStatus, setFilterStatus] = useState<string>(''); const [filterStatus, setFilterStatus] = useState<string>('');
const [filterGenType, setFilterGenType] = useState<string>(''); const [filterGenType, setFilterGenType] = useState<string>('');
const [filterEngineId, setFilterEngineId] = useState<string>('');
const [inputUserId, setInputUserId] = useState<string>(''); const [inputUserId, setInputUserId] = useState<string>('');
const [inputUserName, setInputUserName] = useState<string>(''); const [inputUserName, setInputUserName] = useState<string>('');
const [queryUserId, setQueryUserId] = useState<string>(''); const [queryUserId, setQueryUserId] = useState<string>('');
const [queryUserName, setQueryUserName] = useState<string>(''); const [queryUserName, setQueryUserName] = useState<string>('');
const [queryEngineId, setQueryEngineId] = useState<string>('');
const [createdRange, setCreatedRange] = useState<any>([todayStart(), todayEnd()]);
const [queryCreatedRange, setQueryCreatedRange] = useState<any>([todayStart(), todayEnd()]);
const [reloadKey, setReloadKey] = useState(0); const [reloadKey, setReloadKey] = useState(0);
const [preview, setPreview] = useState<GenerationAITaskOut | null>(null); const [preview, setPreview] = useState<GenerationAITaskOut | null>(null);
@@ -219,6 +242,13 @@ const AdminGenerationAiRecords: React.FC = () => {
const [videoPlaying, setVideoPlaying] = useState(false); const [videoPlaying, setVideoPlaying] = useState(false);
const videoRef = useRef<HTMLVideoElement | null>(null); const videoRef = useRef<HTMLVideoElement | null>(null);
// 资源预览弹窗(附件 / 最终结果)
const [resourcePreview, setResourcePreview] = useState<{
url: string;
type: 'image' | 'video';
title: string;
} | null>(null);
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -227,6 +257,9 @@ const AdminGenerationAiRecords: React.FC = () => {
status: filterStatus || undefined, status: filterStatus || undefined,
userId: queryUserId || undefined, userId: queryUserId || undefined,
userName: queryUserName || undefined, userName: queryUserName || undefined,
engineId: queryEngineId || undefined,
createdStart: queryCreatedRange?.[0]?.toISOString?.(),
createdEnd: queryCreatedRange?.[1]?.toISOString?.(),
page, page,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}); });
@@ -237,7 +270,7 @@ const AdminGenerationAiRecords: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [filterGenType, filterStatus, page, queryUserId, queryUserName]); }, [filterGenType, filterStatus, page, queryUserId, queryUserName, queryEngineId, queryCreatedRange]);
useEffect(() => { useEffect(() => {
load(); load();
@@ -274,6 +307,24 @@ const AdminGenerationAiRecords: React.FC = () => {
setPage(1); setPage(1);
setQueryUserId(inputUserId.trim()); setQueryUserId(inputUserId.trim());
setQueryUserName(inputUserName.trim()); setQueryUserName(inputUserName.trim());
setQueryEngineId(filterEngineId);
setQueryCreatedRange(createdRange);
setReloadKey((v) => v + 1);
};
const handleReset = () => {
setFilterStatus('');
setFilterGenType('');
setFilterEngineId('');
setInputUserId('');
setInputUserName('');
setQueryUserId('');
setQueryUserName('');
setQueryEngineId('');
const defaultRange = [todayStart(), todayEnd()];
setCreatedRange(defaultRange);
setQueryCreatedRange(defaultRange);
setPage(1);
setReloadKey((v) => v + 1); setReloadKey((v) => v + 1);
}; };
@@ -341,6 +392,21 @@ const AdminGenerationAiRecords: React.FC = () => {
window.open(apiUrl(url), '_blank', 'noopener,noreferrer'); window.open(apiUrl(url), '_blank', 'noopener,noreferrer');
}; };
/** 弹窗预览资源(图片/视频) */
const handlePreviewResource = (url: string, type: 'image' | 'video', title: string) => {
if (isBlobUrl(url)) {
message.warning('本地临时素材已失效,暂无法查看');
return;
}
if (isUrlExpired(url)) {
message.warning('链接已超时,请刷新列表或重新搜索后再查看');
return;
}
setResourcePreview({ url: apiUrl(url), type, title });
};
const handleCloseResourcePreview = () => setResourcePreview(null);
const columns = useMemo(() => [ const columns = useMemo(() => [
{ {
title: '用户', key: 'user', width: 150, title: '用户', key: 'user', width: 150,
@@ -358,6 +424,69 @@ const AdminGenerationAiRecords: React.FC = () => {
return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>; return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>;
}, },
}, },
{
title: '引擎', key: 'engine', width: 160,
render: (_: any, r: GenerationAITaskOut) => {
const name = getEngineName(r.engineSnapshot as any);
return (
<Tooltip title={r.engineId || '未知引擎'} placement="topLeft">
<Tag icon={<RobotOutlined />} color="cyan">{name || r.engineId || '-'}</Tag>
</Tooltip>
);
},
},
{
title: '附件', key: 'references', width: 110,
render: (_: any, r: GenerationAITaskOut) => {
const refs = r.mediaReferences || [];
if (refs.length === 0) return <Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}></Typography.Text>;
const imgCount = refs.filter((ref) => getReferenceType(ref) === 'image').length;
const vidCount = refs.filter((ref) => getReferenceType(ref) === 'video').length;
const otherCount = refs.length - imgCount - vidCount;
return (
<Space size={4} wrap>
{imgCount > 0 ? <Tag color="purple" icon={<FileImageOutlined />}>{imgCount} </Tag> : null}
{vidCount > 0 ? <Tag color="geekblue" icon={<VideoCameraOutlined />}>{vidCount} </Tag> : null}
{otherCount > 0 ? <Tag>{otherCount} </Tag> : null}
</Space>
);
},
},
{
title: '结果', key: 'result', width: 90,
render: (_: any, r: GenerationAITaskOut) => {
if (r.status !== 'completed') {
return <Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>-</Typography.Text>;
}
if (r.genType === 'video' && r.videoUrl) {
return (
<Button
size="small"
icon={<PlayCircleOutlined />}
type="link"
style={{ padding: 0 }}
onClick={(e) => { e.stopPropagation(); handleOpenExternalResource(r.videoUrl, '视频'); }}
>
</Button>
);
}
if (r.genType === 'image' && r.imageUrl) {
return (
<Button
size="small"
icon={<FileImageOutlined />}
type="link"
style={{ padding: 0 }}
onClick={(e) => { e.stopPropagation(); handleOpenExternalResource(r.imageUrl, '图片'); }}
>
</Button>
);
}
return <Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}></Typography.Text>;
},
},
{ {
title: '提示词', key: 'prompt', ellipsis: true, title: '提示词', key: 'prompt', ellipsis: true,
render: (_: any, r: GenerationAITaskOut) => ( render: (_: any, r: GenerationAITaskOut) => (
@@ -795,10 +924,34 @@ const AdminGenerationAiRecords: React.FC = () => {
{ value: 'video', label: '视频' }, { value: 'video', label: '视频' },
]} ]}
/> />
<Select
allowClear
showSearch
placeholder="引擎筛选"
value={filterEngineId || undefined}
style={{ width: 180 }}
onChange={(v) => { setFilterEngineId(v || ''); setPage(1); setQueryEngineId(v || ''); }}
optionFilterProp="label"
options={Array.from(
new Map(
records
.filter((r) => r.engineId)
.map((r) => [r.engineId, {
value: r.engineId,
label: getEngineName(r.engineSnapshot as any) || r.engineId,
}]),
).values(),
)}
/>
<RangePicker
value={createdRange}
onChange={(dates) => { setCreatedRange(dates); }}
placeholder={['开始日期', '结束日期']}
/>
<Input <Input
placeholder="用户ID搜索" placeholder="用户ID搜索"
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />} prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
style={{ width: 180 }} style={{ width: 150 }}
value={inputUserId} value={inputUserId}
onChange={(e) => setInputUserId(e.target.value)} onChange={(e) => setInputUserId(e.target.value)}
onPressEnter={handleSearch} onPressEnter={handleSearch}
@@ -807,7 +960,7 @@ const AdminGenerationAiRecords: React.FC = () => {
<Input <Input
placeholder="用户名搜索" placeholder="用户名搜索"
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />} prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
style={{ width: 160 }} style={{ width: 150 }}
value={inputUserName} value={inputUserName}
onChange={(e) => setInputUserName(e.target.value)} onChange={(e) => setInputUserName(e.target.value)}
onPressEnter={handleSearch} onPressEnter={handleSearch}
@@ -816,6 +969,9 @@ const AdminGenerationAiRecords: React.FC = () => {
<Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}> <Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}>
</Button> </Button>
<Button onClick={handleReset} style={{ borderRadius: 8 }}>
</Button>
</Space> </Space>
</div> </div>
@@ -824,7 +980,7 @@ const AdminGenerationAiRecords: React.FC = () => {
loading={loading} loading={loading}
columns={columns as any} columns={columns as any}
dataSource={records} dataSource={records}
scroll={{ x: 1180 }} scroll={{ x: 1650 }}
pagination={{ pagination={{
current: page, current: page,
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
@@ -912,11 +1068,62 @@ const AdminGenerationAiRecords: React.FC = () => {
{renderReferences()} {renderReferences()}
{/* 附件快速查看区 */}
{(preview.mediaReferences && preview.mediaReferences.length > 0) ? (
<div style={{ padding: 12, borderRadius: 10, background: '#f8f9fc' }}>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8', display: 'block', marginBottom: 8 }}>
</Typography.Text>
<Space size={8} wrap>
{preview.mediaReferences.map((ref, idx) => {
const refUrl = getReferenceUrl(ref);
const refType = getReferenceType(ref);
const title = typeof ref.name === 'string' && ref.name ? ref.name : `附件 ${idx + 1}`;
if (!refUrl) return null;
return (
<Button
key={idx}
size="small"
icon={refType === 'video' ? <PlayCircleOutlined /> : <LinkOutlined />}
onClick={() => handlePreviewResource(refUrl, refType === 'video' ? 'video' : 'image', title)}
>
{title}
</Button>
);
})}
</Space>
</div>
) : null}
{preview.status === 'completed' ? ( {preview.status === 'completed' ? (
<div> <div>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8', display: 'block', marginBottom: 6 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
{preview.genType === 'video' ? '生成视频' : '生成图片'} <Typography.Text style={{ fontSize: 12, color: '#94a3b8', display: 'block' }}>
</Typography.Text> {preview.genType === 'video' ? '生成视频' : '生成图片'}
</Typography.Text>
{preview.genType === 'video' && preview.videoUrl ? (
<Button
size="small"
type="link"
icon={<PlayCircleOutlined />}
onClick={() => handlePreviewResource(preview.videoUrl!, 'video', '生成视频')}
style={{ padding: 0 }}
>
</Button>
) : null}
{preview.genType === 'image' && preview.imageUrl ? (
<Button
size="small"
type="link"
icon={<FileImageOutlined />}
onClick={() => handlePreviewResource(preview.imageUrl!, 'image', '生成图片')}
style={{ padding: 0 }}
>
</Button>
) : null}
</div>
{preview.genType === 'video' ? renderResultVideo() : renderResultImage()} {preview.genType === 'video' ? renderResultVideo() : renderResultImage()}
</div> </div>
) : null} ) : null}
@@ -936,6 +1143,34 @@ const AdminGenerationAiRecords: React.FC = () => {
<Empty description="暂无详情" /> <Empty description="暂无详情" />
)} )}
</Modal> </Modal>
{/* 资源预览弹窗(附件 / 最终结果) */}
<Modal
title={resourcePreview?.title || '资源预览'}
open={!!resourcePreview}
onCancel={handleCloseResourcePreview}
footer={null}
width={resourcePreview?.type === 'video' ? 800 : 600}
destroyOnHidden
>
{resourcePreview ? (
resourcePreview.type === 'video' ? (
<video
key={resourcePreview.url}
src={resourcePreview.url}
controls
autoPlay
style={{ width: '100%', maxHeight: 600, background: '#000', borderRadius: 8 }}
/>
) : (
<img
src={resourcePreview.url}
alt={resourcePreview.title}
style={{ width: '100%', maxHeight: 600, objectFit: 'contain', borderRadius: 8 }}
/>
)
) : null}
</Modal>
</div> </div>
); );
}; };
+48 -26
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Button, Card, Space, Table, Tag, Typography, message, Modal, Form, Input, Select, InputNumber, Button, Card, Space, Table, Tag, Tooltip, Typography, message, Modal, Form, Input, Select, InputNumber,
} from 'antd'; } from 'antd';
import { import {
MenuOutlined, ReloadOutlined, PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, MenuOutlined, ReloadOutlined, PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined,
@@ -10,16 +10,16 @@ import { formatDate } from '../utils/formatDate';
interface OAuthApp { interface OAuthApp {
id: string; id: string;
appId: string; app_id: string;
secret: string; secret: string;
status: number; status: number;
count: number; max_count: number;
openType: number; open_type: number;
authUrl?: string; auth_url?: string;
company?: string; company?: string;
createBy: string; create_by: string;
createdAt: string; created_at: string;
updatedAt: string; updated_at: string;
} }
const AdminOauthAppList: React.FC = () => { const AdminOauthAppList: React.FC = () => {
@@ -82,11 +82,11 @@ const AdminOauthAppList: React.FC = () => {
const app = await getOauthApp(id); const app = await getOauthApp(id);
setCurrentApp(app); setCurrentApp(app);
updateForm.setFieldsValue({ updateForm.setFieldsValue({
app_id: app.appId, app_id: app.app_id,
secret: app.secret, secret: app.secret,
open_type: app.openType, open_type: app.open_type,
count: app.count, count: app.max_count,
auth_url: app.authUrl, auth_url: app.auth_url,
company: app.company, company: app.company,
}); });
setUpdateModalVisible(true); setUpdateModalVisible(true);
@@ -143,8 +143,19 @@ const AdminOauthAppList: React.FC = () => {
{ title: '应用ID', dataIndex: 'appId', width: 120, { title: '应用ID', dataIndex: 'appId', width: 120,
render: (v: string) => <Typography.Text>{v}</Typography.Text>, render: (v: string) => <Typography.Text>{v}</Typography.Text>,
}, },
{ title: '应用密钥', dataIndex: 'secret', width: 120, // { title: '应用密钥', dataIndex: 'secret', width: 120,
render: (v: string) => <Typography.Text>{v}</Typography.Text>, // render: (v: string) => <Typography.Text>{v}</Typography.Text>,
// },
{ title: '应用密钥', dataIndex: 'secret', width: 180,
render: (v: string) => {
if (!v) return '-';
const short = v.length > 12 ? `${v.slice(0, 6)}...${v.slice(-4)}` : v;
return (
<Tooltip title={v} placement="topLeft">
<Typography.Text style={{ fontSize: 12 }}>{short}</Typography.Text>
</Tooltip>
);
},
}, },
{ title: '开户方式', dataIndex: 'openType', width: 90, { title: '开户方式', dataIndex: 'openType', width: 90,
render: (v: number) => { render: (v: number) => {
@@ -165,15 +176,26 @@ const AdminOauthAppList: React.FC = () => {
{ title: '状态', dataIndex: 'status', width: 80, { title: '状态', dataIndex: 'status', width: 80,
render: (v: number) => <Tag color={v === 1 ? 'green' : 'red'}>{v === 1 ? '正常' : '禁用'}</Tag>, render: (v: number) => <Tag color={v === 1 ? 'green' : 'red'}>{v === 1 ? '正常' : '禁用'}</Tag>,
}, },
{ title: '授权URL', dataIndex: 'authUrl', width: 180, // { title: '授权URL', dataIndex: 'authUrl', width: 180,
render: (v: string) => <Typography.Text>{v}</Typography.Text>, // render: (v: string) => <Typography.Text>{v}</Typography.Text>,
// },
{ title: '授权URL', dataIndex: 'authUrl', width: 220,
render: (v: string) => {
if (!v) return '-';
return (
<Tooltip title={v} placement="topLeft">
<Typography.Text style={{ fontSize: 12 }} ellipsis>{v.length > 30 ? `${v.slice(0, 27)}...` : v}</Typography.Text>
</Tooltip>
);
},
}, },
{ title: '创建人', dataIndex: 'createBy', width: 120, { title: '创建人', dataIndex: 'createBy', width: 120,
render: (v: string) => <Typography.Text>{v}</Typography.Text>, render: (v: string) => <Typography.Text>{v}</Typography.Text>,
}, },
// { title: '创建时间', dataIndex: 'createdAt', width: 160, { title: '创建时间', dataIndex: 'createdAt', width: 160,
// render: (v: string) => <Typography.Text>{formatDate(v)}</Typography.Text>, render: (v: string) => <Typography.Text>{formatDate(v)}</Typography.Text>,
// }, },
{ title: '更新时间', dataIndex: 'updatedAt', width: 160, { title: '更新时间', dataIndex: 'updatedAt', width: 160,
render: (v: string) => <Typography.Text>{formatDate(v)}</Typography.Text>, render: (v: string) => <Typography.Text>{formatDate(v)}</Typography.Text>,
}, },
@@ -312,22 +334,22 @@ const AdminOauthAppList: React.FC = () => {
{currentApp && ( {currentApp && (
<div style={{ lineHeight: '2' }}> <div style={{ lineHeight: '2' }}>
<p><strong>ID:</strong> {currentApp.id}</p> <p><strong>ID:</strong> {currentApp.id}</p>
<p><strong>ID:</strong> {currentApp.appId}</p> <p><strong>ID:</strong> {currentApp.app_id}</p>
<p><strong>:</strong> {currentApp.secret}</p> <p><strong>:</strong> {currentApp.secret}</p>
<p><strong>:</strong> {(() => { <p><strong>:</strong> {(() => {
const typeMap: Record<number, string> = { const typeMap: Record<number, string> = {
1: '千川', 2: '广告', 3: '本地推', 4: '星图', 5: '快手代理商', 1: '千川', 2: '广告', 3: '本地推', 4: '星图', 5: '快手代理商',
6: '巨量星图', 7: '巨量服务单', 8: '腾讯服务单', 9: '腾讯营销K2', 10: '腾讯营销K3' 6: '巨量星图', 7: '巨量服务单', 8: '腾讯服务单', 9: '腾讯营销K2', 10: '腾讯营销K3'
}; };
return typeMap[currentApp.openType] || currentApp.openType; return typeMap[currentApp.open_type] || currentApp.open_type;
})()}</p> })()}</p>
<p><strong>:</strong> {currentApp.company || '-'}</p> <p><strong>:</strong> {currentApp.company || '-'}</p>
<p><strong>:</strong> {currentApp.count}</p> <p><strong>:</strong> {currentApp.max_count}</p>
<p><strong>:</strong> {currentApp.status === 1 ? '正常' : '禁用'}</p> <p><strong>:</strong> {currentApp.status === 1 ? '正常' : '禁用'}</p>
<p><strong>URL:</strong> {currentApp.authUrl || '-'}</p> <p><strong>URL:</strong> {currentApp.auth_url || '-'}</p>
<p><strong>:</strong> {currentApp.createBy}</p> <p><strong>:</strong> {currentApp.create_by}</p>
<p><strong>:</strong> {formatDate(currentApp.createdAt)}</p> <p><strong>:</strong> {formatDate(currentApp.created_at)}</p>
<p><strong>:</strong> {formatDate(currentApp.updatedAt)}</p> <p><strong>:</strong> {formatDate(currentApp.updated_at)}</p>
</div> </div>
)} )}
</Modal> </Modal>
+3
View File
@@ -426,6 +426,9 @@ export interface GenerationAITaskQueryParams {
pageSize?: number; pageSize?: number;
userId?: string; userId?: string;
userName?: string; userName?: string;
engineId?: string;
createdStart?: string;
createdEnd?: string;
} }
+19 -1
View File
@@ -1,7 +1,7 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from sqlalchemy import select from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_user, get_db from app.dependencies import get_current_user, get_db
@@ -232,6 +232,21 @@ async def list_tasks(
description="查询相关用户名的对应记录[管理后台]", description="查询相关用户名的对应记录[管理后台]",
examples=["demo"], examples=["demo"],
), ),
engine_id: str | None = Query(
None,
description="按引擎ID筛选[管理后台]",
examples=["0019e1697667d0eff39"],
),
created_start: datetime | None = Query(
None,
description="创建时间起始(含),ISO 格式,例如 2026-07-03T00:00:00",
examples=["2026-07-03T00:00:00"],
),
created_end: datetime | None = Query(
None,
description="创建时间截止(含),ISO 格式,例如 2026-07-03T23:59:59",
examples=["2026-07-03T23:59:59"],
),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
@@ -250,6 +265,9 @@ async def list_tasks(
page, page,
page_size, page_size,
is_admin, is_admin,
engine_id=engine_id,
created_start=created_start,
created_end=created_end,
) )
# ====================== 在这里加排序(最新在前)====================== # ====================== 在这里加排序(最新在前)======================
+2 -1
View File
@@ -21,11 +21,12 @@ async def list_apps(
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=100),
open_type: int | None = Query(None, ge=1, le=10, description="开户方式"), open_type: int | None = Query(None, ge=1, le=10, description="开户方式"),
status: int | None = Query(None, ge=1, le=2, description="应用状态,1=正常,2=禁用"), status: int | None = Query(None, ge=1, le=2, description="应用状态,1=正常,2=禁用"),
create_by: str | None = Query(None, description="按创建人ID筛选[可选]"),
admin: User = Depends(get_admin_user), admin: User = Depends(get_admin_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
app_id: str | None = Query(None, max_length=255, description="应用id"), app_id: str | None = Query(None, max_length=255, description="应用id"),
): ):
result = await list_user_oauth_apps(db, page, page_size, open_type, status, admin.id, app_id) result = await list_user_oauth_apps(db, page, page_size, open_type, status, create_by, app_id)
return { return {
"total": result["total"], "total": result["total"],
"page": result["page"], "page": result["page"],
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone, date
from typing import Any from typing import Any
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import func, select from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings from app.config import settings
@@ -507,6 +507,9 @@ async def list_async_generation_tasks(
page: int, page: int,
page_size: int, page_size: int,
is_admin: bool = False, is_admin: bool = False,
engine_id: str | None = None,
created_start: datetime | None = None,
created_end: datetime | None = None,
): ):
if is_admin: if is_admin:
query = ( query = (
@@ -533,6 +536,18 @@ async def list_async_generation_tasks(
if status: if status:
query = query.where(ChatGenerationTask.status == status) query = query.where(ChatGenerationTask.status == status)
if engine_id:
query = query.where(ChatGenerationTask.engine_id == engine_id)
if created_start is not None or created_end is not None:
range_filters = []
if created_start is not None:
range_filters.append(ChatGenerationTask.created_at >= created_start)
if created_end is not None:
range_filters.append(ChatGenerationTask.created_at <= created_end)
if range_filters:
query = query.where(and_(*range_filters))
count_query = select(func.count()).select_from(query.subquery()) count_query = select(func.count()).select_from(query.subquery())
total = (await db.execute(count_query)).scalar_one() total = (await db.execute(count_query)).scalar_one()
@@ -1,6 +1,6 @@
import asyncio import asyncio
import logging
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
import json
from sqlalchemy import select, delete from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,39 +12,9 @@ from app.models.user_oauth_account import UserOAuthAccount
from app.models.resources_material import ResourcesMaterial from app.models.resources_material import ResourcesMaterial
from app.utils.id_gen import generate_id from app.utils.id_gen import generate_id
from app.utils.douyinApi import DouyinApi from app.utils.douyinApi import DouyinApi
from app.utils.logger import get_logger
import os logger = get_logger("material_consumption_task", "material_consumption")
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("material_consumption_task")
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"material_consumption-{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)
douyin_api = DouyinApi() douyin_api = DouyinApi()
@@ -22,13 +22,22 @@ async def list_user_oauth_apps(
if status is not None: if status is not None:
query = query.where(UserOAuthApp.status == status) query = query.where(UserOAuthApp.status == status)
if create_by is not None: #if create_by is not None:
query = query.where(UserOAuthApp.create_by == create_by) # query = query.where(UserOAuthApp.create_by == create_by)
if app_id is not None: if app_id is not None:
query = query.where(UserOAuthApp.app_id.like(f"%{app_id}%")) query = query.where(UserOAuthApp.app_id.like(f"%{app_id}%"))
total_result = await db.execute(select(func.count(UserOAuthApp.id)).where(UserOAuthApp.deleted_at.is_(None))) count_query = select(func.count(UserOAuthApp.id)).where(UserOAuthApp.deleted_at.is_(None))
if open_type is not None:
count_query = count_query.where(UserOAuthApp.open_type == open_type)
if status is not None:
count_query = count_query.where(UserOAuthApp.status == status)
if create_by is not None:
count_query = count_query.where(UserOAuthApp.create_by == create_by)
if app_id is not None:
count_query = count_query.where(UserOAuthApp.app_id.like(f"%{app_id}%"))
total_result = await db.execute(count_query)
total = total_result.scalar() or 0 total = total_result.scalar() or 0
result = await db.execute(query.offset((page - 1) * page_size).limit(page_size)) result = await db.execute(query.offset((page - 1) * page_size).limit(page_size))
@@ -132,6 +141,7 @@ async def delete_user_oauth_app(db: AsyncSession, id: str, create_by: str | None
if create_by is not None: if create_by is not None:
app.create_by = create_by app.create_by = create_by
await db.flush() await db.flush()
await db.commit()
return True return True
@@ -1,39 +1,10 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import asyncio import asyncio
import os
import logging
from app.services.material_consumption_queue import sync_all_advertisers_consumption from app.services.material_consumption_queue import sync_all_advertisers_consumption
from app.utils.logger import get_logger
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") logger = get_logger("material_consumption_task", "material_consumption_task")
os.makedirs(LOG_DIR, exist_ok=True)
logger = logging.getLogger("material_consumption_task")
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"material_consumption_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)
async def schedule_daily_sync(): async def schedule_daily_sync():
+3 -1
View File
@@ -20,7 +20,9 @@ def get_logger(name: str, log_filename: str) -> logging.Logger:
if logger.handlers: if logger.handlers:
return logger return logger
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "log")
if name:
LOG_DIR = os.path.join(LOG_DIR, name)
os.makedirs(LOG_DIR, exist_ok=True) os.makedirs(LOG_DIR, exist_ok=True)
class DailyRotatingFileHandler(logging.FileHandler): class DailyRotatingFileHandler(logging.FileHandler):
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-tJxTOT0B.js"></script> <script type="module" crossorigin src="/assets/index-P3kptQyA.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bi8mcSs8.css"> <link rel="stylesheet" crossorigin href="/assets/index-Bi8mcSs8.css">
</head> </head>
<body> <body>
@@ -58,6 +58,7 @@ import {
InfoOutlined, InfoOutlined,
MenuOutlined, MenuOutlined,
ArrowLeftOutlined, ArrowLeftOutlined,
RobotOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore'; import { useAuthStore } from '../../store/useAuthStore';
@@ -268,7 +269,7 @@ const iconMap: Record<string, React.ReactNode> = {
StarOutlined: <StarOutlined />, StarOutlined: <StarOutlined />,
HeartOutlined: <HeartOutlined />, HeartOutlined: <HeartOutlined />,
CameraOutlined: <CameraOutlined />, CameraOutlined: <CameraOutlined />,
RobotOutlined: <PlayCircleOutlined />, RobotOutlined: <RobotOutlined />,
CalculatorOutlined: <CalculatorOutlined />, CalculatorOutlined: <CalculatorOutlined />,
DollarOutlined: <DollarOutlined />, DollarOutlined: <DollarOutlined />,
GiftOutlined: <GiftOutlined />, GiftOutlined: <GiftOutlined />,