1、后台菜单增加定时任务和银行交易

2、增加后台配置银行账户和请求网址
This commit is contained in:
2026-08-13 15:12:16 +08:00
parent 33a5cc4f94
commit 7dcedf974d
22 changed files with 1741 additions and 3 deletions
+4
View File
@@ -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 />} />
+97
View File
@@ -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(地址)、methodGET/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;
+199 -3
View File
@@ -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: 'MB1024 × 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>
);
};
+31
View File
@@ -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;
}