Merge remote-tracking branch 'origin/main'

This commit is contained in:
孙佳艺
2026-06-30 09:22:42 +08:00
43 changed files with 2592 additions and 922 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -28,8 +28,8 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-BHOoZIhk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-xCZbcxht.css">
<script type="module" crossorigin src="/assets/index-qlqE3vB0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D3IaMBsq.css">
</head>
<body>
<div id="root"></div>
+2
View File
@@ -19,6 +19,7 @@ import RemoveLens from './pages/RemoveLens';
import GeneratedRecord from './pages/GeneratedRecord';
import PreTest from './pages/PreTest';
import AuthorizationPage from './pages/AuthorizationPage';
import AuthAccountPage from './pages/AuthAccountPage';
import MaterialListPage from './pages/MaterialListPage';
import RemoveInfo from './pages/RemoveInfo';
import RemoveRw from './pages/RemoveRw';
@@ -118,6 +119,7 @@ const App = () => {
<Route path="materials" element={<MaterialListPage />} />
<Route path="consume" element={<ConsumePage />} />
<Route path="popular" element={<PopularPage />} />
<Route path="authacc" element={<AuthAccountPage />} />
<Route path="creativeplaza" element={<CreativePlazaPage />} />
</Route>
<Route path="*" element={<Navigate to="/projects" replace />} />
+3 -3
View File
@@ -174,8 +174,8 @@ export async function verifyCaptcha(captchaId: string, x: number): Promise<strin
return res.token;
}
// ── Site Info ─────────────────────────────────────────────
export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementUrl: string; privacyPolicyUrl: string }> {
if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementUrl: '', privacyPolicyUrl: '' };
export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementPrivacyUrl: string; siteCopyright: string }> {
if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementPrivacyUrl: '', siteCopyright: '© 2024 民众智创 版权所有' };
return api.get('/auth/site-info', false);
}
// ── Video Engines ─────────────────────────────────────────
@@ -743,4 +743,4 @@ export async function deleteOAuthAccount(params: DeleteOAuthAccountParams): Prom
// 获取用户全部授权账户列表
export async function getAllOAuthAccountList(): Promise<any> {
return api.post(`/upload-material/oauth_account_list`);
}
}
@@ -85,8 +85,9 @@
.contact-button-wrapper {
position: fixed;
right: 24px;
bottom: 24px;
bottom: 25%;
z-index: 1000;
cursor: move;
}
.contact-tooltip {
@@ -94,7 +95,7 @@
right: 64px;
bottom: 8px;
padding: 8px 16px;
background: #1e293b;
background: rgba(30, 41, 59, 0.9);
color: #ffffff;
border-radius: 8px;
font-size: 13px;
@@ -108,19 +109,26 @@
height: 40px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8) 0%, rgba(139, 92, 246, 0.8) 100%);
color: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4);
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
transition: all 0.3s ease;
}
.contact-button:hover {
transform: scale(1.05);
box-shadow: 0 6px 24px rgba(99, 102, 241, 0.5);
background: linear-gradient(135deg, rgba(99, 102, 241, 0.95) 0%, rgba(139, 92, 246, 0.95) 100%);
box-shadow: 0 6px 24px rgba(99, 102, 241, 0.4);
}
.contact-button.dragging {
cursor: grabbing;
transform: scale(1.1);
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.5);
}
@media (max-width: 767px) {
@@ -272,12 +272,23 @@ const AppLayout: React.FC = () => {
const { user, logout } = useAuthStore();
const [pwdModalOpen, setPwdModalOpen] = useState(false);
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [pwdForm] = Form.useForm();
const [selectedPlan, setSelectedPlan] = useState<number | null>(null);
const [menuItems, setMenuItems] = useState<MenuConfig[]>([]);
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [contactHovered, setContactHovered] = useState(false);
const [contactForm] = Form.useForm();
const [submittingContact, setSubmittingContact] = useState(false);
const [contactPosition, setContactPosition] = useState<{ x: number; y: number }>(() => ({
x: 24,
y: window.innerHeight * 0.75
}));
const [isDragging, setIsDragging] = useState(false);
const hasMovedRef = useRef(false);
const dragStartRef = useRef({ x: 0, y: 0 });
const [mobileExpandedMenus, setMobileExpandedMenus] = useState<Record<string, boolean>>({});
const [siteName, setSiteName] = useState(() => {
const cached = localStorage.getItem('siteInfo');
@@ -310,10 +321,6 @@ const AppLayout: React.FC = () => {
const countdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const currentOrderNoRef = useRef<string | null>(null);
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
const [contactModalOpen, setContactModalOpen] = useState(false);
const [contactForm] = Form.useForm();
const [contactHovered, setContactHovered] = useState(false);
const [submittingContact, setSubmittingContact] = useState(false);
// 资源存储容量(从 getUser().resource_capacity 获取)
const [resourceCapacity, setResourceCapacity] = useState<{
@@ -355,30 +362,52 @@ const AppLayout: React.FC = () => {
}).catch(() => { });
}, []);
// 拉取用户资源容量信息
useEffect(() => {
getUser().then((res: any) => {
console.log('[Storage] getUser 返回:', res);
const handleContactMouseDown = (e: React.MouseEvent) => {
if (e.button === 0) {
setIsDragging(true);
hasMovedRef.current = false;
dragStartRef.current = {
x: e.clientX,
y: e.clientY,
};
}
};
const rc = res?.resourceCapacity;
if (rc) {
setResourceCapacity({
enabled: !!rc.enabled,
usedBytes: Number(rc.usedBytes) || 0,
totalBytes: Number(rc.totalBytes) || 0,
availableBytes: Number(rc.availableBytes) || 0,
usagePercent: Number(rc.usagePercent) || 0,
exceeded: !!rc.exceeded,
limitValue: rc.limitValue ?? '',
limitUnit: rc.limitUnit || 'GB',
});
}
// 没数据时不显示
}).catch((err: any) => {
console.error('[Storage] getUser 失败:', err);
// 接口失败也不显示
});
}, []);
const handleContactMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const deltaX = Math.abs(e.clientX - dragStartRef.current.x);
const deltaY = Math.abs(e.clientY - dragStartRef.current.y);
if (deltaX > 5 || deltaY > 5) {
hasMovedRef.current = true;
}
const newY = Math.max(60, Math.min(window.innerHeight - 60, e.clientY - (dragStartRef.current.y - contactPosition.y)));
setContactPosition(prev => ({ x: prev.x, y: newY }));
};
const handleContactMouseUp = () => {
const moved = hasMovedRef.current;
setIsDragging(false);
hasMovedRef.current = false;
if (!moved) {
setContactModalOpen(true);
}
};
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleContactMouseMove);
document.addEventListener('mouseup', handleContactMouseUp);
return () => {
document.removeEventListener('mousemove', handleContactMouseMove);
document.removeEventListener('mouseup', handleContactMouseUp);
};
}
}, [isDragging]);
const loadUnreadCount = () => {
getUnreadCount().then(count => {
@@ -693,7 +722,7 @@ const AppLayout: React.FC = () => {
}}>
<div style={{
width: 42, height: 42, borderRadius: 14, flexShrink: 0,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)',
//background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a78bfa 100%)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.35)',
overflow: 'hidden',
@@ -777,12 +806,12 @@ const AppLayout: React.FC = () => {
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
}} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: '#1e293b', fontSize: 14, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
<div style={{ color: '#1e293b', fontSize: 16, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username}
</div>
<div style={{
color: '#6366f1',
fontSize: 12,
fontSize: 16,
fontWeight: 500,
letterSpacing: 0,
background: 'rgba(99, 102, 241, 0.08)',
@@ -1096,8 +1125,16 @@ const AppLayout: React.FC = () => {
gap: 8,
}}>
<InfoOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<Typography.Text style={{ color: '#ff0000ff', fontSize: 13 }}>
/
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}>
/
<Typography.Text
style={{
color: '#ff0000ff',
cursor: 'pointer',
textDecoration: 'underline',
}}
onClick={() => { setRechargeModalOpen(false); setContactModalOpen(true); }}
></Typography.Text>
</Typography.Text>
</div>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
@@ -1372,7 +1409,13 @@ const AppLayout: React.FC = () => {
<NotificationPopup />
<div className="contact-button-wrapper">
<div
className="contact-button-wrapper"
style={{
right: `${contactPosition.x}px`,
bottom: `${window.innerHeight - contactPosition.y}px`,
}}
>
<div style={{
position: 'relative',
}}>
@@ -1385,10 +1428,11 @@ const AppLayout: React.FC = () => {
</div>
<button
onClick={() => setContactModalOpen(true)}
className="contact-button"
className={`contact-button ${isDragging ? 'dragging' : ''}`}
onMouseEnter={() => setContactHovered(true)}
onMouseLeave={() => setContactHovered(false)}
onMouseDown={handleContactMouseDown}
onMouseUp={handleContactMouseUp}
>
<MessageOutlined style={{ fontSize: 20 }} />
</button>
+302
View File
@@ -0,0 +1,302 @@
import React, { useEffect, useState } from 'react';
import { Button, Table, Modal, App, Input, Pagination, Typography, Space } from 'antd';
import { LockOutlined } from '@ant-design/icons';
import { getOAuthAccountList, deleteOAuthAccount, getOpenTypeAll } from '../api';
const formatDateTime = (dateStr: string) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
interface AuthorizationData {
id: string;
status: string;
description: string;
account_userid?: string;
open_type?: number;
account_id?: string;
}
const AuthAccountPage: React.FC = () => {
const { message } = App.useApp();
const [authorizations, setAuthorizations] = useState<AuthorizationData[]>([]);
const [listLoading, setListLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [searchParams, setSearchParams] = useState({
advertiser_id: '',
oauth_id: '',
advertiser_name: '',
});
const [openTypeMap, setOpenTypeMap] = useState<Record<number, string>>({});
useEffect(() => {
loadOAuthList();
loadOpenTypeList();
}, []);
const loadOpenTypeList = async () => {
try {
const res = await getOpenTypeAll();
const data = res.data || [];
const map: Record<number, string> = {};
const options: { value: number; label: string }[] = [];
data.forEach(item => {
map[item.openType] = item.typeName;
options.push({ value: item.openType, label: item.typeName });
});
setOpenTypeMap(map);
} catch (error) {
console.error('加载开户方式列表失败:', error);
}
};
const loadOAuthList = async (page = 1, pageSize = 10, params = searchParams) => {
setListLoading(true);
try {
const response = await getOAuthAccountList({
advertiser_id: params.advertiser_id || '',
oauth_id: params.oauth_id || '',
advertiser_name: params.advertiser_name || '',
page,
page_size: pageSize,
});
if (response) {
if (response.data) {
setAuthorizations(response.data.data || response.data);
}
if (response.pagination) {
setTotal(response.pagination.total || 0);
setCurrentPage(response.pagination.page || 1);
setPageSize(response.pagination.pageSize || 10);
}
} else {
setAuthorizations(response || []);
}
} catch (error) {
message.error('获取授权列表失败');
} finally {
setListLoading(false);
}
};
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '广告主账户ID',
dataIndex: 'advertiserId',
key: 'advertiserId',
width: 160,
},
{
title: '广告主账户名称',
dataIndex: 'advertiserName',
key: 'advertiserName',
},
{
title: '广告主账户角色',
dataIndex: 'advertiserRole',
key: 'advertiserRole',
},
{
title: '授权账户ID',
dataIndex: 'accountId',
key: 'accountId',
width: 160,
},
{
title: '授权账户名称',
dataIndex: 'accountName',
key: 'accountName',
},
{
title: '授权账户角色',
dataIndex: 'accountRole',
key: 'accountRole',
render: (role: string) => {
const roleMap: Record<string, string> = {
ADVERTISER: '客户',
CUSTOMER_ADMIN: '普通版工作台-管理员',
CUSTOMER_OPERATOR: '普通版工作台-协作者',
AGENT: '代理商',
CHILD_AGENT: '二级代理商',
PLATFORM_ROLE_STAR: '星图账户',
PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户',
PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商',
PLATFORM_ROLE_STAR_AGENT: '星图代理商',
PLATFORM_ROLE_AWEME: '抖音号',
PLATFORM_ROLE_STAR_MCN: '星图MCN机构',
PLATFORM_ROLE_STAR_ISV: '星图服务商',
AGENT_SYSTEM_ACCOUNT: '代理商系统账户',
PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商',
PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员',
PLATFORM_ROLE_LIFE: '抖音来客账户',
PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员',
PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者',
};
return roleMap[role] || role;
},
},
{
title: '授权账户登录账号ID',
dataIndex: 'accountUserid',
key: 'accountUserid',
},
{
title: '授权账户登录账号名称',
dataIndex: 'accountUsername',
key: 'accountUsername',
},
{
title: '授权ID',
dataIndex: 'oauthId',
key: 'oauthId',
},
{
title: '开户方式',
dataIndex: 'openType',
key: 'openType',
render: (text: number) => <span style={{ color: '#1e293b' }}>{openTypeMap[text] || text}</span>,
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
},
{
title: '操作',
fixed: 'right' as const,
dataIndex: 'action',
key: 'action',
render: (_: string, record: AuthorizationData) => (
<Space>
<Button
size="small"
danger
onClick={() => {
Modal.confirm({
title: '确认删除',
content: '确定要删除该授权账户吗?',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
await deleteOAuthAccount({ id: record.id });
message.success('删除成功');
loadOAuthList(currentPage, pageSize);
} catch (error) {
message.error('删除失败');
}
},
});
}}
>
</Button>
</Space>
),
},
];
const tableData = authorizations.map((item, index) => ({
...item,
index: index + 1,
key: item.id,
}));
return (
<div style={{ minHeight: '94vh' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<LockOutlined style={{ color: '#6366f1', fontSize: 16 }} />
<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 }}>
<Input
placeholder="广告主账户ID"
value={searchParams.advertiser_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_id: e.target.value }))}
style={{ width: 180 }}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Input
placeholder="授权ID"
value={searchParams.oauth_id}
onChange={(e) => setSearchParams(prev => ({ ...prev, oauth_id: e.target.value }))}
style={{ width: 180 }}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Input
placeholder="广告主账户名称"
value={searchParams.advertiser_name}
onChange={(e) => setSearchParams(prev => ({ ...prev, advertiser_name: e.target.value }))}
style={{ width: 180 }}
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
/>
<Button
type="primary"
size="medium"
onClick={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
>
</Button>
<Button
size="medium"
onClick={() => {
setSearchParams({ advertiser_id: '', oauth_id: '', advertiser_name: '' });
setCurrentPage(1);
loadOAuthList(1, pageSize);
}}
>
</Button>
</div>
</div>
<div style={{ background: '#fff', borderRadius: 12, boxShadow: '0 1px 3px rgba(0,0,0,0.05)' }}>
<Table
dataSource={tableData}
columns={columns}
loading={listLoading}
pagination={false}
rowKey="id"
bordered={false}
scroll={{ x: 'max-content' }}
/>
<div style={{ padding: '16px', textAlign: 'right' }}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
showSizeChanger
showTotal={(total) => `${total} 条记录`}
onChange={(page, size) => {
setCurrentPage(page);
setPageSize(size);
loadOAuthList(page, size);
}}
size="small"
/>
</div>
</div>
</div>
);
};
export default AuthAccountPage;
+23 -9
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Button, Table, Tag, Modal, Select, App, Input, Pagination, Typography, Space } from 'antd';
import { useNavigate } from 'react-router-dom';
import { PlusOutlined, LockOutlined } from '@ant-design/icons';
import { getOAuthList, requestOAuth, getOpenTypeAll } from '../api';
@@ -41,13 +42,23 @@ interface AuthorizationData {
id: string;
status: string;
description: string;
account_userid?: string;
open_type?: number;
account_id?: string;
advertiserId?: string;
advertiserName?: string;
accountRole?: string;
accountUserid?: string;
accountUsername?: string;
appid?: string;
materialAuthStatus?: boolean;
openType?: number;
portType?: number;
userId?: string;
createdAt?: string;
updatedAt?: string;
}
const AuthorizationPage: React.FC = () => {
const { message } = App.useApp();
const navigate = useNavigate();
const [authorizations, setAuthorizations] = useState<AuthorizationData[]>([]);
const [loading, setLoading] = useState(false);
const [listLoading, setListLoading] = useState(false);
@@ -250,14 +261,17 @@ const AuthorizationPage: React.FC = () => {
{
title: '操作',
dataIndex: 'action',
fixed: 'right' as const,
key: 'action',
width: 140,
render: (text: string) => (
<Space>
<Button type="primary" size="small" >
</Button>
</Space>
render: (_: unknown, record) => (
<Button
type="link"
onClick={() => navigate(`/consume?advertiserId=${record.advertiserId}`)}
style={{ color: '#6366f1', padding: 0 }}
>
</Button>
),
},
];
+7 -2
View File
@@ -31,7 +31,7 @@ const ConsumePage: React.FC = () => {
const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [advertiserId, setAdvertiserId] = useState(searchParams.get('accountId') || '');
const [advertiserId, setAdvertiserId] = useState(searchParams.get('advertiserId') || '');
const [consumeDateRange, setConsumeDateRange] = useState<[string, string] | undefined>();
const [syncDate, setSyncDate] = useState<string>(dayjs().subtract(1, 'day').format('YYYY-MM-DD'));
const [syncAdvertiserId, setSyncAdvertiserId] = useState<string>('');
@@ -88,7 +88,12 @@ const ConsumePage: React.FC = () => {
}));
const handleBack = () => {
navigate('/authorization');
const referrer = document.referrer;
if (referrer.includes(window.location.origin)) {
navigate(-1);
} else {
navigate('/authorization');
}
};
const handleSearch = () => {
+10 -5
View File
@@ -43,6 +43,7 @@ import {
WarningOutlined,
SettingOutlined,
LayoutOutlined,
ArrowUpOutlined,
} from '@ant-design/icons';
@@ -1558,17 +1559,21 @@ const AIChatPage: React.FC = () => {
<Button
type="primary"
shape="circle"
icon={<SendOutlined />}
icon={<ArrowUpOutlined />}
onClick={handleSend}
disabled={!inputValue.trim() && currentMedia.length === 0}
loading={loading}
style={{
flexShrink: 0,
width: 48,
height: 48,
width: 40,
height: 40,
borderRadius: 14,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.4)',
background: (inputValue.trim() || currentMedia.length > 0)
? 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)'
: '#c7cfdaff',
boxShadow: (inputValue.trim() || currentMedia.length > 0)
? '0 4px 16px rgba(99, 102, 241, 0.4)'
: 'none',
transition: 'all 0.2s ease',
border: 'none',
}}
+24 -24
View File
@@ -37,7 +37,7 @@ const GeneratedRecord: React.FC = () => {
const [uploading, setUploading] = useState(false);
// 上传配置弹窗相关状态
// 推送配置弹窗相关状态
const [uploadConfigModalVisible, setUploadConfigModalVisible] = useState(false);
const [accountIdLists, setAccountIdLists] = useState<{
accountId: string;
@@ -68,7 +68,7 @@ const GeneratedRecord: React.FC = () => {
const [selectedHistoryAccounts, setSelectedHistoryAccounts] = useState<any[]>([]);
const [openTypeMap, setOpenTypeMap] = useState<Record<number, string>>({});
// 上传任务历史弹窗相关状态
// 推送任务历史弹窗相关状态
const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false);
const [uploadHistoryList, setUploadHistoryList] = useState<any[]>([]);
const [uploadHistoryTotal, setUploadHistoryTotal] = useState(0);
@@ -671,7 +671,7 @@ const GeneratedRecord: React.FC = () => {
};
const handleBatchUploadSelected = () => {
if (selectedItems.size === 0) {
message.warning('请先选择要上传的媒体');
message.warning('请先选择要推送的媒体');
return;
}
setAccountIdLists([[]]);
@@ -741,7 +741,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 {
@@ -763,10 +763,10 @@ const GeneratedRecord: React.FC = () => {
setUploadHistoryPageSize(pageSize);
loadUploadHistory();
};
// 批量上传素材
// 批量推送素材
const handleStartBatchUpload = async () => {
if (selectedItems.size === 0) {
message.warning('请先选择要上传的媒体');
message.warning('请先选择要推送的媒体');
return;
}
const itemMap = new Map<string, any>();
@@ -869,8 +869,8 @@ const GeneratedRecord: React.FC = () => {
setMaterialFileNames(new Map());
setUnifiedFileName('');
} catch (error: any) {
console.error('批量上传失败:', error);
message.error(error.message || '批量上传失败');
console.error('批量推送失败:', error);
message.error(error.message || '批量推送失败');
} finally {
setUploading(false);
}
@@ -1056,7 +1056,7 @@ const GeneratedRecord: React.FC = () => {
}, [filterType, filterMedia]);
return (
<div style={{ minHeight: 'calc(100vh - 90px)', background: '#ffffffff', overflowY: 'auto' }} >
{/* 操作栏:筛选 + 上传按钮 */}
{/* 操作栏:筛选 + 推送按钮 */}
<div style={{
display: 'flex',
alignItems: 'center',
@@ -1167,7 +1167,7 @@ const GeneratedRecord: React.FC = () => {
fontWeight: 600,
}}
>
{uploading ? '上传中...' : `推送至账户 (${selectedItems.size})`}
{uploading ? '推送中...' : `推送至账户 (${selectedItems.size})`}
</Button>
</Space>
) : (
@@ -1284,7 +1284,7 @@ const GeneratedRecord: React.FC = () => {
e.currentTarget.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4)';
}}
>
</Button>
</div>
{/* Content area */}
@@ -1369,9 +1369,9 @@ const GeneratedRecord: React.FC = () => {
)}
</div>
)}
{/* 上传配置弹窗 */}
{/* 推送配置弹窗 */}
<Modal
title={selectedItems.size === 1 ? '上传配置' : '批量上传配置'}
title={selectedItems.size === 1 ? '推送配置' : '批量推送配置'}
open={uploadConfigModalVisible}
onCancel={() => {
setUploadConfigModalVisible(false);
@@ -1845,15 +1845,15 @@ const GeneratedRecord: React.FC = () => {
disabled={uploading || (accountTab === 'new' ? accountIdLists.every(list => list.length === 0) : selectedHistoryAccounts.length === 0)}
style={{ borderRadius: 8 }}
>
{uploading ? '上传中...' : '开始上传'}
{uploading ? '推送中...' : '开始推送'}
</Button>
</div>
</div>
</Modal>
{/* 上传任务历史弹窗 */}
{/* 推送任务历史弹窗 */}
<Modal
title="上传任务历史"
title="推送任务历史"
open={uploadHistoryModalVisible}
onCancel={() => setUploadHistoryModalVisible(false)}
footer={null}
@@ -1868,10 +1868,10 @@ const GeneratedRecord: React.FC = () => {
placeholder="选择状态"
style={{ width: 200, marginRight: 12 }}
options={[
{ value: '1', label: '待上传' },
{ value: '2', label: '上传中' },
{ value: '3', label: '上传成功' },
{ value: '4', label: '上传失败' },
{ value: '1', label: '待推送' },
{ value: '2', label: '推送中' },
{ value: '3', label: '推送成功' },
{ value: '4', label: '推送失败' },
]}
allowClear
/>
@@ -1905,10 +1905,10 @@ const GeneratedRecord: React.FC = () => {
width: 100,
render: (status: string) => {
const statusMap: Record<string, string> = {
'1': '待上传',
'2': '上传中',
'3': '上传成功',
'4': '上传失败',
'1': '待推送',
'2': '推送中',
'3': '推送成功',
'4': '推送失败',
};
const statusColorMap: Record<string, string> = {
'1': '#f59e0b',
+29 -3
View File
@@ -1,5 +1,6 @@
.login-page {
min-height: 100vh;
height: 100vh;
max-height: 100vh;
display: flex;
flex-direction: column;
background-image: url(/backimage.png);
@@ -8,7 +9,7 @@
background-repeat: no-repeat;
background-attachment: fixed;
position: relative;
overflow-x: hidden;
overflow: hidden;
}
@media (min-width: 900px) {
@@ -180,10 +181,35 @@
.login-right-section {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1;
padding: 16px 16px 32px;
padding: 16px 16px 60px;
position: relative;
}
.login-right-section-inner {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 440px;
}
.login-copyright-wrapper {
position: absolute;
bottom: 24px;
left: 0;
right: 0;
text-align: center;
}
.login-copyright {
color: #666;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
}
@media (min-width: 900px) {
+191 -179
View File
@@ -44,8 +44,8 @@ const LoginPage: React.FC = () => {
const initialInfo = getInitialSiteInfo();
const [siteName, setSiteName] = useState(initialInfo.siteName);
const [siteLogo, setSiteLogo] = useState(initialInfo.siteLogo);
const [agreementUrl, setAgreementUrl] = useState('');
const [policyUrl, setPolicyUrl] = useState('');
const [agreementPrivacyUrl, setAgreementPrivacyUrl] = useState('');
const [siteCopyright, setSiteCopyright] = useState('');
const navigate = useNavigate();
const { login } = useAuthStore();
@@ -57,14 +57,14 @@ const LoginPage: React.FC = () => {
getSiteInfo().then(info => {
setSiteName(info.siteName);
setSiteLogo(info.siteLogo);
setAgreementUrl(info.userAgreementUrl);
setPolicyUrl(info.privacyPolicyUrl);
setAgreementPrivacyUrl(info.userAgreementPrivacyUrl);
setSiteCopyright(info.siteCopyright);
}).catch(() => {});
}, []);
const checkAgreed = (): boolean => {
if (!agreed) {
message.warning('请先阅读并同意用户协议隐私政策');
message.warning('请先阅读并同意用户协议隐私政策');
return false;
}
return true;
@@ -259,7 +259,15 @@ const LoginPage: React.FC = () => {
};
const openPdf = (url: string) => {
if (url) window.open(`${API_BASE.replace(/\/api$/, '')}${url}`, '_blank');
if (!url) {
message.warning('暂未上传协议文件');
return;
}
if (url.startsWith('http://') || url.startsWith('https://')) {
window.open(url, '_blank');
} else {
window.open(`${API_BASE.replace(/\/api$/, '')}${url}`, '_blank');
}
};
return (
@@ -310,182 +318,186 @@ const LoginPage: React.FC = () => {
</div>
<div className="login-right-section">
<Card className="login-card" styles={{ body: { padding: '36px 28px' } }}>
<Typography.Title level={3} className="login-card-title">
{mode === 'register' ? '创建账号' : '欢迎回来'}
</Typography.Title>
<Typography.Text className="login-card-subtitle">
{mode === 'register' ? '注册新账号,开始创作视频' : '登录您的账号,开始创作视频'}
</Typography.Text>
<div className="login-right-section-inner">
<Card className="login-card" styles={{ body: { padding: '36px 28px' } }}>
<Typography.Title level={3} className="login-card-title">
{mode === 'register' ? '创建账号' : '欢迎回来'}
</Typography.Title>
<Typography.Text className="login-card-subtitle">
{mode === 'register' ? '注册新账号,开始创作视频' : '登录您的账号,开始创作视频'}
</Typography.Text>
{mode !== 'register' && (
<div className="login-tabs">
{(['password', 'phone'] as const).map((t) => (
<div key={t} onClick={() => switchTab(t)}
className={`login-tab ${tab === t ? 'login-tab-active' : ''}`}>
{t === 'password' ? '密码登录' : '验证码登录'}
</div>
))}
</div>
)}
{mode === 'password' && (
<Form form={pwdForm} size="large" layout="vertical" onFinish={handlePasswordLogin}>
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" style={inputStyle} />
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入密码" style={inputStyle} />
</Form.Item>
<Form.Item name="rememberMe" valuePropName="checked" style={{ marginBottom: 12 }}>
<Checkbox></Checkbox>
</Form.Item>
<Form.Item style={{ marginBottom: 12 }}>
<Button type="primary" htmlType="submit" loading={loading} block className="login-submit-btn"> </Button>
</Form.Item>
</Form>
)}
{mode === 'phone' && (
<Form form={phoneForm} size="large" layout="vertical">
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
</Form.Item>
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />}
placeholder="请输入验证码"
maxLength={6}
disabled={!loginSliderVerified}
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
<Button disabled={countdown > 0}
onClick={() => {
if (loginShowResend) {
setLoginSliderVerified(false);
setShowSliderVerify(true);
setSliderKey(prev => prev + 1);
} else {
handleSendCode(phoneForm.getFieldValue('phone'));
}
}}
className="login-code-btn">
{countdown > 0 ? `${countdown}s` : (loginShowResend ? '重新发送' : '获取验证码')}
</Button>
</Space.Compact>
</Form.Item>
{showSliderVerify && (
<Form.Item style={{ marginBottom: 12 }} key={sliderKey}>
<SliderVerify
onSuccess={handleSliderSuccess}
isVerified={loginSliderVerified}
/>
</Form.Item>
)}
<Form.Item style={{ marginBottom: 12 }}>
<Button type="primary" onClick={handlePhoneLogin} loading={loading} block className="login-submit-btn"> </Button>
</Form.Item>
</Form>
)}
{mode === 'register' && (
<Form form={regForm} size="large" layout="vertical">
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
</Form.Item>
<Form.Item name="regCode" rules={[{ required: true, message: '请输入验证码' }]}>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />}
placeholder="请输入验证码"
maxLength={6}
disabled={!sliderVerified}
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
<Button disabled={regCountdown > 0}
onClick={() => {
if (showResend) {
setSliderVerified(false);
setShowSliderVerify(true);
setSliderKey(prev => prev + 1);
} else {
handleSendCode(regForm.getFieldValue('phone'), true);
}
}}
className="login-code-btn">
{regCountdown > 0 ? `${regCountdown}s` : (showResend ? '重新发送' : '获取验证码')}
</Button>
</Space.Compact>
</Form.Item>
{showSliderVerify && (
<Form.Item style={{ marginBottom: 12 }} key={sliderKey}>
<SliderVerify
onSuccess={handleSliderSuccess}
isVerified={sliderVerified}
/>
</Form.Item>
)}
<Form.Item name="password" rules={[{ required: true, message: '请设置密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请设置密码(至少6位)" style={inputStyle} />
</Form.Item>
<Form.Item style={{ marginBottom: 12 }}>
<Button type="primary" onClick={handleRegister} loading={loading} block className="login-submit-btn"> </Button>
</Form.Item>
</Form>
)}
<div className="login-agreement">
<Checkbox checked={agreed} onChange={e => setAgreed(e.target.checked)}>
<span className="login-agreement-text">
<span
onClick={e => { e.stopPropagation(); openPdf(agreementUrl); }}
className="login-link"
></span>
<span
onClick={e => { e.stopPropagation(); openPdf(policyUrl); }}
className="login-link"
></span>
</span>
</Checkbox>
</div>
<div className="login-footer">
{mode === 'register' ? (
<Typography.Text
className="login-switch-btn"
onClick={() => {
setMode('password');
setTab('password');
setShowResend(false);
setLoginShowResend(false);
setSliderVerified(false);
setShowSliderVerify(false);
setSliderKey(prev => prev + 1);
regForm.resetFields();
}}
>
</Typography.Text>
) : (
<Typography.Text
className="login-switch-btn"
onClick={() => {
setMode('register');
setShowResend(false);
setLoginShowResend(false);
setSliderVerified(false);
setShowSliderVerify(false);
setSliderKey(prev => prev + 1);
}}
>
</Typography.Text>
{mode !== 'register' && (
<div className="login-tabs">
{(['password', 'phone'] as const).map((t) => (
<div key={t} onClick={() => switchTab(t)}
className={`login-tab ${tab === t ? 'login-tab-active' : ''}`}>
{t === 'password' ? '密码登录' : '验证码登录'}
</div>
))}
</div>
)}
{mode === 'password' && (
<Form form={pwdForm} size="large" layout="vertical" onFinish={handlePasswordLogin}>
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" style={inputStyle} />
</Form.Item>
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入密码" style={inputStyle} />
</Form.Item>
<Form.Item name="rememberMe" valuePropName="checked" style={{ marginBottom: 12 }}>
<Checkbox></Checkbox>
</Form.Item>
<Form.Item style={{ marginBottom: 12 }}>
<Button type="primary" htmlType="submit" loading={loading} block className="login-submit-btn"> </Button>
</Form.Item>
</Form>
)}
{mode === 'phone' && (
<Form form={phoneForm} size="large" layout="vertical">
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
</Form.Item>
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />}
placeholder="请输入验证码"
maxLength={6}
disabled={!loginSliderVerified}
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
<Button disabled={countdown > 0}
onClick={() => {
if (loginShowResend) {
setLoginSliderVerified(false);
setShowSliderVerify(true);
setSliderKey(prev => prev + 1);
} else {
handleSendCode(phoneForm.getFieldValue('phone'));
}
}}
className="login-code-btn">
{countdown > 0 ? `${countdown}s` : (loginShowResend ? '重新发送' : '获取验证码')}
</Button>
</Space.Compact>
</Form.Item>
{showSliderVerify && (
<Form.Item style={{ marginBottom: 12 }} key={sliderKey}>
<SliderVerify
onSuccess={handleSliderSuccess}
isVerified={loginSliderVerified}
/>
</Form.Item>
)}
<Form.Item style={{ marginBottom: 12 }}>
<Button type="primary" onClick={handlePhoneLogin} loading={loading} block className="login-submit-btn"> </Button>
</Form.Item>
</Form>
)}
{mode === 'register' && (
<Form form={regForm} size="large" layout="vertical">
<Form.Item name="phone" rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1\d{10}$/, message: '请输入正确的手机号' }]}>
<Input prefix={<MobileOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请输入手机号" maxLength={11} style={inputStyle} />
</Form.Item>
<Form.Item name="regCode" rules={[{ required: true, message: '请输入验证码' }]}>
<Space.Compact style={{ width: '100%' }}>
<Input
prefix={<SafetyOutlined style={{ color: '#94a3b8', marginRight: 8 }} />}
placeholder="请输入验证码"
maxLength={6}
disabled={!sliderVerified}
style={{ ...inputStyle, borderRadius: '10px 0 0 10px', flex: 1 }} />
<Button disabled={regCountdown > 0}
onClick={() => {
if (showResend) {
setSliderVerified(false);
setShowSliderVerify(true);
setSliderKey(prev => prev + 1);
} else {
handleSendCode(regForm.getFieldValue('phone'), true);
}
}}
className="login-code-btn">
{regCountdown > 0 ? `${regCountdown}s` : (showResend ? '重新发送' : '获取验证码')}
</Button>
</Space.Compact>
</Form.Item>
{showSliderVerify && (
<Form.Item style={{ marginBottom: 12 }} key={sliderKey}>
<SliderVerify
onSuccess={handleSliderSuccess}
isVerified={sliderVerified}
/>
</Form.Item>
)}
<Form.Item name="password" rules={[{ required: true, message: '请设置密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password prefix={<LockOutlined style={{ color: '#94a3b8', marginRight: 8 }} />} placeholder="请设置密码(至少6位)" style={inputStyle} />
</Form.Item>
<Form.Item style={{ marginBottom: 12 }}>
<Button type="primary" onClick={handleRegister} loading={loading} block className="login-submit-btn"> </Button>
</Form.Item>
</Form>
)}
<div className="login-agreement">
<Checkbox checked={agreed} onChange={e => setAgreed(e.target.checked)}>
<span className="login-agreement-text">
<span
onClick={e => { e.stopPropagation(); openPdf(agreementPrivacyUrl); }}
className="login-link"
></span>
</span>
</Checkbox>
</div>
<div className="login-footer">
{mode === 'register' ? (
<Typography.Text
className="login-switch-btn"
onClick={() => {
setMode('password');
setTab('password');
setShowResend(false);
setLoginShowResend(false);
setSliderVerified(false);
setShowSliderVerify(false);
setSliderKey(prev => prev + 1);
regForm.resetFields();
}}
>
</Typography.Text>
) : (
<Typography.Text
className="login-switch-btn"
onClick={() => {
setMode('register');
setShowResend(false);
setLoginShowResend(false);
setSliderVerified(false);
setShowSliderVerify(false);
setSliderKey(prev => prev + 1);
}}
>
</Typography.Text>
)}
</div>
</Card>
</div>
{siteCopyright && (
<div className="login-copyright-wrapper">
<div className="login-copyright">
{siteCopyright}
</div>
</div>
</Card>
)}
</div>
</div>
);
+9 -4
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Button, Table, Tag, Input, Pagination, Typography, Select, App } from 'antd';
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { FolderOpenOutlined, EyeOutlined } from '@ant-design/icons';
import { getResourcesMaterialList } from '../api';
import PreResultDisplay from '../components/PreResultDisplay';
@@ -81,6 +81,7 @@ interface MaterialData {
const MaterialListPage: React.FC = () => {
const { message } = App.useApp();
const navigate = useNavigate();
const [materials, setMaterials] = useState<MaterialData[]>([]);
const [listLoading, setListLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
@@ -269,12 +270,16 @@ const MaterialListPage: React.FC = () => {
},
{
title: '操作',
fixed: 'right' as const,
key: 'action',
width: 120,
render: (_: unknown, record) => (
<Link to={`/consume?accountId=${record.advertiserId}`} style={{ color: '#6366f1' }}>
<Button
type="link"
onClick={() => navigate(`/consume?advertiserId=${record.advertiserId}`)}
style={{ color: '#6366f1', padding: 0 }}
>
</Link>
</Button>
),
},
// {