2026-03-26 06:55:12 +00:00
|
|
|
|
import React, { 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,
|
2026-01-19 11:03:08 +00:00
|
|
|
|
Upload,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
Tag,
|
|
|
|
|
|
Avatar,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
PlusOutlined,
|
2026-03-26 09:32:31 +00:00
|
|
|
|
SaveOutlined,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
GlobalOutlined,
|
|
|
|
|
|
RobotOutlined,
|
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
|
LinkOutlined,
|
|
|
|
|
|
AppstoreOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
ExportOutlined,
|
|
|
|
|
|
PictureOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
BlockOutlined,
|
|
|
|
|
|
} from '@ant-design/icons';
|
2026-01-19 11:03:08 +00:00
|
|
|
|
import apiClient from '../../utils/apiClient';
|
|
|
|
|
|
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
|
2026-03-26 06:55:12 +00:00
|
|
|
|
import AdminModuleShell from '../../components/AdminModuleShell';
|
|
|
|
|
|
|
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
|
|
|
|
|
|
|
const APP_TYPE_OPTIONS = [
|
|
|
|
|
|
{ label: '全部类型', value: 'all' },
|
|
|
|
|
|
{ label: '原生应用', value: 'native' },
|
|
|
|
|
|
{ label: 'Web 应用', value: 'web' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_OPTIONS = [
|
|
|
|
|
|
{ label: '全部状态', value: 'all' },
|
|
|
|
|
|
{ label: '已启用', value: 'active' },
|
|
|
|
|
|
{ label: '已停用', value: 'inactive' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const parseAppInfo = (appInfo) => {
|
|
|
|
|
|
if (!appInfo) return {};
|
|
|
|
|
|
if (typeof appInfo === 'object') return appInfo;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(appInfo);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getAppEntryUrl = (app) => {
|
|
|
|
|
|
const info = parseAppInfo(app.app_info);
|
|
|
|
|
|
return app.app_type === 'native' ? info.apk_url : info.web_url;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const ExternalAppManagement = () => {
|
|
|
|
|
|
const { message, modal } = App.useApp();
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
|
2026-01-19 11:03:08 +00:00
|
|
|
|
const [apps, setApps] = 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 [selectedApp, setSelectedApp] = useState(null);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const [filterAppType, setFilterAppType] = useState('all');
|
|
|
|
|
|
const [filterStatus, setFilterStatus] = useState('all');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const [uploading, setUploading] = useState(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchApps();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const fetchApps = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST));
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (response.code === '200') {
|
|
|
|
|
|
setApps(response.data || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('获取外部应用列表失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleOpenModal = (app = null) => {
|
|
|
|
|
|
if (app) {
|
|
|
|
|
|
setIsEditing(true);
|
|
|
|
|
|
setSelectedApp(app);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
...app,
|
|
|
|
|
|
app_info: parseAppInfo(app.app_info),
|
|
|
|
|
|
is_active: app.is_active === 1 || app.is_active === true,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setIsEditing(false);
|
|
|
|
|
|
setSelectedApp(null);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
app_type: 'native',
|
|
|
|
|
|
is_active: true,
|
|
|
|
|
|
sort_order: 0,
|
|
|
|
|
|
app_info: {},
|
|
|
|
|
|
});
|
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 payload = {
|
|
|
|
|
|
...values,
|
|
|
|
|
|
app_info: JSON.stringify(values.app_info || {}),
|
|
|
|
|
|
is_active: values.is_active ? 1 : 0,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (isEditing) {
|
|
|
|
|
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(selectedApp.id)), payload);
|
|
|
|
|
|
message.success('应用更新成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await apiClient.post(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.CREATE), payload);
|
|
|
|
|
|
message.success('应用创建成功');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 09:32:31 +00:00
|
|
|
|
setShowDrawer(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
fetchApps();
|
|
|
|
|
|
} 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: `确定要删除应用“${item.app_name}”吗?此操作不可恢复。`,
|
|
|
|
|
|
okText: '确定',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await apiClient.delete(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.DELETE(item.id)));
|
|
|
|
|
|
message.success('删除成功');
|
|
|
|
|
|
fetchApps();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('删除失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-19 11:03:08 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleFileUpload = async (options, type) => {
|
|
|
|
|
|
const { file, onSuccess, onError } = options;
|
|
|
|
|
|
setUploading(true);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const uploadFormData = new FormData();
|
|
|
|
|
|
const endpoint = type === 'apk' ? API_ENDPOINTS.EXTERNAL_APPS.UPLOAD_APK : API_ENDPOINTS.EXTERNAL_APPS.UPLOAD_ICON;
|
|
|
|
|
|
const fieldName = type === 'apk' ? 'apk_file' : 'icon_file';
|
|
|
|
|
|
uploadFormData.append(fieldName, file);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const response = await apiClient.post(buildApiUrl(endpoint), uploadFormData, {
|
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.code === '200') {
|
|
|
|
|
|
if (type === 'apk') {
|
|
|
|
|
|
const apkData = response.data;
|
|
|
|
|
|
const currentInfo = form.getFieldValue('app_info') || {};
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
app_name: apkData.app_name || form.getFieldValue('app_name'),
|
|
|
|
|
|
app_info: {
|
|
|
|
|
|
...currentInfo,
|
|
|
|
|
|
version_name: apkData.version_name,
|
|
|
|
|
|
package_name: apkData.package_name,
|
|
|
|
|
|
apk_url: apkData.apk_url,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
message.success('APK 上传并解析成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
form.setFieldsValue({ icon_url: response.data.icon_url });
|
|
|
|
|
|
message.success('应用图标上传成功');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
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 {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
setUploading(false);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const handleToggleStatus = async (item, checked) => {
|
2026-01-19 11:03:08 +00:00
|
|
|
|
try {
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const newActive = checked ? 1 : 0;
|
|
|
|
|
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(item.id)), { is_active: newActive });
|
|
|
|
|
|
setApps((prev) => prev.map((app) => (
|
|
|
|
|
|
app.id === item.id ? { ...app, is_active: newActive } : app
|
|
|
|
|
|
)));
|
|
|
|
|
|
message.success(`已${newActive ? '启用' : '禁用'}应用`);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error('状态更新失败');
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const filteredApps = useMemo(() => apps.filter((app) => {
|
|
|
|
|
|
if (filterAppType !== 'all' && app.app_type !== filterAppType) {
|
|
|
|
|
|
return false;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
|
const enabled = app.is_active === 1 || app.is_active === true;
|
|
|
|
|
|
if (filterStatus === 'active' && !enabled) {
|
|
|
|
|
|
return false;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
if (filterStatus === 'inactive' && enabled) {
|
|
|
|
|
|
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();
|
|
|
|
|
|
const info = parseAppInfo(app.app_info);
|
|
|
|
|
|
return [
|
|
|
|
|
|
app.app_name,
|
|
|
|
|
|
app.description,
|
|
|
|
|
|
info.package_name,
|
|
|
|
|
|
info.web_url,
|
|
|
|
|
|
info.apk_url,
|
|
|
|
|
|
info.version_name,
|
|
|
|
|
|
].some((field) => String(field || '').toLowerCase().includes(query));
|
|
|
|
|
|
}), [apps, filterAppType, filterStatus, searchQuery]);
|
|
|
|
|
|
|
|
|
|
|
|
const nativeCount = useMemo(() => apps.filter((app) => app.app_type === 'native').length, [apps]);
|
|
|
|
|
|
const webCount = useMemo(() => apps.filter((app) => app.app_type === 'web').length, [apps]);
|
|
|
|
|
|
const activeCount = useMemo(() => apps.filter((app) => app.is_active === 1 || app.is_active === true).length, [apps]);
|
|
|
|
|
|
const iconMissingCount = useMemo(() => apps.filter((app) => !app.icon_url).length, [apps]);
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
2026-02-12 07:34:12 +00:00
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
2026-03-26 06:55:12 +00:00
|
|
|
|
title: '应用',
|
|
|
|
|
|
key: 'app',
|
|
|
|
|
|
width: 240,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Avatar shape="square" size={42} src={record.icon_url} icon={<AppstoreOutlined />} />
|
|
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Text strong>{record.app_name}</Text>
|
|
|
|
|
|
<Text type="secondary">{record.description || '暂无描述'}</Text>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '类型',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
dataIndex: 'app_type',
|
2026-02-12 07:34:12 +00:00
|
|
|
|
key: 'app_type',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width: 120,
|
|
|
|
|
|
render: (type) => (
|
|
|
|
|
|
type === 'native'
|
|
|
|
|
|
? <Tag icon={<RobotOutlined />} color="green">原生应用</Tag>
|
|
|
|
|
|
: <Tag icon={<GlobalOutlined />} color="blue">Web 应用</Tag>
|
|
|
|
|
|
),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-26 06:55:12 +00:00
|
|
|
|
title: '入口信息',
|
2026-02-12 07:34:12 +00:00
|
|
|
|
key: 'info',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
render: (_, record) => {
|
|
|
|
|
|
const info = parseAppInfo(record.app_info);
|
2026-02-12 07:34:12 +00:00
|
|
|
|
return (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Space direction="vertical" size={0}>
|
|
|
|
|
|
<Text type="secondary">版本:{info.version_name || '-'}</Text>
|
|
|
|
|
|
{record.app_type === 'native' ? (
|
|
|
|
|
|
<Text type="secondary" ellipsis style={{ maxWidth: 220 }}>
|
|
|
|
|
|
包名:{info.package_name || '-'}
|
|
|
|
|
|
</Text>
|
2026-02-12 07:34:12 +00:00
|
|
|
|
) : (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Text type="secondary" ellipsis style={{ maxWidth: 240 }}>
|
|
|
|
|
|
地址:{info.web_url || '-'}
|
|
|
|
|
|
</Text>
|
2026-02-12 07:34:12 +00:00
|
|
|
|
)}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Space>
|
2026-02-12 07:34:12 +00:00
|
|
|
|
);
|
2026-03-26 06:55:12 +00:00
|
|
|
|
},
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
key: 'status',
|
2026-03-26 09:32:31 +00:00
|
|
|
|
width: 110,
|
2026-03-26 06:55:12 +00:00
|
|
|
|
render: (_, record) => (
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Switch
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
checked={record.is_active === 1 || record.is_active === true}
|
|
|
|
|
|
onChange={(checked) => handleToggleStatus(record, checked)}
|
|
|
|
|
|
/>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
),
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-03-26 06:55:12 +00:00
|
|
|
|
title: '排序',
|
|
|
|
|
|
dataIndex: 'sort_order',
|
|
|
|
|
|
key: 'sort_order',
|
|
|
|
|
|
align: 'center',
|
|
|
|
|
|
width: 80,
|
2026-02-12 07:34:12 +00:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
fixed: 'right',
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space size="middle">
|
|
|
|
|
|
<Tooltip title="打开入口">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
2026-03-26 09:32:31 +00:00
|
|
|
|
className="btn-text-view"
|
2026-03-26 06:55:12 +00:00
|
|
|
|
icon={<ExportOutlined />}
|
|
|
|
|
|
href={getAppEntryUrl(record)}
|
|
|
|
|
|
target="_blank"
|
|
|
|
|
|
disabled={!getAppEntryUrl(record)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="编辑">
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Button type="text" className="btn-text-edit" icon={<EditOutlined />} onClick={() => handleOpenModal(record)} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="删除">
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Button type="text" danger className="btn-text-delete" icon={<DeleteOutlined />} onClick={() => handleDelete(record)} />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-02-12 07:34:12 +00:00
|
|
|
|
];
|
2026-01-19 11:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<div className="external-app-management">
|
|
|
|
|
|
<AdminModuleShell
|
|
|
|
|
|
icon={<AppstoreOutlined style={{ fontSize: 18, color: '#1d4ed8' }} />}
|
|
|
|
|
|
title="外部应用管理"
|
|
|
|
|
|
subtitle="统一维护原生应用与 Web 应用入口,支持 APK 自动解析、图标上传和状态治理。"
|
|
|
|
|
|
rightActions={(
|
|
|
|
|
|
<Space>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Button icon={<ReloadOutlined />} className="btn-soft-blue" onClick={fetchApps} loading={loading}>刷新</Button>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>添加应用</Button>
|
|
|
|
|
|
</Space>
|
2026-02-12 07:34:12 +00:00
|
|
|
|
)}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
stats={[
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '应用总数',
|
|
|
|
|
|
value: apps.length,
|
|
|
|
|
|
icon: <AppstoreOutlined />,
|
|
|
|
|
|
tone: 'blue',
|
|
|
|
|
|
desc: '统一纳管的外部应用入口总量',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '原生应用',
|
|
|
|
|
|
value: nativeCount,
|
|
|
|
|
|
icon: <RobotOutlined />,
|
|
|
|
|
|
tone: 'green',
|
|
|
|
|
|
desc: '通过 APK 分发和安装的原生应用',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Web 应用',
|
|
|
|
|
|
value: webCount,
|
|
|
|
|
|
icon: <GlobalOutlined />,
|
|
|
|
|
|
tone: 'cyan',
|
|
|
|
|
|
desc: '通过浏览器访问的外部系统入口',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '缺少图标',
|
|
|
|
|
|
value: iconMissingCount,
|
|
|
|
|
|
icon: <PictureOutlined />,
|
|
|
|
|
|
tone: 'amber',
|
|
|
|
|
|
desc: '建议补齐图标以统一系统视觉入口',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '已启用',
|
|
|
|
|
|
value: activeCount,
|
|
|
|
|
|
icon: activeCount > 0 ? <CheckCircleOutlined /> : <BlockOutlined />,
|
|
|
|
|
|
tone: 'violet',
|
|
|
|
|
|
desc: '当前对用户可见并允许访问的应用数量',
|
|
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
toolbar={(
|
|
|
|
|
|
<Space wrap>
|
|
|
|
|
|
<Select value={filterAppType} onChange={setFilterAppType} style={{ width: 140 }} options={APP_TYPE_OPTIONS} />
|
|
|
|
|
|
<Select value={filterStatus} onChange={setFilterStatus} style={{ width: 140 }} options={STATUS_OPTIONS} />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索名称、描述、包名或入口..."
|
|
|
|
|
|
prefix={<SearchOutlined />}
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
|
style={{ width: 280 }}
|
|
|
|
|
|
allowClear
|
2026-02-12 07:34:12 +00:00
|
|
|
|
/>
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="console-table">
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={filteredApps}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
scroll={{ x: 980 }}
|
|
|
|
|
|
pagination={{ pageSize: 10, showTotal: (count) => `共 ${count} 条记录` }}
|
2026-02-12 07:34:12 +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"
|
2026-03-26 06:55:12 +00:00
|
|
|
|
width={680}
|
2026-03-26 09:32:31 +00:00
|
|
|
|
onClose={() => setShowDrawer(false)}
|
|
|
|
|
|
destroyOnClose
|
|
|
|
|
|
extra={(
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button type="primary" icon={<SaveOutlined />} onClick={() => form.submit()}>
|
|
|
|
|
|
{isEditing ? '保存修改' : '创建应用'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
2026-03-26 06:55:12 +00:00
|
|
|
|
>
|
|
|
|
|
|
<Form form={form} layout="vertical" style={{ marginTop: 20 }} onFinish={handleSave}>
|
|
|
|
|
|
<Form.Item name="app_type" label="应用类型" rules={[{ required: true, message: '请选择应用类型' }]}>
|
|
|
|
|
|
<Select>
|
|
|
|
|
|
<Select.Option value="native">原生应用(Android APK)</Select.Option>
|
|
|
|
|
|
<Select.Option value="web">Web 应用</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.app_type !== curr.app_type}>
|
|
|
|
|
|
{({ getFieldValue }) => (
|
|
|
|
|
|
getFieldValue('app_type') === 'native' ? (
|
|
|
|
|
|
<div style={{ marginBottom: 16 }}>
|
|
|
|
|
|
<Upload customRequest={(options) => handleFileUpload(options, 'apk')} showUploadList={false}>
|
|
|
|
|
|
<Button icon={<UploadOutlined />} loading={uploading}>上传并自动解析 APK</Button>
|
|
|
|
|
|
</Upload>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Space align="start" style={{ width: '100%' }}>
|
|
|
|
|
|
<Form.Item label="应用图标">
|
|
|
|
|
|
<Upload customRequest={(options) => handleFileUpload(options, 'icon')} showUploadList={false}>
|
|
|
|
|
|
<div style={{ cursor: 'pointer' }}>
|
|
|
|
|
|
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.icon_url !== curr.icon_url}>
|
|
|
|
|
|
{({ getFieldValue }) => (
|
|
|
|
|
|
<Avatar
|
|
|
|
|
|
size={64}
|
|
|
|
|
|
shape="square"
|
|
|
|
|
|
src={getFieldValue('icon_url')}
|
|
|
|
|
|
icon={getFieldValue('icon_url') ? <PictureOutlined /> : <PlusOutlined />}
|
|
|
|
|
|
style={{ border: '1px dashed #d9d9d9', backgroundColor: '#fff' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Upload>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
<Form.Item name="icon_url" hidden>
|
|
|
|
|
|
<Input />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ flex: 1 }}>
|
|
|
|
|
|
<Form.Item name="app_name" label="应用名称" rules={[{ required: true, message: '请输入应用名称' }]}>
|
|
|
|
|
|
<Input placeholder="请输入应用名称" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.app_type !== curr.app_type}>
|
|
|
|
|
|
{({ getFieldValue }) => (
|
|
|
|
|
|
getFieldValue('app_type') === 'native' ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name={['app_info', 'version_name']}
|
|
|
|
|
|
label="版本名称"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入版本名称' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="例如 1.0.0" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={16}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name={['app_info', 'package_name']}
|
|
|
|
|
|
label="应用包名"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入应用包名' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="com.example.app" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name={['app_info', 'apk_url']}
|
|
|
|
|
|
label="APK 下载链接"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入 APK 下载链接' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input prefix={<LinkOutlined />} placeholder="https://..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={8}>
|
|
|
|
|
|
<Form.Item name={['app_info', 'version_name']} label="版本名称">
|
|
|
|
|
|
<Input placeholder="可选,例如 1.2.0" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={16}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name={['app_info', 'web_url']}
|
|
|
|
|
|
label="Web 应用 URL"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入 URL' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input prefix={<GlobalOutlined />} placeholder="https://..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="description" label="应用描述">
|
|
|
|
|
|
<TextArea rows={2} placeholder="简单描述该应用的用途..." />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="sort_order" label="排序权重">
|
|
|
|
|
|
<InputNumber style={{ width: '100%' }} min={0} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item name="is_active" label="状态" valuePropName="checked">
|
2026-03-26 09:32:31 +00:00
|
|
|
|
<Switch />
|
2026-03-26 06:55:12 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</Form>
|
2026-03-26 09:32:31 +00:00
|
|
|
|
</Drawer>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default ExternalAppManagement;
|