1
This commit is contained in:
+104
-104
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-I3eif2op.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-mGpmDG8h.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import React from 'react';
|
||||
import { Button, DatePicker, Input, Space } from 'antd';
|
||||
import {
|
||||
CheckOutlined,
|
||||
ClearOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
SearchOutlined,
|
||||
UploadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
interface UnifiedFilterBarProps {
|
||||
// 搜索
|
||||
searchValue: string;
|
||||
searchPlaceholder?: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
// 日期
|
||||
showDate?: boolean;
|
||||
dateValue?: Dayjs | null;
|
||||
onDateChange?: (date: Dayjs | null) => void;
|
||||
datePlaceholder?: string;
|
||||
// 批量操作
|
||||
batchCount: number;
|
||||
totalCount: number;
|
||||
onSelectAll: () => void;
|
||||
onClearSelection: () => void;
|
||||
onDelete: () => void;
|
||||
onDownload?: () => void;
|
||||
onPush?: () => void;
|
||||
pushLoading?: boolean;
|
||||
pushText?: string;
|
||||
// 右侧额外操作
|
||||
extraActions?: React.ReactNode;
|
||||
}
|
||||
|
||||
const btnBase: React.CSSProperties = {
|
||||
borderRadius: 8,
|
||||
fontWeight: 600,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
const btnSecondary: React.CSSProperties = {
|
||||
...btnBase,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#334155',
|
||||
};
|
||||
|
||||
const btnDanger: React.CSSProperties = {
|
||||
...btnBase,
|
||||
background: 'linear-gradient(135deg, #ef4444, #dc2626)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
};
|
||||
|
||||
const btnPrimary: React.CSSProperties = {
|
||||
...btnBase,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
};
|
||||
|
||||
const UnifiedFilterBar: React.FC<UnifiedFilterBarProps> = ({
|
||||
searchValue,
|
||||
searchPlaceholder = '搜索...',
|
||||
onSearchChange,
|
||||
onSearch,
|
||||
showDate = false,
|
||||
dateValue,
|
||||
onDateChange,
|
||||
datePlaceholder = '选择日期',
|
||||
batchCount,
|
||||
totalCount,
|
||||
onSelectAll,
|
||||
onClearSelection,
|
||||
onDelete,
|
||||
onDownload,
|
||||
onPush,
|
||||
pushLoading = false,
|
||||
pushText = '推送至账户',
|
||||
extraActions,
|
||||
}) => {
|
||||
const allSelected = batchCount === totalCount && totalCount > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
padding: '12px 16px',
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
}}
|
||||
>
|
||||
{/* 左侧:搜索 + 日期 */}
|
||||
<Space size={8} wrap>
|
||||
<Input.Search
|
||||
placeholder={searchPlaceholder}
|
||||
allowClear
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
onSearchChange(val);
|
||||
if (!val) onSearch();
|
||||
}}
|
||||
onSearch={onSearch}
|
||||
style={{ width: 240, borderRadius: 8 }}
|
||||
enterButton
|
||||
/>
|
||||
{showDate && (
|
||||
<>
|
||||
<DatePicker
|
||||
value={dateValue || undefined}
|
||||
onChange={(date) => onDateChange?.(date)}
|
||||
format="YYYY-MM-DD"
|
||||
placeholder={datePlaceholder}
|
||||
style={{ width: 160, borderRadius: 8 }}
|
||||
allowClear
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{/* 右侧:批量操作 + 额外操作 */}
|
||||
<Space size={8} wrap>
|
||||
{/* 全选 / 取消选择 */}
|
||||
<Button
|
||||
icon={allSelected ? <ClearOutlined /> : <CheckOutlined />}
|
||||
onClick={allSelected ? onClearSelection : onSelectAll}
|
||||
style={btnSecondary}
|
||||
>
|
||||
{allSelected ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
|
||||
{/* 删除 */}
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={onDelete}
|
||||
disabled={batchCount === 0}
|
||||
style={batchCount === 0 ? { ...btnSecondary, opacity: 0.5 } : btnDanger}
|
||||
>
|
||||
删除 {batchCount > 0 && `(${batchCount})`}
|
||||
</Button>
|
||||
|
||||
{/* 下载 */}
|
||||
{onDownload && (
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onDownload}
|
||||
disabled={batchCount === 0}
|
||||
style={batchCount === 0 ? { ...btnSecondary, opacity: 0.5 } : btnPrimary}
|
||||
>
|
||||
下载 {batchCount > 0 && `(${batchCount})`}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 推送 */}
|
||||
{onPush && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UploadOutlined />}
|
||||
onClick={onPush}
|
||||
loading={pushLoading}
|
||||
disabled={batchCount === 0}
|
||||
style={batchCount === 0 ? { ...btnBase, opacity: 0.5 } : btnPrimary}
|
||||
>
|
||||
{pushLoading ? '推送中...' : `${pushText} ${batchCount > 0 ? `(${batchCount})` : ''}`}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 额外操作按钮 */}
|
||||
{extraActions}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedFilterBar;
|
||||
@@ -30,6 +30,7 @@ interface UploadSelectorProps {
|
||||
usedAudioCount?: number;
|
||||
maxAudioDuration?: number;
|
||||
usedAudioDuration?: number;
|
||||
hideLimitHint?: boolean;
|
||||
}
|
||||
|
||||
const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
@@ -52,6 +53,7 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
usedAudioCount,
|
||||
maxAudioDuration,
|
||||
usedAudioDuration,
|
||||
hideLimitHint,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [portraitPickerOpen, setPortraitPickerOpen] = useState(false);
|
||||
@@ -243,6 +245,7 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
usedAudioCount={usedAudioCount}
|
||||
maxAudioDuration={maxAudioDuration}
|
||||
usedAudioDuration={usedAudioDuration}
|
||||
hideLimitHint={hideLimitHint}
|
||||
/>
|
||||
|
||||
<PrivatePortraitAssetPicker
|
||||
|
||||
@@ -120,7 +120,7 @@ const PrivatePortraitLibraryPanel: React.FC = () => {
|
||||
activeKey={activeKey}
|
||||
onChange={handleTabChange}
|
||||
items={items}
|
||||
destroyInactiveTabPane={false}
|
||||
destroyOnHidden={false}
|
||||
tabBarExtraContent={
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: '#4f46e5', marginTop: 8 }}>{quotaText}。真人/虚拟共用,图片/视频共用;音频暂不开放。</div>
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
const PrivatePortraitProjectList: React.FC<Props> = ({ items, selectedId, onSelect }) => {
|
||||
if (!items.length) return <Empty description="暂无项目组" />;
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={10}>
|
||||
<Space orientation="vertical" style={{ width: '100%' }} size={10}>
|
||||
{items.map((project) => {
|
||||
const active = selectedId === project.id;
|
||||
return (
|
||||
|
||||
@@ -122,7 +122,14 @@ const RealPersonLibraryPanel: React.FC = () => {
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>真人素材库</Typography.Title>
|
||||
<Typography.Text type="secondary">创建项目组时先完成真人认证,认证成功后项目组才会正式创建并可上传素材。</Typography.Text>
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>创建项目组</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
|
||||
>
|
||||
创建项目组
|
||||
</Button>
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} lg={5}>
|
||||
@@ -155,7 +162,7 @@ const RealPersonLibraryPanel: React.FC = () => {
|
||||
onOk={validateSession ? undefined : handleCreate}
|
||||
okText="开始认证并创建"
|
||||
confirmLoading={creating}
|
||||
maskClosable={!polling}
|
||||
mask={{ closable: !polling }}
|
||||
>
|
||||
{!validateSession ? (
|
||||
<Form form={form} layout="vertical">
|
||||
|
||||
@@ -338,7 +338,7 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
<Card
|
||||
key={asset.id}
|
||||
hoverable
|
||||
bodyStyle={{ padding: 12 }}
|
||||
styles={{ body: { padding: 12 } }}
|
||||
style={{ borderRadius: 16, overflow: 'hidden', borderColor: '#eef2f7' }}
|
||||
cover={(
|
||||
<div style={{ height: 170, background: '#f8fafc', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
|
||||
@@ -392,7 +392,14 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>虚拟素材库</Typography.Title>
|
||||
<Typography.Text type="secondary">可提前上传虚拟人像素材,后续生成视频时可直接使用。</Typography.Text>
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>创建项目组</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateOpen(true)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
|
||||
>
|
||||
创建项目组
|
||||
</Button>
|
||||
</div>
|
||||
<Row gutter={[10, 10]}>
|
||||
<Col xs={24} lg={5} >
|
||||
@@ -446,11 +453,24 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
title={selectedProject ? selectedProject.name : '素材资产'}
|
||||
extra={(
|
||||
<Space wrap>
|
||||
{/* <Button icon={<ReloadOutlined />} onClick={() => { loadProjects(); loadAssets(assetPage, assetPageSize); }} loading={assetLoading}>刷新</Button> */}
|
||||
<Button type="primary" icon={<UploadOutlined />} disabled={!selectedProjectId} onClick={() => setUploadOpen(true)}>上传图片/视频</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UploadOutlined />}
|
||||
disabled={!selectedProjectId}
|
||||
onClick={() => setUploadOpen(true)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: 'transparent' }}
|
||||
>
|
||||
上传图片/视频
|
||||
</Button>
|
||||
{selectedProjectId && (
|
||||
<Popconfirm title="确认删除当前虚拟人像项目组吗?" onConfirm={handleDeleteProject}>
|
||||
<Button danger icon={<DeleteOutlined />}>删除项目组</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ borderRadius: 8, fontWeight: 600 }}
|
||||
>
|
||||
删除项目组
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
@@ -464,14 +484,15 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onSearch={() => loadAssets(1, assetPageSize)}
|
||||
style={{ width: 240 }}
|
||||
style={{ width: 240, borderRadius: 8 }}
|
||||
enterButton
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="素材状态"
|
||||
value={assetStatus}
|
||||
onChange={(value) => setAssetStatus(value)}
|
||||
style={{ width: 150 }}
|
||||
style={{ width: 140, borderRadius: 8 }}
|
||||
options={[
|
||||
{ value: 'Processing', label: '处理中' },
|
||||
{ value: 'Active', label: '可用' },
|
||||
@@ -483,13 +504,19 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
placeholder="素材类型"
|
||||
value={assetType}
|
||||
onChange={(value) => setAssetType(value)}
|
||||
style={{ width: 130 }}
|
||||
style={{ width: 120, borderRadius: 8 }}
|
||||
options={[
|
||||
{ value: 'Image', label: '图片' },
|
||||
{ value: 'Video', label: '视频' },
|
||||
]}
|
||||
/>
|
||||
<Button onClick={() => loadAssets(1, assetPageSize)}>筛选</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => loadAssets(1, assetPageSize)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: 'transparent' }}
|
||||
>
|
||||
筛选
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Spin spinning={assetLoading}>
|
||||
@@ -563,7 +590,7 @@ const VirtualMaterialPanel: React.FC = () => {
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal title="素材预览" open={previewOpen} onCancel={() => setPreviewOpen(false)} footer={null} width={760} destroyOnClose>
|
||||
<Modal title="素材预览" open={previewOpen} onCancel={() => setPreviewOpen(false)} footer={null} width={760} destroyOnHidden>
|
||||
<div style={{ minHeight: 420, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0f172a', borderRadius: 12, overflow: 'hidden' }}>
|
||||
{previewType === 'Video' ? (
|
||||
<video src={previewUrl} controls autoPlay style={{ maxWidth: '100%', maxHeight: 520 }} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Empty, Input, List, Modal, Select, Space, Spin, Tag, Typography, message } from 'antd';
|
||||
import { Button, Empty, Input, Modal, Select, Space, Spin, Tag, Typography, message } from 'antd';
|
||||
import { CheckOutlined, PictureOutlined, ReloadOutlined, SearchOutlined, VideoCameraOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
getPrivatePortraitProjects,
|
||||
@@ -251,33 +251,36 @@ const PrivatePortraitAssetPicker: React.FC<PrivatePortraitAssetPickerProps> = ({
|
||||
<Button size="small" icon={<ReloadOutlined />} onClick={loadProjects} loading={loadingProjects} />
|
||||
</Space>
|
||||
<Spin spinning={loadingProjects}>
|
||||
<List
|
||||
dataSource={projects}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无项目组" /> }}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setProjectId(item.id)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
marginBottom: 6,
|
||||
border: projectId === item.id ? '1px solid #8b5cf6' : '1px solid transparent',
|
||||
background: projectId === item.id ? '#f5f3ff' : '#fff',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<Text strong ellipsis style={{ display: 'block' }}>{item.name}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{/* {item.activeAssetCount || 0} */}
|
||||
{typeof item.activeImageAssetCount === 'number' || typeof item.activeVideoAssetCount === 'number'
|
||||
? ` 图 ${item.activeImageAssetCount || 0} / 视 ${item.activeVideoAssetCount || 0}`
|
||||
: ''}
|
||||
</Text>
|
||||
{projects.length === 0 ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无项目组" />
|
||||
) : (
|
||||
<div>
|
||||
{projects.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => setProjectId(item.id)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '10px 12px',
|
||||
borderRadius: 10,
|
||||
marginBottom: 6,
|
||||
border: projectId === item.id ? '1px solid #8b5cf6' : '1px solid transparent',
|
||||
background: projectId === item.id ? '#f5f3ff' : '#fff',
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%' }}>
|
||||
<Text strong ellipsis style={{ display: 'block' }}>{item.name}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{/* {item.activeAssetCount || 0} */}
|
||||
{typeof item.activeImageAssetCount === 'number' || typeof item.activeVideoAssetCount === 'number'
|
||||
? ` 图 ${item.activeImageAssetCount || 0} / 视 ${item.activeVideoAssetCount || 0}`
|
||||
: ''}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -173,45 +173,93 @@ const UploadResourceHistoryPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<Space wrap>
|
||||
<Select
|
||||
value={resourceType}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '视频', value: 'video' },
|
||||
{ label: '音频', value: 'audio' },
|
||||
]}
|
||||
onChange={(value) => {
|
||||
setResourceType(value);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
{/* 统一筛选栏:媒体类型 + 搜索 + 批量操作 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
padding: '12px 16px',
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
{/* 左侧:媒体类型按钮 */}
|
||||
<Space size={8} wrap>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14, marginRight: 4 }}>媒体类型:</Typography.Text>
|
||||
<Button
|
||||
type={resourceType === '' ? 'primary' : 'default'}
|
||||
onClick={() => { setResourceType(''); setPage(1); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === '' ? 'transparent' : '#e2e8f0', color: resourceType === '' ? '#fff' : '#64748b' }}
|
||||
>
|
||||
全部
|
||||
</Button>
|
||||
<Button
|
||||
type={resourceType === 'image' ? 'primary' : 'default'}
|
||||
onClick={() => { setResourceType('image'); setPage(1); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === 'image' ? 'transparent' : '#e2e8f0', color: resourceType === 'image' ? '#fff' : '#64748b' }}
|
||||
>
|
||||
图片
|
||||
</Button>
|
||||
<Button
|
||||
type={resourceType === 'video' ? 'primary' : 'default'}
|
||||
onClick={() => { setResourceType('video'); setPage(1); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === 'video' ? 'transparent' : '#e2e8f0', color: resourceType === 'video' ? '#fff' : '#64748b' }}
|
||||
>
|
||||
视频
|
||||
</Button>
|
||||
<Button
|
||||
type={resourceType === 'audio' ? 'primary' : 'default'}
|
||||
onClick={() => { setResourceType('audio'); setPage(1); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === 'audio' ? 'transparent' : '#e2e8f0', color: resourceType === 'audio' ? '#fff' : '#64748b' }}
|
||||
>
|
||||
音频
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{/* 右侧:搜索 + 批量操作 */}
|
||||
<Space size={8} wrap>
|
||||
<Input.Search
|
||||
allowClear
|
||||
value={keyword}
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索文件名或URL"
|
||||
style={{ width: 240 }}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onPressEnter={() => {
|
||||
setPage(1);
|
||||
load();
|
||||
}}
|
||||
onSearch={() => { setPage(1); load(); }}
|
||||
style={{ width: 240, borderRadius: 8 }}
|
||||
enterButton
|
||||
/>
|
||||
{/* <Button icon={<ReloadOutlined />} onClick={load}>刷新</Button> */}
|
||||
</Space>
|
||||
<Space>
|
||||
{batchMode ? (
|
||||
<>
|
||||
<Button onClick={selectAll}>{selectedIds.size && selectedIds.size === allItems.length ? '取消全选' : '全选'}</Button>
|
||||
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete}>批量删除 ({selectedIds.size})</Button>
|
||||
<Button onClick={() => { setBatchMode(false); setSelectedIds(new Set()); }}>取消批量操作</Button>
|
||||
<Button
|
||||
onClick={selectAll}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#334155' }}
|
||||
>
|
||||
{selectedIds.size && selectedIds.size === allItems.length ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={handleBatchDelete}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #ef4444, #dc2626)', border: 'none', color: '#fff' }}
|
||||
>
|
||||
批量删除 {selectedIds.size > 0 && `(${selectedIds.size})`}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => { setBatchMode(false); setSelectedIds(new Set()); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#64748b' }}
|
||||
>
|
||||
取消选择
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={() => setBatchMode(true)}>批量操作</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setBatchMode(true)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
|
||||
>
|
||||
批量操作
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
@@ -232,7 +280,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
|
||||
const checked = selectedIds.has(item.id);
|
||||
return (
|
||||
<div key={item.id} style={{ width: '17%', minWidth: 240, margin: 16, border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', borderRadius: 16, overflow: 'hidden', background: '#fff', boxShadow: '0 10px 24px rgba(15,23,42,0.06)' }}>
|
||||
<div style={{ position: 'relative', background: '#f1f5f9' }}>
|
||||
<div style={{ position: 'relative', background: '#f1f5f9', cursor: batchMode ? 'pointer' : 'default' }} onClick={() => batchMode && toggle(item.id)}>
|
||||
{renderMedia(item)}
|
||||
{batchMode && <Checkbox checked={checked} onChange={() => toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} />}
|
||||
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ position: 'absolute', top: 10, right: 10, margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
|
||||
|
||||
@@ -26,15 +26,72 @@ interface UploadResourceHistoryPickerProps {
|
||||
usedAudioCount?: number;
|
||||
maxAudioDuration?: number;
|
||||
usedAudioDuration?: number;
|
||||
hideLimitHint?: boolean;
|
||||
}
|
||||
|
||||
const buildPreviewUrl = (url: string) => {
|
||||
if (!url) return '';
|
||||
if (/^(https?:|data:|blob:)/i.test(url)) return url;
|
||||
if (/^data:|blob:/i.test(url)) return url;
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.pathname.startsWith('/uploads')) {
|
||||
return parsed.pathname + parsed.search;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, '');
|
||||
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
};
|
||||
|
||||
const validateImageDimensions = (width: number, height: number): string | null => {
|
||||
if (width < 300 || width > 6000) return `图片宽度需在 300~6000px 之间,当前为 ${width}px`;
|
||||
if (height < 300 || height > 6000) return `图片高度需在 300~6000px 之间,当前为 ${height}px`;
|
||||
const ratio = width / height;
|
||||
if (ratio < 0.4 || ratio > 2.5) return `图片宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateVideoDimensions = (width: number, height: number): string | null => {
|
||||
if (width < 300 || width > 6000) return `视频宽度需在 300~6000px 之间,当前为 ${width}px`;
|
||||
if (height < 300 || height > 6000) return `视频高度需在 300~6000px 之间,当前为 ${height}px`;
|
||||
const ratio = width / height;
|
||||
if (ratio < 0.4 || ratio > 2.5) return `视频宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
|
||||
const totalPixels = width * height;
|
||||
if (totalPixels < 409600) return `视频总像素数过小(${width}×${height}=${totalPixels}),需 ≥ 640×640=409600`;
|
||||
if (totalPixels > 8295044) return `视频总像素数过大(${width}×${height}=${totalPixels}),需 ≤ 3326×2494=8295044`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const getItemValidationError = (item: UploadResourceHistoryItem): string | null => {
|
||||
const itemAny = item as any;
|
||||
const width = itemAny.width || itemAny.videoWidth || itemAny.imageWidth || 0;
|
||||
const height = itemAny.height || itemAny.videoHeight || itemAny.imageHeight || 0;
|
||||
|
||||
if (item.resourceType === 'image') {
|
||||
if (width > 0 && height > 0) {
|
||||
return validateImageDimensions(width, height);
|
||||
}
|
||||
} else if (item.resourceType === 'video') {
|
||||
if (width > 0 && height > 0) {
|
||||
return validateVideoDimensions(width, height);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getImageDimensions = (url: string): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
resolve({ width: img.width, height: img.height });
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve({ width: 0, height: 0 });
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
const typeIcon = (type: string) => {
|
||||
if (type === 'video') return <VideoCameraOutlined />;
|
||||
if (type === 'audio') return <AudioOutlined />;
|
||||
@@ -64,6 +121,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
usedAudioCount,
|
||||
maxAudioDuration,
|
||||
usedAudioDuration,
|
||||
hideLimitHint,
|
||||
}) => {
|
||||
const [resourceType, setResourceType] = useState<MediaTypeFilter>('');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
@@ -175,11 +233,42 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
if (open) setCheckedMap(new Map());
|
||||
}, [open]);
|
||||
|
||||
const toggle = (item: UploadResourceHistoryItem) => {
|
||||
const toggle = async (item: UploadResourceHistoryItem) => {
|
||||
if (selectedIdSet.has(item.id)) {
|
||||
message.warning('该素材已经在参考内容中');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemAny = item as any;
|
||||
let width = itemAny.width || itemAny.videoWidth || itemAny.imageWidth || 0;
|
||||
let height = itemAny.height || itemAny.videoHeight || itemAny.imageHeight || 0;
|
||||
|
||||
if (item.resourceType === 'image') {
|
||||
if (width <= 0 || height <= 0) {
|
||||
const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl);
|
||||
if (preview) {
|
||||
const dims = await getImageDimensions(preview);
|
||||
width = dims.width;
|
||||
height = dims.height;
|
||||
}
|
||||
}
|
||||
if (width > 0 && height > 0) {
|
||||
const error = validateImageDimensions(width, height);
|
||||
if (error) {
|
||||
message.error(`${item.fileName || '图片'}:${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (item.resourceType === 'video') {
|
||||
if (width > 0 && height > 0) {
|
||||
const error = validateVideoDimensions(width, height);
|
||||
if (error) {
|
||||
message.error(`${item.fileName || '视频'}:${error}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCheckedMap((prev) => {
|
||||
const next = new Map(prev);
|
||||
if (next.has(item.id)) next.delete(item.id);
|
||||
@@ -249,19 +338,21 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadGroups}>刷新</Button>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 12, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 13, color: imageExceeded ? '#ef4444' : '#64748b', fontWeight: imageExceeded ? 600 : 400 }}>
|
||||
还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张
|
||||
</span>
|
||||
<span style={{ fontSize: 13, color: videoExceeded || durationExceeded ? '#ef4444' : '#64748b', fontWeight: videoExceeded || durationExceeded ? 600 : 400 }}>
|
||||
视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒)
|
||||
</span>
|
||||
{maxAudioCount !== undefined && (
|
||||
<span style={{ fontSize: 13, color: audioExceeded || audioDurationExceeded ? '#ef4444' : '#64748b', fontWeight: audioExceeded || audioDurationExceeded ? 600 : 400 }}>
|
||||
音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒)
|
||||
{!hideLimitHint && (
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 12, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 13, color: imageExceeded ? '#ef4444' : '#64748b', fontWeight: imageExceeded ? 600 : 400 }}>
|
||||
还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: videoExceeded || durationExceeded ? '#ef4444' : '#64748b', fontWeight: videoExceeded || durationExceeded ? 600 : 400 }}>
|
||||
视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒)
|
||||
</span>
|
||||
{maxAudioCount !== undefined && (
|
||||
<span style={{ fontSize: 13, color: audioExceeded || audioDurationExceeded ? '#ef4444' : '#64748b', fontWeight: audioExceeded || audioDurationExceeded ? 600 : 400 }}>
|
||||
音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '240px 1fr', gap: 16, height: 500 }}>
|
||||
@@ -310,7 +401,9 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14 }}>
|
||||
{items.map((item) => {
|
||||
const checked = checkedMap.has(item.id);
|
||||
const disabled = selectedIdSet.has(item.id);
|
||||
const selectedDisabled = selectedIdSet.has(item.id);
|
||||
const validationError = getItemValidationError(item);
|
||||
const disabled = selectedDisabled || !!validationError;
|
||||
const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl);
|
||||
return (
|
||||
<div
|
||||
@@ -326,6 +419,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
overflow: 'hidden',
|
||||
boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)',
|
||||
}}
|
||||
title={validationError ? validationError : undefined}
|
||||
>
|
||||
<div style={{ background: '#f1f5f9' }}>
|
||||
{item.resourceType === 'image' ? (
|
||||
@@ -340,6 +434,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
<Space size={6} style={{ marginBottom: 6 }}>
|
||||
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
|
||||
{item.durationSeconds ? <Tag style={{ margin: 0 }}>{Number(item.durationSeconds).toFixed(1)}s</Tag> : null}
|
||||
{validationError && <Tag color="red" style={{ margin: 0 }}>尺寸不符</Tag>}
|
||||
</Space>
|
||||
<Text ellipsis style={{ display: 'block', fontSize: 13, color: '#334155' }} title={item.fileName || item.id}>
|
||||
{item.fileName || item.id}
|
||||
@@ -352,7 +447,12 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
<CheckOutlined />
|
||||
</div>
|
||||
)}
|
||||
{disabled && <div style={{ position: 'absolute', top: 8, right: 8 }}><Tag>已选</Tag></div>}
|
||||
{selectedDisabled && <div style={{ position: 'absolute', top: 8, right: 8 }}><Tag>已选</Tag></div>}
|
||||
{validationError && (
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'rgba(239, 68, 68, 0.9)', color: '#fff', padding: '4px 8px', fontSize: 11, textAlign: 'center', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{validationError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -7,6 +7,9 @@ import dayjs from 'dayjs';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
// 统一按钮样式
|
||||
const btnBase: React.CSSProperties = { borderRadius: 8, fontWeight: 600 };
|
||||
|
||||
const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
|
||||
interface ConsumptionRecord {
|
||||
@@ -164,13 +167,24 @@ const ConsumePage: React.FC = () => {
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>消耗记录</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
padding: '12px 16px',
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="广告主ID"
|
||||
value={advertiserId}
|
||||
onChange={(e) => setAdvertiserId(e.target.value)}
|
||||
style={{ width: 180 }}
|
||||
style={{ width: 180, borderRadius: 8 }}
|
||||
allowClear
|
||||
onPressEnter={() => { setCurrentPage(1); loadData(1, pageSize); }}
|
||||
/>
|
||||
@@ -183,25 +197,32 @@ const ConsumePage: React.FC = () => {
|
||||
setConsumeDateRange(undefined);
|
||||
}
|
||||
}}
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="medium"
|
||||
onClick={handleSearch}
|
||||
style={{ ...btnBase, borderColor: 'transparent' }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
onClick={handleSync}
|
||||
loading={syncLoading}
|
||||
style={{ borderRadius: 8 }}
|
||||
style={btnBase}
|
||||
>
|
||||
拉取消耗
|
||||
</Button>
|
||||
<Button icon={<SettingOutlined />} onClick={() => setShowModal(true)} style={{ borderRadius: 8 }}>自定义表头</Button>
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => setShowModal(true)}
|
||||
style={{ ...btnBase, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#334155' }}
|
||||
>
|
||||
自定义表头
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const CreditRecordsPage: React.FC = () => {
|
||||
consume: { color: '#ef4444', label: '消费', bg: 'rgba(239,68,68,0.1)' },
|
||||
admin: { color: '#f59e0b', label: '管理员调整', bg: 'rgba(245,158,11,0.1)' },
|
||||
refund: { color: '#8b5cf6', label: '退款', bg: 'rgba(139,92,246,0.1)' },
|
||||
team_internal: { color: '#0958d9', label: '团队内部', bg: 'rgba(9, 88, 217, 0.1)' },
|
||||
team_internal: { color: '#0958d9', label: '团队内部', bg: 'rgba(139,92,246,0.1)' },
|
||||
};
|
||||
const config = typeConfig[text] || { color: '#64748b', label: text, bg: 'rgba(100,116,139,0.1)' };
|
||||
return (
|
||||
|
||||
@@ -978,7 +978,7 @@ const AIChatPage: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
console.log(mediaReferences);
|
||||
// console.log(mediaReferences);
|
||||
|
||||
// 创建用户消息对象
|
||||
const newMessage: Message = {
|
||||
@@ -1240,7 +1240,7 @@ const AIChatPage: React.FC = () => {
|
||||
|
||||
try {
|
||||
const { width, height } = await getImageDimensions(file);
|
||||
const error = validateVideoDimensions(width, height);
|
||||
const error = validateImageDimensions(width, height);
|
||||
if (error) {
|
||||
antdMessage.error(error);
|
||||
return false;
|
||||
@@ -1316,7 +1316,7 @@ const AIChatPage: React.FC = () => {
|
||||
if (isImage) {
|
||||
try {
|
||||
const { width, height } = await getImageDimensions(file);
|
||||
const error = validateVideoDimensions(width, height);
|
||||
const error = validateImageDimensions(width, height);
|
||||
if (error) {
|
||||
antdMessage.error(error);
|
||||
return false;
|
||||
@@ -1456,7 +1456,7 @@ const AIChatPage: React.FC = () => {
|
||||
if (isImage) {
|
||||
try {
|
||||
const { width, height } = await getImageDimensions(file);
|
||||
const error = validateVideoDimensions(width, height);
|
||||
const error = validateImageDimensions(width, height);
|
||||
if (error) {
|
||||
antdMessage.error(error);
|
||||
return false;
|
||||
@@ -2183,13 +2183,16 @@ const AIChatPage: React.FC = () => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setInputValue(msg.originalPrompt || '');
|
||||
if (msg.genType) {
|
||||
setMediaType(msg.genType);
|
||||
}
|
||||
if (msg.mediaReferences && msg.mediaReferences.length > 0) {
|
||||
const hasFirstLastFrame = msg.mediaReferences.some((ref: any) => ref.role === 'first_frame' || ref.role === 'last_frame');
|
||||
if (hasFirstLastFrame && msg.genType === 'video') {
|
||||
const first = msg.mediaReferences.find((ref: any) => ref.role === 'first_frame');
|
||||
const last = msg.mediaReferences.find((ref: any) => ref.role === 'last_frame');
|
||||
setFirstFrame(first ? { ...first, label: first.label || '' } : null);
|
||||
setLastFrame(last ? { ...last, label: last.label || '' } : null);
|
||||
setFirstFrame(first ? { ...first, label: first.label || '', duration: first.duration } : null);
|
||||
setLastFrame(last ? { ...last, label: last.label || '', duration: last.duration } : null);
|
||||
setCurrentMedia([]);
|
||||
setReferenceMode('first_last_frame');
|
||||
} else {
|
||||
@@ -2199,6 +2202,7 @@ const AIChatPage: React.FC = () => {
|
||||
url: ref.url,
|
||||
label: ref.label || '',
|
||||
role: ref.role,
|
||||
duration: ref.duration,
|
||||
})));
|
||||
setFirstFrame(null);
|
||||
setLastFrame(null);
|
||||
@@ -2887,6 +2891,7 @@ const AIChatPage: React.FC = () => {
|
||||
usedAudioCount={currentMedia.filter(m => m.type === 'audio').length}
|
||||
maxAudioDuration={15}
|
||||
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||
hideLimitHint={mediaType === 'image'}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@@ -3068,6 +3073,7 @@ const AIChatPage: React.FC = () => {
|
||||
usedAudioCount={currentMedia.filter(m => m.type === 'audio').length}
|
||||
maxAudioDuration={15}
|
||||
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||
hideLimitHint={mediaType === 'image'}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -261,6 +261,25 @@ const GeneratePage: React.FC = () => {
|
||||
const [creditRatios, setCreditRatios] = useState<any>([]);
|
||||
const [cimage, setCimage] = useState<any>([]);
|
||||
|
||||
const validateImageDimensions = (width: number, height: number): string | null => {
|
||||
if (width < 300 || width > 6000) return `图片宽度需在 300~6000px 之间,当前为 ${width}px`;
|
||||
if (height < 300 || height > 6000) return `图片高度需在 300~6000px 之间,当前为 ${height}px`;
|
||||
const ratio = width / height;
|
||||
if (ratio < 0.4 || ratio > 2.5) return `图片宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const validateVideoDimensions = (width: number, height: number): string | null => {
|
||||
if (width < 300 || width > 6000) return `视频宽度需在 300~6000px 之间,当前为 ${width}px`;
|
||||
if (height < 300 || height > 6000) return `视频高度需在 300~6000px 之间,当前为 ${height}px`;
|
||||
const ratio = width / height;
|
||||
if (ratio < 0.4 || ratio > 2.5) return `视频宽高比需在 0.4~2.5 之间,当前为 ${ratio.toFixed(2)}`;
|
||||
const totalPixels = width * height;
|
||||
if (totalPixels < 409600) return `视频总像素数过小(${width}×${height}=${totalPixels}),需 ≥ 640×640=409600`;
|
||||
if (totalPixels > 8295044) return `视频总像素数过大(${width}×${height}=${totalPixels}),需 ≤ 3326×2494=8295044`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const handlePasteUpload = async (file: File) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isVideo = file.type.startsWith("video/");
|
||||
@@ -299,23 +318,51 @@ const GeneratePage: React.FC = () => {
|
||||
}
|
||||
let fileDuration = 0;
|
||||
if (isVideo) {
|
||||
fileDuration = await new Promise<number>((resolve) => {
|
||||
const videoInfo = await new Promise<{ duration: number; width: number; height: number }>((resolve) => {
|
||||
const video = document.createElement("video");
|
||||
video.preload = "metadata";
|
||||
video.onloadedmetadata = () => {
|
||||
resolve(video.duration || 0);
|
||||
resolve({ duration: video.duration || 0, width: video.videoWidth || 0, height: video.videoHeight || 0 });
|
||||
video.remove();
|
||||
};
|
||||
video.onerror = () => {
|
||||
resolve(0);
|
||||
resolve({ duration: 0, width: 0, height: 0 });
|
||||
video.remove();
|
||||
};
|
||||
video.src = URL.createObjectURL(file);
|
||||
});
|
||||
fileDuration = videoInfo.duration;
|
||||
if (videoInfo.width > 0 && videoInfo.height > 0) {
|
||||
const error = validateVideoDimensions(videoInfo.width, videoInfo.height);
|
||||
if (error) {
|
||||
message.error(`${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (videoDuration + fileDuration > MAX_VIDEO_DURATION) {
|
||||
message.error(`视频总时长不能超过${MAX_VIDEO_DURATION}秒`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const imageInfo = await new Promise<{ width: number; height: number }>((resolve) => {
|
||||
const img = document.createElement('img');
|
||||
img.onload = () => {
|
||||
resolve({ width: img.width, height: img.height });
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve({ width: 0, height: 0 });
|
||||
URL.revokeObjectURL(img.src);
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
if (imageInfo.width > 0 && imageInfo.height > 0) {
|
||||
const error = validateImageDimensions(imageInfo.width, imageInfo.height);
|
||||
if (error) {
|
||||
message.error(`${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
setUploading(true);
|
||||
try {
|
||||
@@ -1968,7 +2015,7 @@ const GeneratePage: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
rows={3}
|
||||
placeholder="上传参考素材、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
|
||||
placeholder="上传参考素材(只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
|
||||
maxLength={500}
|
||||
bordered={false}
|
||||
autoSize={{ minRows: 2, maxRows: 6 }}
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
DeleteOutlined,
|
||||
LoadingOutlined,
|
||||
UserOutlined,
|
||||
CheckOutlined,
|
||||
ClearOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { PrivatePortraitLibraryPanel } from '../components/privatePortrait';
|
||||
@@ -299,7 +301,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
folder?.file(filename, blob);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`);
|
||||
// console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`);
|
||||
hasError = true;
|
||||
break;
|
||||
}
|
||||
@@ -448,7 +450,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
const res = await getPreTestList({ page: 1, pageSize: 100 });
|
||||
setPreTestTemplates(res.data?.data || res.data || []);
|
||||
} catch (error) {
|
||||
console.error('加载前测模板失败:', error);
|
||||
// console.error('加载前测模板失败:', error);
|
||||
} finally {
|
||||
setPreTestTemplatesLoading(false);
|
||||
}
|
||||
@@ -489,7 +491,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
setOauthList(data || []);
|
||||
setOauthTotal(res.pagination.total || 0);
|
||||
} catch (error) {
|
||||
console.error('加载授权列表失败:', error);
|
||||
// console.error('加载授权列表失败:', error);
|
||||
setOauthList([]);
|
||||
setOauthTotal(0);
|
||||
} finally {
|
||||
@@ -513,7 +515,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
});
|
||||
setOpenTypeMap(map);
|
||||
} catch (error) {
|
||||
console.error('加载历史授权账户列表失败:', error);
|
||||
// console.error('加载历史授权账户列表失败:', error);
|
||||
setHistoryOAuthList([]);
|
||||
setOpenTypeMap({});
|
||||
} finally {
|
||||
@@ -532,7 +534,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
setUploadHistoryList(data || []);
|
||||
setUploadHistoryTotal(res.pagination?.total || res.total || 0);
|
||||
} catch (error) {
|
||||
console.error('加载推送历史失败:', error);
|
||||
// console.error('加载推送历史失败:', error);
|
||||
setUploadHistoryList([]);
|
||||
setUploadHistoryTotal(0);
|
||||
} finally {
|
||||
@@ -669,7 +671,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
setIsPreTest('2');
|
||||
setPreTestTemplate('');
|
||||
} catch (error: any) {
|
||||
console.error('批量推送失败:', error);
|
||||
// console.error('批量推送失败:', error);
|
||||
message.error(error.message || '批量推送失败');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
@@ -699,7 +701,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
});
|
||||
message.success('文件名更新成功');
|
||||
} catch (error: any) {
|
||||
console.error('文件名更新失败:', error);
|
||||
// console.error('文件名更新失败:', error);
|
||||
message.error(error.message || '文件名更新失败');
|
||||
}
|
||||
};
|
||||
@@ -733,11 +735,11 @@ const GeneratedRecord: React.FC = () => {
|
||||
}),
|
||||
}));
|
||||
});
|
||||
console.log(response);
|
||||
// console.log(response);
|
||||
const successCount = response?.successCount || 0;
|
||||
message.success(`已更新 ${successCount} 个文件名`);
|
||||
} catch (error: any) {
|
||||
console.error('文件名更新失败:', error);
|
||||
// console.error('文件名更新失败:', error);
|
||||
message.error(error.message || '文件名更新失败');
|
||||
}
|
||||
};
|
||||
@@ -909,10 +911,36 @@ const GeneratedRecord: React.FC = () => {
|
||||
}, [filterType, filterMedia]);
|
||||
return (
|
||||
<div className="content_box" >
|
||||
{/* 操作栏:筛选 + 推送按钮 */}
|
||||
{/* 顶部:标签切换 */}
|
||||
<Tabs
|
||||
activeKey={filterType}
|
||||
onChange={(key) => {
|
||||
setFilterType(key as typeof filterType);
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
items={[
|
||||
{ key: 'project', label: <span><FolderOpenOutlined /> 项目记录</span> },
|
||||
{ key: 'creation', label: <span><FileTextOutlined /> 创作记录</span> },
|
||||
{ key: 'hot_opening_replicate', label: <span><StarOutlined /> 爆款复刻</span> },
|
||||
{ key: 'shot_replicate', label: <span><PlayCircleOutlined /> 拆镜复刻</span> },
|
||||
{ key: 'private_portrait', label: <span><UserOutlined /> 私域素材库</span> },
|
||||
{ key: 'upload_resource', label: <span><UploadOutlined /> 历史素材</span> },
|
||||
]}
|
||||
size="middle"
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
{filterType === 'private_portrait' ? (
|
||||
<PrivatePortraitLibraryPanel />
|
||||
) : filterType === 'upload_resource' ? (
|
||||
<UploadResourceHistoryPanel />
|
||||
) : (
|
||||
<>
|
||||
{/* 统一筛选栏:搜索 + 日期 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
@@ -920,338 +948,110 @@ const GeneratedRecord: React.FC = () => {
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
|
||||
<FilterOutlined style={{ color: '#090a0cff', fontSize: 14 }} />
|
||||
<Space size={8} wrap={true}>
|
||||
<Button
|
||||
type={filterType === 'project' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterType('project');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'project'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'project' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'project' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FolderOpenOutlined />}
|
||||
>
|
||||
项目记录
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'creation' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterType('creation');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'creation'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'creation' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'creation' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
创作记录
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'hot_opening_replicate' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterType('hot_opening_replicate');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'hot_opening_replicate'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'hot_opening_replicate' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'hot_opening_replicate' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<StarOutlined />}
|
||||
>
|
||||
爆款复刻
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'shot_replicate' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterType('shot_replicate');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'shot_replicate'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'shot_replicate' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'shot_replicate' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<PlayCircleOutlined />}
|
||||
>
|
||||
拆镜复刻
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'private_portrait' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterType('private_portrait');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'private_portrait'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'private_portrait' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'private_portrait' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<UserOutlined />}
|
||||
>
|
||||
私域素材库
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'upload_resource' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterType('upload_resource');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'upload_resource'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'upload_resource' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'upload_resource' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<UploadOutlined />}
|
||||
>
|
||||
历史素材
|
||||
</Button>
|
||||
</Space>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14, marginRight: 4 }}>媒体类型:</Typography.Text>
|
||||
<Button
|
||||
type={filterMedia === 'video' ? 'primary' : 'default'}
|
||||
onClick={() => { setFilterMedia('video'); setIsSelectionMode(false); setSelectedItems(new Set()); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: filterMedia === 'video' ? 'transparent' : '#e2e8f0', color: filterMedia === 'video' ? '#fff' : '#64748b' }}
|
||||
icon={<VideoCameraOutlined />}
|
||||
>
|
||||
视频
|
||||
</Button>
|
||||
<Button
|
||||
type={filterMedia === 'image' ? 'primary' : 'default'}
|
||||
onClick={() => { setFilterMedia('image'); setIsSelectionMode(false); setSelectedItems(new Set()); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: filterMedia === 'image' ? 'transparent' : '#e2e8f0', color: filterMedia === 'image' ? '#fff' : '#64748b' }}
|
||||
icon={<PictureOutlined />}
|
||||
>
|
||||
图片
|
||||
</Button>
|
||||
</div>
|
||||
{filterType !== 'private_portrait' && filterType !== 'upload_resource' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' }}>
|
||||
{/* 多选模式按钮 */}
|
||||
{isSelectionMode ? (
|
||||
<Space size={8} wrap={true}>
|
||||
<Button
|
||||
onClick={handleSelectAll}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#222222ff',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDeleteSelected()}
|
||||
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #bf0b0a 0%, #ff8165 100%)', fontWeight: 600, padding: '8px 24px', color: '#fff' }}
|
||||
>
|
||||
删除 ({selectedItems.size})
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDownloadSelected()}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
下载 ({selectedItems.size})
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleBatchUploadSelected}
|
||||
loading={uploading}
|
||||
disabled={uploading || selectedItems.size === 0}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
color: '#fff',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{uploading ? '推送中...' : `推送至账户 (${selectedItems.size})`}
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<UploadOutlined />}
|
||||
onClick={() => setIsSelectionMode(true)}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
批量操作
|
||||
</Button>
|
||||
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{filterType === 'private_portrait' ? (
|
||||
<PrivatePortraitLibraryPanel />
|
||||
) : filterType === 'upload_resource' ? (
|
||||
<UploadResourceHistoryPanel />
|
||||
) : (
|
||||
<>
|
||||
{/* Second row filter: 视频 / 图片 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 24,
|
||||
padding: '12px 16px',
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>媒体类型:</Typography.Text>
|
||||
<Space size={8} wrap={true}>
|
||||
<Button
|
||||
type={filterMedia === 'video' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterMedia('video');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterMedia === 'video'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterMedia === 'video' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterMedia === 'video' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<VideoCameraOutlined />}
|
||||
>
|
||||
视频
|
||||
</Button>
|
||||
<Button
|
||||
type={filterMedia === 'image' ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
setFilterMedia('image');
|
||||
setIsSelectionMode(false);
|
||||
setSelectedItems(new Set());
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterMedia === 'image'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterMedia === 'image' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterMedia === 'image' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<PictureOutlined />}
|
||||
>
|
||||
图片
|
||||
</Button>
|
||||
<DatePicker
|
||||
picker="date"
|
||||
value={selectedDate ? dayjs(selectedDate) : undefined}
|
||||
onChange={(date, dateString) => handleDateChange(dateString || '')}
|
||||
format="YYYY-MM-DD"
|
||||
style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }}
|
||||
placeholder="选择日期"
|
||||
/>
|
||||
{selectedDate && (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => handleDateChange('')}
|
||||
style={{ color: '#94a3b8', fontSize: 12 }}
|
||||
>
|
||||
清除
|
||||
</Button>
|
||||
)}
|
||||
<Input.Search
|
||||
placeholder="搜索提示词"
|
||||
allowClear
|
||||
value={searchKeyword}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setSearchKeyword(val);
|
||||
if (!val) {
|
||||
setPagebreak(prev => ({ ...prev, page: 1 }));
|
||||
loadRecordList();
|
||||
}
|
||||
}}
|
||||
onSearch={() => {
|
||||
<Space size={8} wrap>
|
||||
<DatePicker
|
||||
picker="date"
|
||||
value={selectedDate ? dayjs(selectedDate) : undefined}
|
||||
onChange={(date, dateString) => handleDateChange(dateString || '')}
|
||||
format="YYYY-MM-DD"
|
||||
style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }}
|
||||
placeholder="选择日期"
|
||||
allowClear
|
||||
/>
|
||||
<Input.Search
|
||||
placeholder="搜索提示词"
|
||||
allowClear
|
||||
value={searchKeyword}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setSearchKeyword(val);
|
||||
if (!val) {
|
||||
setPagebreak(prev => ({ ...prev, page: 1 }));
|
||||
loadRecordList();
|
||||
}}
|
||||
style={{ width: 240, borderRadius: 8 }}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
<Button
|
||||
icon={<ClockCircleOutlined />}
|
||||
onClick={handleOpenUploadHistory}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
color: '#ffffff',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4)',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4)';
|
||||
}}
|
||||
>
|
||||
查询推送任务历史
|
||||
</Button>
|
||||
}
|
||||
}}
|
||||
onSearch={() => {
|
||||
setPagebreak(prev => ({ ...prev, page: 1 }));
|
||||
loadRecordList();
|
||||
}}
|
||||
style={{ width: 240, borderRadius: 8 }}
|
||||
enterButton
|
||||
/>
|
||||
<Button
|
||||
icon={<ClockCircleOutlined />}
|
||||
onClick={handleOpenUploadHistory}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', color: '#fff' }}
|
||||
>
|
||||
推送任务历史
|
||||
</Button>
|
||||
{/* 批量操作按钮 */}
|
||||
{isSelectionMode ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) && recordlist.length > 0 ? () => { setSelectedItems(new Set()); } : handleSelectAll}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#334155' }}
|
||||
>
|
||||
{selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) && recordlist.length > 0 ? '取消全选' : '全选'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => { setIsSelectionMode(false); setSelectedItems(new Set()); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#64748b' }}
|
||||
>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={handleDeleteSelected}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #ef4444, #dc2626)', border: 'none', color: '#fff' }}
|
||||
>
|
||||
删除 {selectedItems.size > 0 && `(${selectedItems.size})`}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDownloadSelected}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', color: '#fff' }}
|
||||
>
|
||||
下载 {selectedItems.size > 0 && `(${selectedItems.size})`}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleBatchUploadSelected}
|
||||
loading={uploading}
|
||||
disabled={uploading || selectedItems.size === 0}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', color: '#fff' }}
|
||||
>
|
||||
{uploading ? '推送中...' : `推送至账户 ${selectedItems.size > 0 ? `(${selectedItems.size})` : ''}`}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setIsSelectionMode(true)}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
|
||||
>
|
||||
批量操作
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
{/* Content area */}
|
||||
{loading ? (
|
||||
@@ -1263,7 +1063,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
}}>
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 24, color: '#64748b' }} spin />}
|
||||
tip="加载中..."
|
||||
description="加载中..."
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
@@ -1408,18 +1208,20 @@ const GeneratedRecord: React.FC = () => {
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
width: 20,
|
||||
height: 20,
|
||||
top: 6,
|
||||
left: 6,
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: isSelectedItem ? '#10b981' : 'rgba(255,255,255,0.9)',
|
||||
border: isSelectedItem ? '2px solid #10b981' : '2px solid #d1d5db',
|
||||
backgroundColor: isSelectedItem ? '#6366f1' : 'rgba(255,255,255,0.92)',
|
||||
border: isSelectedItem ? '2px solid #6366f1' : '2px solid #cbd5e1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
boxShadow: isSelectedItem ? '0 2px 8px rgba(99,102,241,0.4)' : '0 1px 3px rgba(0,0,0,0.1)',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -1517,10 +1319,11 @@ const GeneratedRecord: React.FC = () => {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
border: '3px solid #10b981',
|
||||
border: '3px solid #6366f1',
|
||||
borderRadius: 4,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 5,
|
||||
boxShadow: 'inset 0 0 0 1px rgba(99,102,241,0.2)',
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -206,7 +206,7 @@ const HomePage: React.FC = () => {
|
||||
setActiveCaseTab(firstId);
|
||||
// 初始请求第一个 tab 的数据
|
||||
getHomeCaseButton(firstId).then((btnRes: any) => {
|
||||
console.log('caseButton:', btnRes);
|
||||
// console.log('caseButton:', btnRes);
|
||||
if (btnRes?.categories?.[0]?.assets) {
|
||||
setCaseAssets(btnRes.categories[0].assets);
|
||||
}
|
||||
@@ -357,7 +357,7 @@ const HomePage: React.FC = () => {
|
||||
{
|
||||
icon: <FileTextOutlined style={{ fontSize: 24 }} />,
|
||||
title: '爆款复刻',
|
||||
description: '上传参考视频与产品图片,一键复刻爆款视频开头',
|
||||
description: '上传参考视频与产品图片,一键复刻爆款',
|
||||
action: '立即创作',
|
||||
path: '/initial',
|
||||
},
|
||||
|
||||
@@ -14,6 +14,13 @@ function clonePlain<T>(value: T): T {
|
||||
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
const buildMediaUrl = (url: string): string => {
|
||||
if (!url) return '';
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, '');
|
||||
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
};
|
||||
|
||||
function InitialInfo() {
|
||||
const navigate = useNavigate();
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
@@ -101,7 +108,7 @@ function InitialInfo() {
|
||||
}, [previewVisible, previewType]);
|
||||
|
||||
const openPreview = (url: string, type: 'image' | 'video') => {
|
||||
console.log(url, type);
|
||||
// console.log(url, type);
|
||||
setPreviewUrl(url);
|
||||
setPreviewType(type);
|
||||
setPreviewVisible(true);
|
||||
@@ -587,7 +594,7 @@ function InitialInfo() {
|
||||
{taskDetail?.material?.materialVideoUrl ? (
|
||||
<video
|
||||
|
||||
src={taskDetail.material.materialVideoUrl}
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
@@ -602,7 +609,7 @@ function InitialInfo() {
|
||||
</span>
|
||||
<div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialImageUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialImageUrl && openPreview(taskDetail.material.materialImageUrl, 'image')}>
|
||||
{taskDetail?.material?.materialImageUrl ? (
|
||||
<img src={taskDetail.material.materialImageUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
<img src={buildMediaUrl(taskDetail.material.materialImageUrl)} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}>暂无图片</div>
|
||||
)}
|
||||
@@ -721,7 +728,7 @@ function InitialInfo() {
|
||||
<div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)' }}>
|
||||
{taskDetail?.material?.materialVideoUrl ? (
|
||||
<video
|
||||
src={taskDetail.material.materialVideoUrl}
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
|
||||
controls
|
||||
/>
|
||||
@@ -741,7 +748,7 @@ function InitialInfo() {
|
||||
<div style={{ height: 180, borderRadius: 10, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{taskDetail?.material?.materialImageUrl ? (
|
||||
<img
|
||||
src={taskDetail.material.materialImageUrl}
|
||||
src={buildMediaUrl(taskDetail.material.materialImageUrl)}
|
||||
alt="产品图片"
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
|
||||
/>
|
||||
|
||||
@@ -383,13 +383,24 @@ const MaterialListPage: React.FC = () => {
|
||||
<FolderOpenOutlined style={{ color: '#6366f1', fontSize: 16 }} />
|
||||
<Typography.Text strong style={{ fontSize: 16 }}>素材列表</Typography.Text>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'top', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
padding: '12px 16px',
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="广告主ID"
|
||||
value={searchParams.advertiser_id}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
|
||||
style={{ width: 160 }}
|
||||
style={{ width: 150, borderRadius: 8 }}
|
||||
allowClear
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
@@ -397,7 +408,7 @@ const MaterialListPage: React.FC = () => {
|
||||
placeholder="素材ID"
|
||||
value={searchParams.material_id}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, material_id: e.target.value }))}
|
||||
style={{ width: 160 }}
|
||||
style={{ width: 150, borderRadius: 8 }}
|
||||
allowClear
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
@@ -405,7 +416,7 @@ const MaterialListPage: React.FC = () => {
|
||||
placeholder="上传ID"
|
||||
value={searchParams.upload_id}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, upload_id: e.target.value }))}
|
||||
style={{ width: 160 }}
|
||||
style={{ width: 150, borderRadius: 8 }}
|
||||
allowClear
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
@@ -413,7 +424,7 @@ const MaterialListPage: React.FC = () => {
|
||||
placeholder="文件名"
|
||||
value={searchParams.file_name}
|
||||
onChange={(e) => setSearchParams(prev => ({ ...prev, file_name: e.target.value }))}
|
||||
style={{ width: 140 }}
|
||||
style={{ width: 140, borderRadius: 8 }}
|
||||
allowClear
|
||||
onPressEnter={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
/>
|
||||
@@ -421,7 +432,7 @@ const MaterialListPage: React.FC = () => {
|
||||
placeholder="资源类型"
|
||||
value={searchParams.resource_type}
|
||||
onChange={(value) => setSearchParams(prev => ({ ...prev, resource_type: value }))}
|
||||
style={{ width: 120 }}
|
||||
style={{ width: 110, borderRadius: 8 }}
|
||||
allowClear
|
||||
options={[
|
||||
{ value: 'image', label: '图片' },
|
||||
@@ -431,21 +442,16 @@ const MaterialListPage: React.FC = () => {
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => { setCurrentPage(1); loadMaterialList(1, pageSize); }}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: 'transparent' }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Button
|
||||
icon={<RobotOutlined />}
|
||||
onClick={() => navigate('/generated?filterType=private_portrait&portraitTab=aigc_virtual')}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
fontSize: 14,
|
||||
borderColor: '#8b5cf6',
|
||||
color: '#7c3aed',
|
||||
background: '#f5f3ff',
|
||||
}}
|
||||
style={{ borderRadius: 8, fontWeight: 600, borderColor: '#8b5cf6', color: '#7c3aed', background: '#f5f3ff' }}
|
||||
>
|
||||
私域虚拟人像库
|
||||
</Button>
|
||||
@@ -454,16 +460,9 @@ const MaterialListPage: React.FC = () => {
|
||||
loading={pushTemplatesLoading}
|
||||
onClick={handleOpenPushModal}
|
||||
disabled={selectedRows.size === 0}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
fontSize: 14,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
boxShadow: '0 4px 14px rgba(99,102,241,0.3)',
|
||||
}}
|
||||
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', color: '#fff' }}
|
||||
>
|
||||
推送前测 ({selectedRows.size})
|
||||
推送前测 {selectedRows.size > 0 && `(${selectedRows.size})`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -128,7 +128,7 @@ const ProjectsPage: React.FC = () => {
|
||||
backgroundClip: 'text',
|
||||
// textAlign: 'center',
|
||||
}}>
|
||||
我的项目
|
||||
行业制造
|
||||
</h2>
|
||||
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
|
||||
共 {projects.length} 个项目 · 按行业分类管理
|
||||
|
||||
@@ -473,7 +473,7 @@ function RemoveInfo() {
|
||||
</>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 12 }}>
|
||||
{record.splitStatus === 'failed' ? '切割失败' : '切割中'}
|
||||
{record.splitStatus === 'failed' ? '切割失败' : record.splitStatus === 'pending' ? '等待切割' : '切割中'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -486,6 +486,14 @@ function RemoveInfo() {
|
||||
render: (_: any, record: any) => {
|
||||
const splitStatus = record.split_status || record.splitStatus;
|
||||
|
||||
if (splitStatus === 'pending') {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{ fontSize: 14, color: '#64748b' }}>等待切割</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (splitStatus === 'processing') {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
|
||||
@@ -14,6 +14,13 @@ function clonePlain<T>(value: T): T {
|
||||
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
const buildMediaUrl = (url: string): string => {
|
||||
if (!url) return '';
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
const base = (import.meta.env.VITE_API_BASE || 'http://localhost:8000').replace(/\/$/, '');
|
||||
return `${base}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
};
|
||||
|
||||
function InitialInfo() {
|
||||
const navigate = useNavigate();
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
@@ -590,7 +597,7 @@ function InitialInfo() {
|
||||
{taskDetail?.material?.materialVideoUrl ? (
|
||||
<video
|
||||
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.material.materialVideoUrl}`}
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
) : (
|
||||
@@ -605,7 +612,7 @@ function InitialInfo() {
|
||||
</span>
|
||||
<div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialImageUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialImageUrl && openPreview(taskDetail.material.materialImageUrl, 'image')}>
|
||||
{taskDetail?.material?.materialImageUrl ? (
|
||||
<img src={taskDetail.material.materialImageUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
<img src={buildMediaUrl(taskDetail.material.materialImageUrl)} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}>暂无图片</div>
|
||||
)}
|
||||
@@ -721,7 +728,7 @@ function InitialInfo() {
|
||||
<div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.06)' }}>
|
||||
{taskDetail?.material?.materialVideoUrl ? (
|
||||
<video
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.material.materialVideoUrl}`}
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
|
||||
controls
|
||||
/>
|
||||
@@ -741,7 +748,7 @@ function InitialInfo() {
|
||||
<div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.06)' }}>
|
||||
{taskDetail?.material?.materialImageUrl ? (
|
||||
<img
|
||||
src={taskDetail.material.materialImageUrl}
|
||||
src={buildMediaUrl(taskDetail.material.materialImageUrl)}
|
||||
alt="产品图片"
|
||||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
|
||||
/>
|
||||
|
||||
@@ -4,4 +4,12 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/uploads': {
|
||||
target: 'http://ceshi.apiforeign.minzhong.cn',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user