1
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>
|
||||||
<script type="module" crossorigin src="/assets/index-CE6vN0iM.js"></script>
|
<script type="module" crossorigin src="/assets/index-DNsZEwFL.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -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,8 +20,10 @@ 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';
|
||||||
@@ -28,12 +31,23 @@ 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 +214,18 @@ const InfoItem: React.FC<{ label: string; value?: React.ReactNode }> = ({ label,
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const todayStart = (): Date => {
|
||||||
|
const d = new Date();
|
||||||
|
d.setHours(0, 0, 0, 0);
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
|
const todayEnd = (): Date => {
|
||||||
|
const d = new Date();
|
||||||
|
d.setHours(23, 59, 59, 999);
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
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 +234,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);
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -358,6 +409,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) => (
|
||||||
@@ -789,16 +903,41 @@ const AdminGenerationAiRecords: React.FC = () => {
|
|||||||
placeholder="类型筛选"
|
placeholder="类型筛选"
|
||||||
value={filterGenType || undefined}
|
value={filterGenType || undefined}
|
||||||
style={{ width: 120 }}
|
style={{ width: 120 }}
|
||||||
onChange={(v) => { setFilterGenType(v || ''); setPage(1); }}
|
onChange={(v) => { setFilterGenType(v || ''); setPage(1); setReloadKey((k) => k + 1); }}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'image', label: '图片' },
|
{ value: 'image', label: '图片' },
|
||||||
{ value: 'video', label: '视频' },
|
{ value: 'video', label: '视频' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
placeholder="引擎筛选"
|
||||||
|
value={filterEngineId || undefined}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={(v) => { setFilterEngineId(v || ''); setPage(1); setQueryEngineId(v || ''); setReloadKey((k) => k + 1); }}
|
||||||
|
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
|
||||||
|
showTime
|
||||||
|
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 +946,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 +955,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 +966,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 +1054,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={() => handleOpenExternalResource(refUrl, refType === 'video' ? '视频附件' : '图片附件')}
|
||||||
|
>
|
||||||
|
{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={() => handleOpenExternalResource(preview.videoUrl, '视频')}
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
新窗口播放
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{preview.genType === 'image' && preview.imageUrl ? (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
icon={<FileImageOutlined />}
|
||||||
|
onClick={() => handleOpenExternalResource(preview.imageUrl, '图片')}
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
新窗口查看
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
{preview.genType === 'video' ? renderResultVideo() : renderResultImage()}
|
{preview.genType === 'video' ? renderResultVideo() : renderResultImage()}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ====================== 在这里加排序(最新在前)======================
|
# ====================== 在这里加排序(最新在前)======================
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user