diff --git a/video-gen-admin/src/App.tsx b/video-gen-admin/src/App.tsx index 59222f02..e91dde54 100644 --- a/video-gen-admin/src/App.tsx +++ b/video-gen-admin/src/App.tsx @@ -45,6 +45,8 @@ import AdminApiKeys from './pages/AdminApiKeys'; import AdminApiModelPricings from './pages/AdminApiModelPricings'; import AdminApiUsage from './pages/AdminApiUsage'; import AdminInvoices from './pages/AdminInvoices'; +import AdminBankTransactions from './pages/AdminBankTransactions'; +import AdminScheduledTasks from './pages/AdminScheduledTasks'; import { useAdminStore } from './store'; @@ -112,6 +114,8 @@ const App = () => { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index 269f89d7..0a764ade 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -18,6 +18,7 @@ import type { PrivatePortraitConfig, PrivatePortraitProjectListOut, PrivatePortraitAssetListOut, AdminUploadFileResult, AdminUploadResourceType, AdminUploadScene, VideoUpscaleConfigOut, VideoUpscaleConfigSavePayload, CreditProduct, + BankAccount, ScheduledTask, } from '../types'; import type { @@ -216,6 +217,102 @@ export async function toggleUserStatus(userId: string, isActive: boolean): Promi await api.put(`/admin/users/${userId}/status`, { is_active: isActive }); } +export async function queryBankTransactions(params: { + accountId: string; + startDate: string; + endDate: string; + dcFlag?: number; + page?: number; + pageSize?: number; +}): Promise<{ items: any[]; total: number }> { + const qs = new URLSearchParams(); + qs.set('account_id', params.accountId); + qs.set('start_date', params.startDate); + qs.set('end_date', params.endDate); + if (params.dcFlag !== undefined) qs.set('dc_flag', String(params.dcFlag)); + if (params.page) qs.set('page', String(params.page)); + if (params.pageSize) qs.set('page_size', String(params.pageSize)); + return api.get(`/admin/bank/transactions?${qs.toString()}`); +} + +// ── Bank Account Management ──────────────────────────────── + +export async function listBankAccounts(): Promise<{ items: BankAccount[] }> { + return api.get('/admin/bank/accounts'); +} + +export async function createBankAccount(payload: { + account_name: string; + bank_name: string; + account_no: string; + is_active?: boolean; + is_default?: boolean; + description?: string; +}): Promise<{ id: string; message: string }> { + return api.post('/admin/bank/accounts', payload); +} + +export async function updateBankAccount( + accountId: string, + payload: Partial<{ + account_name: string; + bank_name: string; + account_no: string; + is_active: boolean; + is_default: boolean; + description: string; + }>, +): Promise<{ message: string }> { + return api.put(`/admin/bank/accounts/${accountId}`, payload); +} + +export async function deleteBankAccount(accountId: string): Promise<{ message: string }> { + await api.delete(`/admin/bank/accounts/${accountId}`); + return { message: '删除成功' }; +} + +// ── Scheduled Tasks ──────────────────────────────────────── + +export async function listScheduledTasks(): Promise<{ items: ScheduledTask[] }> { + return api.get('/admin/scheduled-tasks'); +} + +export async function createScheduledTask(payload: { + name: string; + task_type: 'external_api' | 'internal_method'; + schedule: string; + config?: Record | string; + is_active?: boolean; +}): Promise<{ id: string; message: string }> { + return api.post('/admin/scheduled-tasks', payload); +} + +export async function updateScheduledTask( + taskId: string, + payload: Partial<{ + name: string; + task_type: 'external_api' | 'internal_method'; + schedule: string; + config: Record | string; + is_active: boolean; + }>, +): Promise<{ message: string }> { + return api.put(`/admin/scheduled-tasks/${taskId}`, payload); +} + +export async function deleteScheduledTask(taskId: string): Promise<{ message: string }> { + await api.delete(`/admin/scheduled-tasks/${taskId}`); + return { message: '删除成功' }; +} + +export async function runScheduledTask(taskId: string): Promise<{ message: string }> { + return api.post(`/admin/scheduled-tasks/${taskId}/run`, {}); +} + +export async function toggleScheduledTask(taskId: string): Promise<{ is_active: boolean; message: string }> { + return api.post(`/admin/scheduled-tasks/${taskId}/toggle`, {}); +} + export async function updateSingleDeviceLoginOverride(userId: string, override: boolean | null): Promise { await api.put(`/admin/users/${userId}/single-device-login-override`, { override }); } diff --git a/video-gen-admin/src/pages/AdminBankTransactions.tsx b/video-gen-admin/src/pages/AdminBankTransactions.tsx new file mode 100644 index 00000000..0fe0be0f --- /dev/null +++ b/video-gen-admin/src/pages/AdminBankTransactions.tsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Table, Button, Space, Typography, message, Card, DatePicker, Select } from 'antd'; +import { BankOutlined, SearchOutlined } from '@ant-design/icons'; +import { queryBankTransactions, listBankAccounts } from '../api'; +import { formatDate } from '../utils/formatDate'; +import type { BankAccount } from '../types'; + +const { RangePicker } = DatePicker; + +interface TransactionRecord { + counterAcctNo?: string; + cnterName?: string; + cnterBankName?: string; + acctNo?: string; + transAmt?: number | string; + dcFlag?: number; + dcFlagLabel?: string; + transTimeStr?: string; + digestCode?: string; + remark?: string; + balance?: number | string; + tellerSeqno?: string; + transferId?: string; +} + +const AdminBankTransactions: React.FC = () => { + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(20); + + // 银行账户 + const [accounts, setAccounts] = useState([]); + const [selectedAccountId, setSelectedAccountId] = useState(undefined); + + // 搜索条件 + const [dateRange, setDateRange] = useState<[string | null, string | null]>([null, null]); + const [dcFlag, setDcFlag] = useState(undefined); + + useEffect(() => { + loadAccounts(); + }, []); + + const loadAccounts = async () => { + try { + const res = await listBankAccounts(); + setAccounts(res.items || []); + } catch (err: any) { + message.error(err?.message || '加载银行账户失败'); + } + }; + + const fetchData = useCallback(async () => { + if (!selectedAccountId || !dateRange[0] || !dateRange[1]) return; + setLoading(true); + try { + const res = await queryBankTransactions({ + accountId: selectedAccountId, + startDate: dateRange[0], + endDate: dateRange[1], + dcFlag, + page, + pageSize, + }); + setData(res.items || []); + setTotal(res.total || 0); + } catch (err: any) { + message.error(err?.message || '查询失败'); + } finally { + setLoading(false); + } + }, [selectedAccountId, dateRange, dcFlag, page, pageSize]); + + useEffect(() => { + if (selectedAccountId && dateRange[0] && dateRange[1]) { + fetchData(); + } + }, [fetchData]); + + const handleSearch = () => { + if (!selectedAccountId) { + message.warning('请选择银行账户'); + return; + } + if (!dateRange[0] || !dateRange[1]) { + message.warning('请选择交易时间区间'); + return; + } + setPage(1); + fetchData(); + }; + + const selectedAccount = accounts.find(a => a.id === selectedAccountId); + + const columns = [ + { + title: '收款账号', + dataIndex: 'counterAcctNo', + width: 180, + render: (v: string) => v || '-', + }, + { + title: '收款户名', + dataIndex: 'cnterName', + width: 120, + render: (v: string) => v || '-', + }, + { + title: '收款开户行', + dataIndex: 'cnterBankName', + width: 150, + render: (v: string) => v || '-', + }, + { + title: '我方付款账号', + dataIndex: 'acctNo', + width: 180, + render: (v: string) => v || '-', + }, + { + title: '打款金额', + dataIndex: 'transAmt', + width: 120, + align: 'right' as const, + render: (v: number | string) => v != null ? Number(v).toFixed(2) : '-', + }, + { + title: '借贷方向', + dataIndex: 'dcFlagLabel', + width: 100, + render: (v: string, r: TransactionRecord) => v || (r.dcFlag === 0 ? '借/出金' : r.dcFlag === 1 ? '贷/入金' : '-'), + }, + { + title: '打款时间', + dataIndex: 'transTimeStr', + width: 160, + render: (v: string) => v ? formatDate(v) : '-', + }, + { + title: '摘要', + dataIndex: 'digestCode', + width: 100, + render: (v: string) => v || '-', + }, + { + title: '用途/备注', + dataIndex: 'remark', + width: 150, + render: (v: string) => v || '-', + }, + { + title: '账户余额', + dataIndex: 'balance', + width: 120, + align: 'right' as const, + render: (v: number | string) => v != null ? Number(v).toFixed(2) : '-', + }, + { + title: '流水号', + dataIndex: 'tellerSeqno', + width: 150, + render: (v: string) => v || '-', + }, + ]; + + return ( +
+ +
+
+ +
+
+ 银行交易查询 + 查询银行账户交易流水记录 +
+
+ + + + 借/出金 + 贷/入金 + + + + + {selectedAccount && ( +
+ 当前查询账户:{selectedAccount.bank_name} | {selectedAccount.account_no} | {selectedAccount.account_name} +
+ )} +
+ + + `${r.tellerSeqno || ''}-${r.transferId || ''}-${i}`} + columns={columns} + dataSource={data} + loading={loading} + scroll={{ x: 1400 }} + pagination={{ + current: page, + pageSize, + total, + showSizeChanger: true, + showTotal: (t) => `共 ${t} 条`, + onChange: (p, ps) => { + setPage(p); + setPageSize(ps); + }, + }} + /> + + + ); +}; + +export default AdminBankTransactions; diff --git a/video-gen-admin/src/pages/AdminScheduledTasks.tsx b/video-gen-admin/src/pages/AdminScheduledTasks.tsx new file mode 100644 index 00000000..ff39e1aa --- /dev/null +++ b/video-gen-admin/src/pages/AdminScheduledTasks.tsx @@ -0,0 +1,311 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + Table, Button, Space, Typography, message, Card, Modal, Form, Input, Select, Switch, Tag, Popconfirm, Tabs, +} from 'antd'; +import { + ClockCircleOutlined, PlusOutlined, EditOutlined, DeleteOutlined, PlayCircleOutlined, StopOutlined, CheckCircleOutlined, CloseCircleOutlined, +} from '@ant-design/icons'; +import { + listScheduledTasks, + createScheduledTask, + updateScheduledTask, + deleteScheduledTask, + runScheduledTask, + toggleScheduledTask, +} from '../api'; +import type { ScheduledTask } from '../types'; + +const { TextArea } = Input; + +const SCHEDULE_TYPE_OPTIONS = [ + { value: 'external_api', label: '外部接口调用' }, + { value: 'internal_method', label: '内部方法执行' }, +]; + +const TASK_TYPE_LABELS: Record = { + external_api: '外部接口', + internal_method: '内部方法', +}; + +const STATUS_LABELS: Record = { + success: { label: '成功', color: 'green' }, + error: { label: '失败', color: 'red' }, +}; + +const AdminScheduledTasks: React.FC = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [modal, setModal] = useState(false); + const [editing, setEditing] = useState(null); + const [form] = Form.useForm(); + const [saving, setSaving] = useState(false); + const [activeTab, setActiveTab] = useState<'basic' | 'config'>('basic'); + + const fetchData = useCallback(async () => { + setLoading(true); + try { + const res = await listScheduledTasks(); + setData(res.items || []); + } catch (err: any) { + message.error(err?.message || '加载失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const openModal = (task?: ScheduledTask) => { + if (task) { + setEditing(task); + let configStr = ''; + if (task.config) { + try { + configStr = typeof task.config === 'string' ? JSON.stringify(JSON.parse(task.config), null, 2) : JSON.stringify(task.config, null, 2); + } catch { + configStr = task.config; + } + } + form.setFieldsValue({ + name: task.name, + task_type: task.task_type, + schedule: task.schedule, + config: configStr, + is_active: task.is_active, + }); + } else { + setEditing(null); + form.resetFields(); + form.setFieldsValue({ is_active: true, task_type: 'external_api' }); + } + setActiveTab('basic'); + setModal(true); + }; + + const handleSave = async () => { + try { + const values = await form.validateFields(); + let configVal = values.config; + if (configVal && typeof configVal === 'string') { + try { + configVal = JSON.stringify(JSON.parse(configVal)); + } catch { + message.warning('配置 JSON 格式不合法,将按原样保存'); + } + } + setSaving(true); + if (editing) { + await updateScheduledTask(editing.id, { ...values, config: configVal }); + message.success('更新成功'); + } else { + await createScheduledTask({ ...values, config: configVal }); + message.success('创建成功'); + } + setModal(false); + await fetchData(); + } catch (err: any) { + message.error(err?.message || '保存失败'); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (taskId: string) => { + try { + await deleteScheduledTask(taskId); + message.success('删除成功'); + await fetchData(); + } catch (err: any) { + message.error(err?.message || '删除失败'); + } + }; + + const handleRun = async (taskId: string) => { + try { + await runScheduledTask(taskId); + message.success('任务已提交执行'); + setTimeout(fetchData, 1500); + } catch (err: any) { + message.error(err?.message || '执行失败'); + } + }; + + const handleToggle = async (task: ScheduledTask) => { + try { + const res = await toggleScheduledTask(task.id); + message.success(res.message); + await fetchData(); + } catch (err: any) { + message.error(err?.message || '操作失败'); + } + }; + + const taskType = Form.useWatch('task_type', form); + + const columns = [ + { title: '任务名称', dataIndex: 'name', width: 160, ellipsis: true }, + { + title: '类型', + dataIndex: 'task_type', + width: 100, + render: (v: string) => {TASK_TYPE_LABELS[v] || v}, + }, + { title: '调度表达式', dataIndex: 'schedule', width: 140, render: (v: string) => {v} }, + { + title: '状态', + dataIndex: 'is_active', + width: 80, + render: (v: boolean) => {v ? '启用' : '禁用'}, + }, + { + title: '最后执行', + dataIndex: 'last_run_at', + width: 160, + render: (v: string, r: ScheduledTask) => { + if (!v) return '-'; + const status = r.last_status ? STATUS_LABELS[r.last_status] : null; + return ( + + {v.includes('T') ? v.replace('T', ' ').slice(0, 19) : v} + {status && {status.label}} + + ); + }, + }, + { + title: '操作', + width: 220, + render: (_: any, record: ScheduledTask) => ( + + + + + handleDelete(record.id)} okText="确定" cancelText="取消"> + + + + ), + }, + ]; + + const scheduleHelp = ( +
+
• 纯数字:间隔秒数(如 60 = 每 60 秒执行一次)
+
• Cron 表达式(5 字段):分 时 日 月 周
+
• 示例:* * * * * = 每分钟 | 0 * * * * = 每小时
+
+ ); + + return ( +
+ +
+
+
+ +
+
+ 定时任务管理 + 自定义定时执行外部接口调用或内部方法 +
+
+ +
+
+ + +
`共 ${t} 条` }} + /> + + + {/* 新增/编辑弹窗 */} + setModal(false)} + confirmLoading={saving} + okText="保存" + cancelText="取消" + destroyOnClose + width={600} + > +
+ setActiveTab(k as 'basic' | 'config')} + items={[ + { + key: 'basic', + label: '基本设置', + children: ( + <> + + + + + + + + + + + ), + }, + { + key: 'config', + label: '任务配置', + children: ( + +