2026-04-08 09:29:06 +00:00
|
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
import {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Space,
|
2026-03-26 09:32:31 +00:00
|
|
|
|
Drawer,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
Form,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
App,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Switch,
|
|
|
|
|
|
InputNumber,
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Upload,
|
|
|
|
|
|
Tabs,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
DownloadOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
MobileOutlined,
|
|
|
|
|
|
DesktopOutlined,
|
|
|
|
|
|
PartitionOutlined,
|
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
|
InfoCircleOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
CloudUploadOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
RocketOutlined,
|
2026-03-26 09:32:31 +00:00
|
|
|
|
SaveOutlined,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
} from '@ant-design/icons';
|
2026-04-08 11:19:33 +00:00
|
|
|
|
import httpService from '../services/httpService';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import AdminModuleShell from '../components/AdminModuleShell';
|
2026-04-03 16:25:53 +00:00
|
|
|
|
import ActionButton from '../components/ActionButton';
|
2026-04-08 09:29:06 +00:00
|
|
|
|
import useSystemPageSize from '../hooks/useSystemPageSize';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
|
|
|
|
|
|
|
const CLIENT_TABS = [
|
|
|
|
|
|
{ key: 'all', label: '全部' },
|
|
|
|
|
|
{ key: 'mobile', label: <Space><MobileOutlined />移动端</Space> },
|
|
|
|
|
|
{ key: 'desktop', label: <Space><DesktopOutlined />桌面端</Space> },
|
|
|
|
|
|
{ key: 'terminal', label: <Space><PartitionOutlined />专用终端</Space> },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_OPTIONS = [
|
|
|
|
|
|
{ label: '全部状态', value: 'all' },
|
|
|
|
|
|
{ label: '已启用', value: 'active' },
|
|
|
|
|
|
{ label: '已停用', value: 'inactive' },
|
|
|
|
|
|
{ label: '最新版本', value: 'latest' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const isTruthy = (value) => value === true || value === 1 || value === '1';
|
|
|
|
|
|
|
|
|
|
|
|
const ClientManagement = () => {
|
|
|
|
|
|
const { message, modal } = App.useApp();
|
|
|
|
|
|
const [form] = Form.useForm();
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
const [clients, setClients] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-03-26 09:32:31 +00:00
|
|
|
|
const [showDrawer, setShowDrawer] = useState(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
|
|
const [selectedClient, setSelectedClient] = useState(null);
|
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
|
|
|
|
const [activeTab, setActiveTab] = useState('all');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
const [uploadingFile, setUploadingFile] = useState(false);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const [updatingStatusId, setUpdatingStatusId] = useState(null);
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const pageSize = useSystemPageSize(10);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
const [platforms, setPlatforms] = useState({ tree: [], items: [] });
|
|
|
|
|
|
const [platformsMap, setPlatformsMap] = useState({});
|
|
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const fetchPlatforms = useCallback(async () => {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.DICT_DATA.BY_TYPE('client_platform')));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (response.code === '200') {
|
|
|
|
|
|
const { tree = [], items = [] } = response.data || {};
|
|
|
|
|
|
setPlatforms({ tree, items });
|
|
|
|
|
|
|
|
|
|
|
|
const nextMap = {};
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
|
nextMap[item.dict_code] = item;
|
|
|
|
|
|
});
|
|
|
|
|
|
setPlatformsMap(nextMap);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('获取平台列表失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
}, [message]);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
const fetchClients = useCallback(async () => {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
const response = await httpService.get(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.LIST));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (response.code === '200') {
|
|
|
|
|
|
setClients(response.data.clients || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('获取客户端列表失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
}, [message]);
|
|
|
|
|
|
|
|
|
|
|
|
const getPlatformLabel = useCallback(
|
|
|
|
|
|
(platformCode) => platformsMap[platformCode]?.label_cn || platformCode || '-',
|
|
|
|
|
|
[platformsMap],
|
|
|
|
|
|
);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-04-08 09:29:06 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchPlatforms();
|
|
|
|
|
|
fetchClients();
|
|
|
|
|
|
}, [fetchClients, fetchPlatforms]);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const openModal = (client = null) => {
|
|
|
|
|
|
if (client) {
|
|
|
|
|
|
setIsEditing(true);
|
|
|
|
|
|
setSelectedClient(client);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
...client,
|
|
|
|
|
|
version_code: client.version_code,
|
|
|
|
|
|
file_size: client.file_size,
|
|
|
|
|
|
is_active: isTruthy(client.is_active),
|
|
|
|
|
|
is_latest: isTruthy(client.is_latest),
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setIsEditing(false);
|
|
|
|
|
|
setSelectedClient(null);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
is_active: true,
|
|
|
|
|
|
is_latest: false,
|
|
|
|
|
|
});
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
2026-03-26 09:32:31 +00:00
|
|
|
|
setShowDrawer(true);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleSave = async () => {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
const platformInfo = platformsMap[values.platform_code];
|
|
|
|
|
|
const parentCode = platformInfo?.parent_code;
|
|
|
|
|
|
|
|
|
|
|
|
let platformType = 'desktop';
|
|
|
|
|
|
if (parentCode === 'MOBILE') {
|
|
|
|
|
|
platformType = 'mobile';
|
|
|
|
|
|
} else if (parentCode === 'TERMINAL') {
|
|
|
|
|
|
platformType = 'terminal';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const payload = {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
...values,
|
|
|
|
|
|
platform_type: platformType,
|
|
|
|
|
|
platform_name: String(values.platform_code || '').toLowerCase(),
|
2026-01-19 11:03:08 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (isEditing) {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
await httpService.put(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(selectedClient.id)), payload);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('版本更新成功');
|
|
|
|
|
|
} else {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
await httpService.post(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.CREATE), payload);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('版本创建成功');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 09:32:31 +00:00
|
|
|
|
setShowDrawer(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
fetchClients();
|
|
|
|
|
|
} catch (error) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (!error?.errorFields) {
|
|
|
|
|
|
message.error('保存失败');
|
|
|
|
|
|
}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleDelete = (item) => {
|
|
|
|
|
|
modal.confirm({
|
|
|
|
|
|
title: '删除客户端版本',
|
|
|
|
|
|
content: `确定要删除 ${getPlatformLabel(item.platform_code)} ${item.version} 吗?`,
|
|
|
|
|
|
okText: '确定',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
await httpService.delete(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.DELETE(item.id)));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.success('删除成功');
|
|
|
|
|
|
fetchClients();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-19 11:03:08 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleToggleActive = async (item, checked) => {
|
|
|
|
|
|
setUpdatingStatusId(item.id);
|
|
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
await httpService.put(buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPDATE(item.id)), {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
is_active: checked,
|
2026-01-19 11:03:08 +00:00
|
|
|
|
});
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setClients((prev) => prev.map((client) => (
|
|
|
|
|
|
client.id === item.id ? { ...client, is_active: checked } : client
|
|
|
|
|
|
)));
|
|
|
|
|
|
message.success(`已${checked ? '启用' : '停用'}版本`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('状态更新失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setUpdatingStatusId(null);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleFileUpload = async (options) => {
|
|
|
|
|
|
const { file, onSuccess, onError } = options;
|
|
|
|
|
|
const platformCode = form.getFieldValue('platform_code');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (!platformCode) {
|
|
|
|
|
|
message.warning('请先选择发布平台');
|
|
|
|
|
|
onError(new Error('No platform selected'));
|
2026-01-19 11:03:08 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setUploadingFile(true);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const uploadFormData = new FormData();
|
|
|
|
|
|
uploadFormData.append('file', file);
|
|
|
|
|
|
uploadFormData.append('platform_code', platformCode);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-04-08 11:19:33 +00:00
|
|
|
|
const response = await httpService.post(
|
2026-01-19 11:03:08 +00:00
|
|
|
|
buildApiUrl(API_ENDPOINTS.CLIENT_DOWNLOADS.UPLOAD),
|
|
|
|
|
|
uploadFormData,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
2026-01-19 11:03:08 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (response.code === '200') {
|
|
|
|
|
|
const {
|
|
|
|
|
|
file_size: fileSize,
|
|
|
|
|
|
download_url: downloadUrl,
|
|
|
|
|
|
version_code: versionCode,
|
|
|
|
|
|
version_name: versionName,
|
|
|
|
|
|
} = response.data;
|
|
|
|
|
|
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
file_size: fileSize,
|
|
|
|
|
|
download_url: downloadUrl,
|
|
|
|
|
|
version_code: versionCode,
|
|
|
|
|
|
version: versionName,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
message.success('安装包上传成功,已自动填充版本信息');
|
|
|
|
|
|
onSuccess(response.data);
|
|
|
|
|
|
}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} catch (error) {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
message.error('文件上传失败');
|
|
|
|
|
|
onError(error);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setUploadingFile(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatFileSize = (bytes) => {
|
|
|
|
|
|
if (!bytes) return '-';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const filteredClients = useMemo(() => clients.filter((client) => {
|
|
|
|
|
|
if (activeTab !== 'all' && client.platform_type !== activeTab) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const enabled = isTruthy(client.is_active);
|
|
|
|
|
|
const latest = isTruthy(client.is_latest);
|
|
|
|
|
|
|
|
|
|
|
|
if (statusFilter === 'active' && !enabled) {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (statusFilter === 'inactive' && enabled) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (statusFilter === 'latest' && !latest) {
|
|
|
|
|
|
return false;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (!searchQuery) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const query = searchQuery.toLowerCase();
|
|
|
|
|
|
return [
|
|
|
|
|
|
client.version,
|
|
|
|
|
|
client.platform_code,
|
|
|
|
|
|
getPlatformLabel(client.platform_code),
|
|
|
|
|
|
client.min_system_version,
|
|
|
|
|
|
client.download_url,
|
|
|
|
|
|
client.release_notes,
|
|
|
|
|
|
].some((field) => String(field || '').toLowerCase().includes(query));
|
2026-04-08 09:29:06 +00:00
|
|
|
|
}), [activeTab, clients, getPlatformLabel, searchQuery, statusFilter]);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
|
|
|
|
|
|
const publishedCount = clients.length;
|
|
|
|
|
|
const activeCount = clients.filter((item) => isTruthy(item.is_active)).length;
|
|
|
|
|
|
const latestCount = clients.filter((item) => isTruthy(item.is_latest)).length;
|
|
|
|
|
|
const terminalCount = clients.filter((item) => item.platform_type === 'terminal').length;
|
|
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '平台',
|
|
|
|
|
|
dataIndex: 'platform_code',
|
|
|
|
|
|
key: 'platform_code',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (code) => <Tag color="blue">{getPlatformLabel(code)}</Tag>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '版本信息',
|
|
|
|
|
|
key: 'version',
|
|
|
|
|
|
width: 200,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Text strong>{record.version}</Text>
|
|
|
|
|
|
<Text type="secondary">版本码:{record.version_code}</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '安装包信息',
|
|
|
|
|
|
key: 'package',
|
|
|
|
|
|
width: 220,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Text type="secondary">大小:{formatFileSize(record.file_size)}</Text>
|
|
|
|
|
|
<Text type="secondary" ellipsis style={{ maxWidth: 200 }}>
|
|
|
|
|
|
系统要求:{record.min_system_version || '-'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '发布状态',
|
|
|
|
|
|
key: 'status',
|
2026-03-26 09:32:31 +00:00
|
|
|
|
width: 140,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<Switch
|
2026-03-26 09:32:31 +00:00
|
|
|
|
size="small"
|
2026-03-26 06:55:12 +00:00
|
|
|
|
checked={isTruthy(record.is_active)}
|
|
|
|
|
|
loading={updatingStatusId === record.id}
|
|
|
|
|
|
onChange={(checked) => handleToggleActive(record, checked)}
|
|
|
|
|
|
/>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
{isTruthy(record.is_latest) ? <Badge status="success" text="最新版本" /> : <Badge status="default" text="历史版本" />}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '更新时间',
|
|
|
|
|
|
dataIndex: 'updated_at',
|
|
|
|
|
|
key: 'updated_at',
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (value) => (value ? new Date(value).toLocaleString() : '-'),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (_, record) => (
|
2026-04-03 16:25:53 +00:00
|
|
|
|
<Space size={6}>
|
|
|
|
|
|
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => openModal(record)} />
|
|
|
|
|
|
<ActionButton tone="view" variant="iconSm" tooltip="下载" icon={<DownloadOutlined />} href={record.download_url} target="_blank" />
|
|
|
|
|
|
<ActionButton tone="delete" variant="iconSm" tooltip="删除" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<div className="client-management">
|
|
|
|
|
|
<AdminModuleShell
|
|
|
|
|
|
icon={<DesktopOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />}
|
|
|
|
|
|
title="客户端管理"
|
|
|
|
|
|
subtitle="统一维护移动端、桌面端与专用终端的版本发布、安装包上传、启停控制与最新版本标记。"
|
|
|
|
|
|
rightActions={(
|
|
|
|
|
|
<Space>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Button icon={<ReloadOutlined />} className="btn-soft-blue" onClick={fetchClients} loading={loading}>刷新</Button>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal()}>新增发布</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
stats={[
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '发布总数',
|
|
|
|
|
|
value: publishedCount,
|
|
|
|
|
|
icon: <CloudUploadOutlined />,
|
|
|
|
|
|
tone: 'blue',
|
|
|
|
|
|
desc: '当前系统登记的全部客户端版本',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '启用版本',
|
|
|
|
|
|
value: activeCount,
|
|
|
|
|
|
icon: <CheckCircleOutlined />,
|
|
|
|
|
|
tone: 'green',
|
|
|
|
|
|
desc: '对外可见、允许分发的版本数量',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最新版本',
|
|
|
|
|
|
value: latestCount,
|
|
|
|
|
|
icon: <RocketOutlined />,
|
|
|
|
|
|
tone: 'violet',
|
|
|
|
|
|
desc: '被标记为最新的各平台主推版本',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '终端发布',
|
|
|
|
|
|
value: terminalCount,
|
|
|
|
|
|
icon: <PartitionOutlined />,
|
|
|
|
|
|
tone: 'amber',
|
|
|
|
|
|
desc: '面向专用终端设备的独立发布包数量',
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
toolbar={(
|
|
|
|
|
|
<Space wrap>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索平台、版本、系统要求或下载地址..."
|
|
|
|
|
|
prefix={<SearchOutlined />}
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(event) => setSearchQuery(event.target.value)}
|
|
|
|
|
|
style={{ width: 300 }}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={statusFilter}
|
|
|
|
|
|
onChange={setStatusFilter}
|
|
|
|
|
|
options={STATUS_OPTIONS}
|
|
|
|
|
|
style={{ width: 150 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Tabs
|
|
|
|
|
|
className="console-tabs"
|
|
|
|
|
|
activeKey={activeTab}
|
|
|
|
|
|
onChange={setActiveTab}
|
|
|
|
|
|
items={CLIENT_TABS.map((tab) => ({ ...tab, children: null }))}
|
|
|
|
|
|
/>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<div className="console-table">
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={filteredClients}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
scroll={{ x: 1040 }}
|
2026-04-08 09:29:06 +00:00
|
|
|
|
pagination={{ pageSize, showTotal: (total) => `共 ${total} 条记录` }}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
/>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</AdminModuleShell>
|
|
|
|
|
|
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Drawer
|
|
|
|
|
|
open={showDrawer}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
title={isEditing ? '编辑客户端版本' : '发布新版本'}
|
2026-03-26 09:32:31 +00:00
|
|
|
|
placement="right"
|
|
|
|
|
|
width={760}
|
|
|
|
|
|
onClose={() => setShowDrawer(false)}
|
|
|
|
|
|
destroyOnClose
|
|
|
|
|
|
extra={(
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave}>
|
|
|
|
|
|
{isEditing ? '保存修改' : '发布版本'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
2026-01-19 11:03:08 +00:00
|
|
|
|
>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Form form={form} layout="vertical" style={{ marginTop: 20 }}>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="platform_code" label="发布平台" rules={[{ required: true, message: '请选择发布平台' }]}>
|
|
|
|
|
|
<Select placeholder="选择平台" disabled={isEditing}>
|
|
|
|
|
|
{platforms.tree.map((parentNode) => (
|
|
|
|
|
|
<Select.OptGroup key={parentNode.dict_code} label={parentNode.label_cn}>
|
|
|
|
|
|
{parentNode.children?.map((childNode) => (
|
|
|
|
|
|
<Select.Option key={childNode.dict_code} value={childNode.dict_code}>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
{childNode.label_cn}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Select.Option>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
))}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Select.OptGroup>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
))}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item label="安装包上传">
|
|
|
|
|
|
<Upload customRequest={handleFileUpload} showUploadList={false} disabled={uploadingFile}>
|
|
|
|
|
|
<Button icon={<UploadOutlined />} loading={uploadingFile} style={{ width: '100%' }}>
|
|
|
|
|
|
{uploadingFile ? '上传中...' : '点击上传安装包'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Upload>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="version" label="版本号" rules={[{ required: true, message: '请输入版本号' }]}>
|
|
|
|
|
|
<Input placeholder="例如 1.0.0" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="version_code" label="版本代码" rules={[{ required: true, message: '请输入版本代码' }]}>
|
|
|
|
|
|
<InputNumber style={{ width: '100%' }} min={0} placeholder="例如 100" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="download_url" label="下载链接" rules={[{ required: true, message: '请输入下载链接' }]}>
|
|
|
|
|
|
<Input placeholder="https://..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="file_size" label="文件大小(Bytes)">
|
|
|
|
|
|
<InputNumber style={{ width: '100%' }} min={0} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="min_system_version" label="最低系统要求">
|
|
|
|
|
|
<Input placeholder="例如 Android 8.0 / macOS 11" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="release_notes" label="更新说明">
|
|
|
|
|
|
<TextArea rows={4} placeholder="请输入本次版本更新内容..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Space size="large">
|
|
|
|
|
|
<Form.Item name="is_active" label="立即启用" valuePropName="checked">
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="is_latest" label="设为最新版本" valuePropName="checked">
|
|
|
|
|
|
<Switch />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ backgroundColor: '#fffbe6', padding: '8px 12px', borderRadius: 8, marginTop: 8 }}>
|
|
|
|
|
|
<Space align="start">
|
|
|
|
|
|
<InfoCircleOutlined style={{ color: '#faad14', marginTop: 2 }} />
|
|
|
|
|
|
<Text type="secondary">
|
|
|
|
|
|
上传 APK 会自动提取版本信息;设为“最新版本”后,同平台其他版本会自动取消最新标记。
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
</Drawer>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ClientManagement;
|