import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Table, Button, Input, Space, Drawer, Form, Select, App, Tooltip, Switch, InputNumber, Upload, Tag, Avatar, Row, Col, Typography, } from 'antd'; import { PlusOutlined, SaveOutlined, DeleteOutlined, EditOutlined, SearchOutlined, GlobalOutlined, RobotOutlined, UploadOutlined, LinkOutlined, AppstoreOutlined, ReloadOutlined, ExportOutlined, PictureOutlined, CheckCircleOutlined, BlockOutlined, } from '@ant-design/icons'; import httpService from '../../services/httpService'; import { buildApiUrl, API_ENDPOINTS } from '../../config/api'; import AdminModuleShell from '../../components/AdminModuleShell'; import ActionButton from '../../components/ActionButton'; import useSystemPageSize from '../../hooks/useSystemPageSize'; 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(); const pageSize = useSystemPageSize(10); const [apps, setApps] = useState([]); const [loading, setLoading] = useState(true); const [showDrawer, setShowDrawer] = useState(false); const [isEditing, setIsEditing] = useState(false); const [selectedApp, setSelectedApp] = useState(null); const [filterAppType, setFilterAppType] = useState('all'); const [filterStatus, setFilterStatus] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [uploading, setUploading] = useState(false); const fetchApps = useCallback(async () => { setLoading(true); try { const response = await httpService.get(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.LIST)); if (response.code === '200') { setApps(response.data || []); } } catch { message.error('获取外部应用列表失败'); } finally { setLoading(false); } }, [message]); useEffect(() => { fetchApps(); }, [fetchApps]); 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: {}, }); } setShowDrawer(true); }; const handleSave = async () => { try { 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 httpService.put(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.UPDATE(selectedApp.id)), payload); message.success('应用更新成功'); } else { await httpService.post(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.CREATE), payload); message.success('应用创建成功'); } setShowDrawer(false); fetchApps(); } catch (error) { if (!error.errorFields) { message.error('保存失败'); } } }; const handleDelete = (item) => { modal.confirm({ title: '删除外部应用', content: `确定要删除应用“${item.app_name}”吗?此操作不可恢复。`, okText: '确定', okType: 'danger', onOk: async () => { try { await httpService.delete(buildApiUrl(API_ENDPOINTS.EXTERNAL_APPS.DELETE(item.id))); message.success('删除成功'); fetchApps(); } catch { message.error('删除失败'); } }, }); }; const handleFileUpload = async (options, type) => { const { file, onSuccess, onError } = options; setUploading(true); 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); try { const response = await httpService.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('应用图标上传成功'); } onSuccess(response.data); } } catch (error) { message.error('上传失败'); onError(error); } finally { setUploading(false); } }; const handleToggleStatus = async (item, checked) => { try { const newActive = checked ? 1 : 0; await httpService.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('状态更新失败'); } }; const filteredApps = useMemo(() => apps.filter((app) => { if (filterAppType !== 'all' && app.app_type !== filterAppType) { return false; } const enabled = app.is_active === 1 || app.is_active === true; if (filterStatus === 'active' && !enabled) { return false; } if (filterStatus === 'inactive' && enabled) { return false; } if (!searchQuery) { return true; } 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]); const columns = [ { title: '应用', key: 'app', width: 240, render: (_, record) => ( } /> {record.app_name} {record.description || '暂无描述'} ), }, { title: '类型', dataIndex: 'app_type', key: 'app_type', width: 120, render: (type) => ( type === 'native' ? } color="green">原生应用 : } color="blue">Web 应用 ), }, { title: '入口信息', key: 'info', render: (_, record) => { const info = parseAppInfo(record.app_info); return ( 版本:{info.version_name || '-'} {record.app_type === 'native' ? ( 包名:{info.package_name || '-'} ) : ( 地址:{info.web_url || '-'} )} ); }, }, { title: '状态', key: 'status', width: 110, render: (_, record) => ( handleToggleStatus(record, checked)} /> ), }, { title: '排序', dataIndex: 'sort_order', key: 'sort_order', align: 'center', width: 80, }, { title: '操作', key: 'action', fixed: 'right', width: 140, render: (_, record) => ( } href={getAppEntryUrl(record)} target="_blank" disabled={!getAppEntryUrl(record)} /> } onClick={() => handleOpenModal(record)} /> } onClick={() => handleDelete(record)} /> ), }, ]; return (
} title="外部应用管理" subtitle="统一维护原生应用与 Web 应用入口,支持 APK 自动解析、图标上传和状态治理。" rightActions={( )} stats={[ { label: '应用总数', value: apps.length, icon: , tone: 'blue', desc: '统一纳管的外部应用入口总量', }, { label: '原生应用', value: nativeCount, icon: , tone: 'green', desc: '通过 APK 分发和安装的原生应用', }, { label: 'Web 应用', value: webCount, icon: , tone: 'cyan', desc: '通过浏览器访问的外部系统入口', }, { label: '缺少图标', value: iconMissingCount, icon: , tone: 'amber', desc: '建议补齐图标以统一系统视觉入口', }, { label: '已启用', value: activeCount, icon: activeCount > 0 ? : , tone: 'violet', desc: '当前对用户可见并允许访问的应用数量', }, ]} toolbar={( } value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} style={{ width: 280 }} allowClear /> )} >
`共 ${count} 条记录` }} /> setShowDrawer(false)} destroyOnClose extra={( )} >
prev.app_type !== curr.app_type}> {({ getFieldValue }) => ( getFieldValue('app_type') === 'native' ? (
handleFileUpload(options, 'apk')} showUploadList={false}>
) : null )}
handleFileUpload(options, 'icon')} showUploadList={false}>
prev.icon_url !== curr.icon_url}> {({ getFieldValue }) => ( : } style={{ border: '1px dashed #d9d9d9', backgroundColor: '#fff' }} /> )}
prev.app_type !== curr.app_type}> {({ getFieldValue }) => ( getFieldValue('app_type') === 'native' ? ( <>
} placeholder="https://..." /> ) : ( <> } placeholder="https://..." /> ) )}