1、后台菜单增加定时任务和银行交易
2、增加后台配置银行账户和请求网址
This commit is contained in:
@@ -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 = () => {
|
||||
<Route path="api-model-pricings" element={<AdminApiModelPricings />} />
|
||||
<Route path="api-usage" element={<AdminApiUsage />} />
|
||||
<Route path="invoices" element={<AdminInvoices />} />
|
||||
<Route path="bank-transactions" element={<AdminBankTransactions />} />
|
||||
<Route path="scheduled-tasks" element={<AdminScheduledTasks />} />
|
||||
<Route path="notifications" element={<AdminNotificationManager />} />
|
||||
<Route path="oauthapp-list" element={<AdminOauthAppList />} />
|
||||
<Route path="operation-logs" element={<AdminOperationLogs />} />
|
||||
|
||||
@@ -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, unknown> | 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, unknown> | 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<void> {
|
||||
await api.put(`/admin/users/${userId}/single-device-login-override`, { override });
|
||||
}
|
||||
|
||||
@@ -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<TransactionRecord[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
|
||||
// 银行账户
|
||||
const [accounts, setAccounts] = useState<BankAccount[]>([]);
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<string | undefined>(undefined);
|
||||
|
||||
// 搜索条件
|
||||
const [dateRange, setDateRange] = useState<[string | null, string | null]>([null, null]);
|
||||
const [dcFlag, setDcFlag] = useState<number | undefined>(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 (
|
||||
<div>
|
||||
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(99,102,241,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, color: '#6366f1',
|
||||
}}>
|
||||
<BankOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>银行交易查询</Typography.Title>
|
||||
<Typography.Text type="secondary">查询银行账户交易流水记录</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Select
|
||||
placeholder="选择银行账户"
|
||||
value={selectedAccountId}
|
||||
onChange={setSelectedAccountId}
|
||||
style={{ width: 260 }}
|
||||
allowClear
|
||||
options={accounts.map(a => ({
|
||||
value: a.id,
|
||||
label: `${a.bank_name} - ${a.account_no} (${a.account_name})`,
|
||||
}))}
|
||||
/>
|
||||
<RangePicker
|
||||
onChange={(dates, dateStrings) => {
|
||||
if (Array.isArray(dateStrings)) {
|
||||
setDateRange([dateStrings[0] || null, dateStrings[1] || null]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
placeholder="借贷方向"
|
||||
value={dcFlag}
|
||||
onChange={setDcFlag}
|
||||
style={{ width: 140 }}
|
||||
allowClear
|
||||
>
|
||||
<Select.Option value={0}>借/出金</Select.Option>
|
||||
<Select.Option value={1}>贷/入金</Select.Option>
|
||||
</Select>
|
||||
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
{selectedAccount && (
|
||||
<div style={{ padding: '8px 12px', background: '#f8fafc', borderRadius: 8, fontSize: 13, color: '#64748b' }}>
|
||||
<strong>当前查询账户:</strong>{selectedAccount.bank_name} | {selectedAccount.account_no} | {selectedAccount.account_name}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Table
|
||||
rowKey={(r, i) => `${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);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminBankTransactions;
|
||||
@@ -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<string, string> = {
|
||||
external_api: '外部接口',
|
||||
internal_method: '内部方法',
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||
success: { label: '成功', color: 'green' },
|
||||
error: { label: '失败', color: 'red' },
|
||||
};
|
||||
|
||||
const AdminScheduledTasks: React.FC = () => {
|
||||
const [data, setData] = useState<ScheduledTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [editing, setEditing] = useState<ScheduledTask | null>(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) => <Tag color={v === 'external_api' ? 'blue' : 'purple'}>{TASK_TYPE_LABELS[v] || v}</Tag>,
|
||||
},
|
||||
{ title: '调度表达式', dataIndex: 'schedule', width: 140, render: (v: string) => <code style={{ background: '#f1f5f9', padding: '2px 6px', borderRadius: 4 }}>{v}</code> },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'is_active',
|
||||
width: 80,
|
||||
render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag>,
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<span>
|
||||
{v.includes('T') ? v.replace('T', ' ').slice(0, 19) : v}
|
||||
{status && <Tag color={status.color} style={{ marginLeft: 6 }}>{status.label}</Tag>}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_: any, record: ScheduledTask) => (
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<PlayCircleOutlined />} onClick={() => handleRun(record.id)}>执行</Button>
|
||||
<Button type="link" size="small" icon={record.is_active ? <StopOutlined /> : <CheckCircleOutlined />} onClick={() => handleToggle(record)}>
|
||||
{record.is_active ? '禁用' : '启用'}
|
||||
</Button>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openModal(record)}>编辑</Button>
|
||||
<Popconfirm title="确定删除该任务?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const scheduleHelp = (
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginTop: 4 }}>
|
||||
<div>• 纯数字:间隔秒数(如 60 = 每 60 秒执行一次)</div>
|
||||
<div>• Cron 表达式(5 字段):分 时 日 月 周</div>
|
||||
<div>• 示例:<code>* * * * *</code> = 每分钟 | <code>0 * * * *</code> = 每小时</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 44, height: 44, borderRadius: 10,
|
||||
background: 'rgba(99,102,241,0.08)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 20, color: '#6366f1',
|
||||
}}>
|
||||
<ClockCircleOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>定时任务管理</Typography.Title>
|
||||
<Typography.Text type="secondary">自定义定时执行外部接口调用或内部方法</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal()}>
|
||||
新增任务
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20, showTotal: (t) => `共 ${t} 条` }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 新增/编辑弹窗 */}
|
||||
<Modal
|
||||
title={editing ? '编辑定时任务' : '新增定时任务'}
|
||||
open={modal}
|
||||
onOk={handleSave}
|
||||
onCancel={() => setModal(false)}
|
||||
confirmLoading={saving}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
destroyOnClose
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(k) => setActiveTab(k as 'basic' | 'config')}
|
||||
items={[
|
||||
{
|
||||
key: 'basic',
|
||||
label: '基本设置',
|
||||
children: (
|
||||
<>
|
||||
<Form.Item name="name" label="任务名称" rules={[{ required: true, message: '请输入任务名称' }]}>
|
||||
<Input placeholder="例如:每小时同步银行交易" />
|
||||
</Form.Item>
|
||||
<Form.Item name="task_type" label="任务类型" rules={[{ required: true, message: '请选择任务类型' }]}>
|
||||
<Select options={SCHEDULE_TYPE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="schedule"
|
||||
label="调度表达式"
|
||||
rules={[{ required: true, message: '请输入调度表达式' }]}
|
||||
extra={scheduleHelp}
|
||||
>
|
||||
<Input placeholder="纯数字(秒)或 Cron 表达式:* * * * *" />
|
||||
</Form.Item>
|
||||
<Form.Item name="is_active" label="启用" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||
</Form.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'config',
|
||||
label: '任务配置',
|
||||
children: (
|
||||
<Form.Item
|
||||
name="config"
|
||||
label="配置 JSON"
|
||||
extra={
|
||||
taskType === 'external_api'
|
||||
? '字段:url(地址)、method(GET/POST/PUT/DELETE)、headers(对象)、payload(对象/参数)、timeout(秒,默认 30)'
|
||||
: '字段:module(模块路径,如 app.services.xxx)、function(函数名)、args(参数数组)'
|
||||
}
|
||||
>
|
||||
<TextArea
|
||||
rows={8}
|
||||
placeholder={taskType === 'external_api'
|
||||
? '{\n "url": "https://api.example.com/data",\n "method": "POST",\n "headers": {},\n "payload": {}\n}'
|
||||
: '{\n "module": "app.services.report",\n "function": "generate_daily_report",\n "args": []\n}'
|
||||
}
|
||||
style={{ fontFamily: 'monospace', fontSize: 13 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminScheduledTasks;
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button, Card, Form, Input, InputNumber, message, Select, Space, Switch, Tabs, Typography, Upload,
|
||||
Button, Card, Form, Input, InputNumber, message, Modal, Select, Space, Switch, Tabs, Typography, Upload, Table, Tag, Popconfirm,
|
||||
} from 'antd';
|
||||
import {
|
||||
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, VideoCameraOutlined, RobotOutlined,
|
||||
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined, DatabaseOutlined, VideoCameraOutlined, RobotOutlined, BankOutlined, PlusOutlined, EditOutlined, DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
createSystemConfig,
|
||||
@@ -14,8 +14,12 @@ import {
|
||||
uploadLogo,
|
||||
uploadPdf,
|
||||
uploadLoginVideo,
|
||||
listBankAccounts,
|
||||
createBankAccount,
|
||||
updateBankAccount,
|
||||
deleteBankAccount,
|
||||
} from '../api';
|
||||
import type { ResourceCapacityUnit, SystemConfig } from '../types';
|
||||
import type { ResourceCapacityUnit, SystemConfig, BankAccount } from '../types';
|
||||
|
||||
const capacityUnitOptions: { value: ResourceCapacityUnit; label: string }[] = [
|
||||
{ value: 'MB', label: 'MB(1024 × 1024 字节)' },
|
||||
@@ -30,10 +34,116 @@ const AdminSettings: React.FC = () => {
|
||||
const [uploading, setUploading] = useState('');
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 银行账户管理
|
||||
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
||||
const [loadingAccounts, setLoadingAccounts] = useState(false);
|
||||
const [accountModal, setAccountModal] = useState(false);
|
||||
const [accountEditing, setAccountEditing] = useState<BankAccount | null>(null);
|
||||
const [accountForm] = Form.useForm();
|
||||
const [accountSaving, setAccountSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
loadBankAccounts();
|
||||
}, []);
|
||||
|
||||
const loadBankAccounts = async () => {
|
||||
setLoadingAccounts(true);
|
||||
try {
|
||||
const res = await listBankAccounts();
|
||||
setBankAccounts(res.items || []);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载银行账户失败');
|
||||
} finally {
|
||||
setLoadingAccounts(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openAccountModal = (account?: BankAccount) => {
|
||||
if (account) {
|
||||
setAccountEditing(account);
|
||||
accountForm.setFieldsValue({
|
||||
account_name: account.account_name,
|
||||
bank_name: account.bank_name,
|
||||
account_no: account.account_no,
|
||||
is_active: account.is_active,
|
||||
is_default: account.is_default,
|
||||
description: account.description,
|
||||
});
|
||||
} else {
|
||||
setAccountEditing(null);
|
||||
accountForm.resetFields();
|
||||
accountForm.setFieldsValue({ is_active: true, is_default: false });
|
||||
}
|
||||
setAccountModal(true);
|
||||
};
|
||||
|
||||
const handleAccountSave = async () => {
|
||||
try {
|
||||
const values = await accountForm.validateFields();
|
||||
setAccountSaving(true);
|
||||
if (accountEditing) {
|
||||
await updateBankAccount(accountEditing.id, values);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await createBankAccount(values);
|
||||
message.success('创建成功');
|
||||
}
|
||||
setAccountModal(false);
|
||||
await loadBankAccounts();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
setAccountSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountDelete = async (accountId: string) => {
|
||||
try {
|
||||
await deleteBankAccount(accountId);
|
||||
message.success('删除成功');
|
||||
await loadBankAccounts();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const accountColumns = [
|
||||
{ title: '账户名称', dataIndex: 'account_name', width: 150 },
|
||||
{ title: '开户银行', dataIndex: 'bank_name', width: 150 },
|
||||
{
|
||||
title: '银行账号',
|
||||
dataIndex: 'account_no',
|
||||
width: 180,
|
||||
render: (v: string) => <code style={{ background: '#f1f5f9', padding: '2px 6px', borderRadius: 4 }}>{v}</code>,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'is_active',
|
||||
width: 80,
|
||||
render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '禁用'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '默认',
|
||||
dataIndex: 'is_default',
|
||||
width: 80,
|
||||
render: (v: boolean) => v && <Tag color="blue">默认</Tag>,
|
||||
},
|
||||
{ title: '备注', dataIndex: 'description', ellipsis: true, render: (v: string) => v || '-' },
|
||||
{
|
||||
title: '操作',
|
||||
width: 140,
|
||||
render: (_: any, record: BankAccount) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openAccountModal(record)}>编辑</Button>
|
||||
<Popconfirm title="确定删除该账户?" onConfirm={() => handleAccountDelete(record.id)} okText="确定" cancelText="取消">
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -49,6 +159,10 @@ const AdminSettings: React.FC = () => {
|
||||
if (!data.some(c => c.key === 'single_device_login_enabled')) {
|
||||
data.push({ id: 'cfg_single_device_login_enabled', key: 'single_device_login_enabled', value: 'false', description: '启用单设备登录(同端互斥):同一设备类型只允许一个登录会话' });
|
||||
}
|
||||
// 确保 OA 地址配置存在
|
||||
if (!data.some(c => c.key === 'oa_url')) {
|
||||
data.push({ id: 'cfg_oa_url', key: 'oa_url', value: '', description: 'OA系统地址' });
|
||||
}
|
||||
setConfigs(data);
|
||||
const formValues: Record<string, any> = {};
|
||||
data.forEach(c => { formValues[c.key] = c.value; });
|
||||
@@ -203,6 +317,7 @@ const AdminSettings: React.FC = () => {
|
||||
'协议配置': configs.filter(c => c.key === 'user_agreement_privacy_url'),
|
||||
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
||||
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
|
||||
'系统对接': configs.filter(c => c.key.startsWith('oa_')),
|
||||
'其他配置': configs.filter(c => c.key === 'operation_manual'),
|
||||
};
|
||||
|
||||
@@ -413,6 +528,52 @@ const AdminSettings: React.FC = () => {
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'integration',
|
||||
label: '系统对接',
|
||||
children: (
|
||||
<div>
|
||||
<Form form={form} layout="vertical">
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Typography.Text strong style={{ fontSize: 14, display: 'block', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||||
系统对接
|
||||
</Typography.Text>
|
||||
{groupedConfigs['系统对接']?.map(config => (
|
||||
<Form.Item
|
||||
key={config.id}
|
||||
name={config.key}
|
||||
label={<span style={{ fontWeight: 500 }}>{config.description}</span>}
|
||||
extra={getFieldDescription(config)}
|
||||
>
|
||||
{getFieldComponent(config)}
|
||||
</Form.Item>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{/* 银行账户管理 */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, paddingBottom: 8, borderBottom: '1px solid #f0f0f5' }}>
|
||||
<Typography.Text strong style={{ fontSize: 14 }}>
|
||||
<BankOutlined style={{ marginRight: 8, color: '#6366f1' }} />
|
||||
银行账户管理
|
||||
</Typography.Text>
|
||||
<Button type="primary" icon={<PlusOutlined />} size="small" onClick={() => openAccountModal()}>
|
||||
新增账户
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={bankAccounts}
|
||||
loading={loadingAccounts}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
columns={accountColumns}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
label: '其他配置',
|
||||
@@ -575,6 +736,41 @@ const AdminSettings: React.FC = () => {
|
||||
保存配置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 银行账户新增/编辑弹窗 */}
|
||||
<Modal
|
||||
title={accountEditing ? '编辑银行账户' : '新增银行账户'}
|
||||
open={accountModal}
|
||||
onOk={handleAccountSave}
|
||||
onCancel={() => setAccountModal(false)}
|
||||
confirmLoading={accountSaving}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={accountForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="account_name" label="账户名称" rules={[{ required: true, message: '请输入账户名称' }]}>
|
||||
<Input placeholder="例如:民众普康科技有限公司" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bank_name" label="开户银行" rules={[{ required: true, message: '请输入开户银行' }]}>
|
||||
<Input placeholder="例如:中国工商银行北京分行" />
|
||||
</Form.Item>
|
||||
<Form.Item name="account_no" label="银行账号" rules={[{ required: true, message: '请输入银行账号' }]}>
|
||||
<Input placeholder="银行账号" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="可选备注信息" />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
<Form.Item name="is_active" label="启用" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||
</Form.Item>
|
||||
<Form.Item name="is_default" label="设为默认" valuePropName="checked">
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1610,3 +1610,34 @@ export interface InvoiceDetail {
|
||||
updatedAt: string | null;
|
||||
orders: InvoiceOrder[];
|
||||
}
|
||||
|
||||
// ── Bank Account ───────────────────────────────────────
|
||||
|
||||
export interface BankAccount {
|
||||
id: string;
|
||||
account_name: string;
|
||||
bank_name: string;
|
||||
account_no: string;
|
||||
is_active: boolean;
|
||||
is_default: boolean;
|
||||
description?: string | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
// ── Scheduled Task ─────────────────────────────────────
|
||||
|
||||
export interface ScheduledTask {
|
||||
id: string;
|
||||
name: string;
|
||||
task_type: 'external_api' | 'internal_method';
|
||||
schedule: string;
|
||||
config?: string | null;
|
||||
is_active: boolean;
|
||||
last_run_at?: string | null;
|
||||
last_status?: string | null;
|
||||
last_error?: string | null;
|
||||
created_by?: string | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user