解决冲突
This commit is contained in:
Vendored
+89
-89
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -611,6 +611,9 @@ export async function getAdminGenerationAiTasks(params?: GenerationAITaskQueryPa
|
||||
if (params?.pageSize) q.set('page_size', String(params.pageSize));
|
||||
if (params?.userId) q.set('user_id', params.userId);
|
||||
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();
|
||||
return api.get<GenerationAITaskListOut>(`/generation-ai/tasks${qs ? `?${qs}` : ''}`);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DatePicker,
|
||||
Empty,
|
||||
Input,
|
||||
message,
|
||||
@@ -19,21 +20,35 @@ import {
|
||||
CloseCircleOutlined,
|
||||
EyeOutlined,
|
||||
FileImageOutlined,
|
||||
LinkOutlined,
|
||||
LoadingOutlined,
|
||||
PlayCircleOutlined,
|
||||
RobotOutlined,
|
||||
SearchOutlined,
|
||||
VideoCameraOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { getAdminGenerationAiTasks } from '../api';
|
||||
import type { GenerationAIMediaReference, GenerationAITaskOut } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
|
||||
// 资源 URL 由后端返回相对路径,例如 /generate/images/xxx.png?exp=xxx&sign=xxx。
|
||||
// 如果 VITE_API_BASE 被配置成 http://host/api 或 /api,这里会去掉 /api,避免拼成 /api/generate/xxx 导致 404。
|
||||
const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, '');
|
||||
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';
|
||||
|
||||
interface PreviewResourceState {
|
||||
@@ -200,6 +215,10 @@ const InfoItem: React.FC<{ label: string; value?: React.ReactNode }> = ({ label,
|
||||
</div>
|
||||
);
|
||||
|
||||
const todayStart = () => dayjs().startOf('day');
|
||||
|
||||
const todayEnd = () => dayjs().endOf('day');
|
||||
|
||||
const AdminGenerationAiRecords: React.FC = () => {
|
||||
const [records, setRecords] = useState<GenerationAITaskOut[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -208,10 +227,14 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
|
||||
const [filterStatus, setFilterStatus] = useState<string>('');
|
||||
const [filterGenType, setFilterGenType] = useState<string>('');
|
||||
const [filterEngineId, setFilterEngineId] = useState<string>('');
|
||||
const [inputUserId, setInputUserId] = useState<string>('');
|
||||
const [inputUserName, setInputUserName] = useState<string>('');
|
||||
const [queryUserId, setQueryUserId] = 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 [preview, setPreview] = useState<GenerationAITaskOut | null>(null);
|
||||
@@ -219,6 +242,13 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
const [videoPlaying, setVideoPlaying] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
|
||||
// 资源预览弹窗(附件 / 最终结果)
|
||||
const [resourcePreview, setResourcePreview] = useState<{
|
||||
url: string;
|
||||
type: 'image' | 'video';
|
||||
title: string;
|
||||
} | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -227,6 +257,9 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
status: filterStatus || undefined,
|
||||
userId: queryUserId || undefined,
|
||||
userName: queryUserName || undefined,
|
||||
engineId: queryEngineId || undefined,
|
||||
createdStart: queryCreatedRange?.[0]?.toISOString?.(),
|
||||
createdEnd: queryCreatedRange?.[1]?.toISOString?.(),
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
});
|
||||
@@ -237,7 +270,7 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filterGenType, filterStatus, page, queryUserId, queryUserName]);
|
||||
}, [filterGenType, filterStatus, page, queryUserId, queryUserName, queryEngineId, queryCreatedRange]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
@@ -274,6 +307,24 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
setPage(1);
|
||||
setQueryUserId(inputUserId.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);
|
||||
};
|
||||
|
||||
@@ -341,6 +392,21 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
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(() => [
|
||||
{
|
||||
title: '用户', key: 'user', width: 150,
|
||||
@@ -358,6 +424,69 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
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,
|
||||
render: (_: any, r: GenerationAITaskOut) => (
|
||||
@@ -795,10 +924,34 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
{ 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
|
||||
placeholder="用户ID搜索"
|
||||
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||
style={{ width: 180 }}
|
||||
style={{ width: 150 }}
|
||||
value={inputUserId}
|
||||
onChange={(e) => setInputUserId(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
@@ -807,7 +960,7 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
<Input
|
||||
placeholder="用户名搜索"
|
||||
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
||||
style={{ width: 160 }}
|
||||
style={{ width: 150 }}
|
||||
value={inputUserName}
|
||||
onChange={(e) => setInputUserName(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
@@ -816,6 +969,9 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
<Button type="primary" onClick={handleSearch} style={{ borderRadius: 8 }}>
|
||||
搜索
|
||||
</Button>
|
||||
<Button onClick={handleReset} style={{ borderRadius: 8 }}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@@ -824,7 +980,7 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
loading={loading}
|
||||
columns={columns as any}
|
||||
dataSource={records}
|
||||
scroll={{ x: 1180 }}
|
||||
scroll={{ x: 1650 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: PAGE_SIZE,
|
||||
@@ -912,11 +1068,62 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
|
||||
{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' ? (
|
||||
<div>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#94a3b8', display: 'block', marginBottom: 6 }}>
|
||||
{preview.genType === 'video' ? '生成视频' : '生成图片'}
|
||||
</Typography.Text>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#94a3b8', display: 'block' }}>
|
||||
{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()}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -936,6 +1143,34 @@ const AdminGenerationAiRecords: React.FC = () => {
|
||||
<Empty description="暂无详情" />
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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';
|
||||
import {
|
||||
MenuOutlined, ReloadOutlined, PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined,
|
||||
@@ -10,16 +10,16 @@ import { formatDate } from '../utils/formatDate';
|
||||
|
||||
interface OAuthApp {
|
||||
id: string;
|
||||
appId: string;
|
||||
app_id: string;
|
||||
secret: string;
|
||||
status: number;
|
||||
count: number;
|
||||
openType: number;
|
||||
authUrl?: string;
|
||||
max_count: number;
|
||||
open_type: number;
|
||||
auth_url?: string;
|
||||
company?: string;
|
||||
createBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
create_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
const AdminOauthAppList: React.FC = () => {
|
||||
@@ -82,11 +82,11 @@ const AdminOauthAppList: React.FC = () => {
|
||||
const app = await getOauthApp(id);
|
||||
setCurrentApp(app);
|
||||
updateForm.setFieldsValue({
|
||||
app_id: app.appId,
|
||||
app_id: app.app_id,
|
||||
secret: app.secret,
|
||||
open_type: app.openType,
|
||||
count: app.count,
|
||||
auth_url: app.authUrl,
|
||||
open_type: app.open_type,
|
||||
count: app.max_count,
|
||||
auth_url: app.auth_url,
|
||||
company: app.company,
|
||||
});
|
||||
setUpdateModalVisible(true);
|
||||
@@ -143,8 +143,19 @@ const AdminOauthAppList: React.FC = () => {
|
||||
{ title: '应用ID', dataIndex: 'appId', width: 120,
|
||||
render: (v: string) => <Typography.Text>{v}</Typography.Text>,
|
||||
},
|
||||
{ title: '应用密钥', dataIndex: 'secret', width: 120,
|
||||
render: (v: string) => <Typography.Text>{v}</Typography.Text>,
|
||||
// { title: '应用密钥', dataIndex: 'secret', width: 120,
|
||||
// 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,
|
||||
render: (v: number) => {
|
||||
@@ -165,15 +176,26 @@ const AdminOauthAppList: React.FC = () => {
|
||||
{ title: '状态', dataIndex: 'status', width: 80,
|
||||
render: (v: number) => <Tag color={v === 1 ? 'green' : 'red'}>{v === 1 ? '正常' : '禁用'}</Tag>,
|
||||
},
|
||||
{ title: '授权URL', dataIndex: 'authUrl', width: 180,
|
||||
render: (v: string) => <Typography.Text>{v}</Typography.Text>,
|
||||
// { title: '授权URL', dataIndex: 'authUrl', width: 180,
|
||||
// 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,
|
||||
render: (v: string) => <Typography.Text>{v}</Typography.Text>,
|
||||
},
|
||||
// { title: '创建时间', dataIndex: 'createdAt', width: 160,
|
||||
// render: (v: string) => <Typography.Text>{formatDate(v)}</Typography.Text>,
|
||||
// },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 160,
|
||||
render: (v: string) => <Typography.Text>{formatDate(v)}</Typography.Text>,
|
||||
},
|
||||
{ title: '更新时间', dataIndex: 'updatedAt', width: 160,
|
||||
render: (v: string) => <Typography.Text>{formatDate(v)}</Typography.Text>,
|
||||
},
|
||||
@@ -312,22 +334,22 @@ const AdminOauthAppList: React.FC = () => {
|
||||
{currentApp && (
|
||||
<div style={{ lineHeight: '2' }}>
|
||||
<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> {(() => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '千川', 2: '广告', 3: '本地推', 4: '星图', 5: '快手代理商',
|
||||
6: '巨量星图', 7: '巨量服务单', 8: '腾讯服务单', 9: '腾讯营销K2', 10: '腾讯营销K3'
|
||||
};
|
||||
return typeMap[currentApp.openType] || currentApp.openType;
|
||||
return typeMap[currentApp.open_type] || currentApp.open_type;
|
||||
})()}</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>授权URL:</strong> {currentApp.authUrl || '-'}</p>
|
||||
<p><strong>创建人:</strong> {currentApp.createBy}</p>
|
||||
<p><strong>创建时间:</strong> {formatDate(currentApp.createdAt)}</p>
|
||||
<p><strong>更新时间:</strong> {formatDate(currentApp.updatedAt)}</p>
|
||||
<p><strong>授权URL:</strong> {currentApp.auth_url || '-'}</p>
|
||||
<p><strong>创建人:</strong> {currentApp.create_by}</p>
|
||||
<p><strong>创建时间:</strong> {formatDate(currentApp.created_at)}</p>
|
||||
<p><strong>更新时间:</strong> {formatDate(currentApp.updated_at)}</p>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -426,6 +426,9 @@ export interface GenerationAITaskQueryParams {
|
||||
pageSize?: number;
|
||||
userId?: string;
|
||||
userName?: string;
|
||||
engineId?: string;
|
||||
createdStart?: string;
|
||||
createdEnd?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
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 app.dependencies import get_current_user, get_db
|
||||
@@ -232,6 +232,21 @@ async def list_tasks(
|
||||
description="查询相关用户名的对应记录[管理后台]",
|
||||
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),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -250,6 +265,9 @@ async def list_tasks(
|
||||
page,
|
||||
page_size,
|
||||
is_admin,
|
||||
engine_id=engine_id,
|
||||
created_start=created_start,
|
||||
created_end=created_end,
|
||||
)
|
||||
|
||||
# ====================== 在这里加排序(最新在前)======================
|
||||
|
||||
@@ -21,11 +21,12 @@ async def list_apps(
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
open_type: int | None = Query(None, ge=1, le=10, description="开户方式"),
|
||||
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),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
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 {
|
||||
"total": result["total"],
|
||||
"page": result["page"],
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone, date
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
@@ -507,6 +507,9 @@ async def list_async_generation_tasks(
|
||||
page: int,
|
||||
page_size: int,
|
||||
is_admin: bool = False,
|
||||
engine_id: str | None = None,
|
||||
created_start: datetime | None = None,
|
||||
created_end: datetime | None = None,
|
||||
):
|
||||
if is_admin:
|
||||
query = (
|
||||
@@ -533,6 +536,18 @@ async def list_async_generation_tasks(
|
||||
if 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())
|
||||
total = (await db.execute(count_query)).scalar_one()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import json
|
||||
|
||||
from sqlalchemy import select, delete
|
||||
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.utils.id_gen import generate_id
|
||||
from app.utils.douyinApi import DouyinApi
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
import os
|
||||
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)
|
||||
logger = get_logger("material_consumption_task", "material_consumption")
|
||||
|
||||
douyin_api = DouyinApi()
|
||||
|
||||
|
||||
@@ -22,13 +22,22 @@ async def list_user_oauth_apps(
|
||||
if status is not None:
|
||||
query = query.where(UserOAuthApp.status == status)
|
||||
|
||||
if create_by is not None:
|
||||
query = query.where(UserOAuthApp.create_by == create_by)
|
||||
#if create_by is not None:
|
||||
# query = query.where(UserOAuthApp.create_by == create_by)
|
||||
|
||||
if app_id is not None:
|
||||
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
|
||||
|
||||
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:
|
||||
app.create_by = create_by
|
||||
await db.flush()
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,39 +1,10 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
|
||||
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")
|
||||
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)
|
||||
logger = get_logger("material_consumption_task", "material_consumption_task")
|
||||
|
||||
|
||||
async def schedule_daily_sync():
|
||||
|
||||
@@ -20,7 +20,9 @@ def get_logger(name: str, log_filename: str) -> logging.Logger:
|
||||
if logger.handlers:
|
||||
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)
|
||||
|
||||
class DailyRotatingFileHandler(logging.FileHandler):
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
InfoOutlined,
|
||||
MenuOutlined,
|
||||
ArrowLeftOutlined,
|
||||
RobotOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
@@ -268,7 +269,7 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
StarOutlined: <StarOutlined />,
|
||||
HeartOutlined: <HeartOutlined />,
|
||||
CameraOutlined: <CameraOutlined />,
|
||||
RobotOutlined: <PlayCircleOutlined />,
|
||||
RobotOutlined: <RobotOutlined />,
|
||||
CalculatorOutlined: <CalculatorOutlined />,
|
||||
DollarOutlined: <DollarOutlined />,
|
||||
GiftOutlined: <GiftOutlined />,
|
||||
|
||||
Reference in New Issue
Block a user