refactor: 统一使用全局租户ID并优化客户端和外部应用管理页面

- 在 `ClientDownloadServiceImpl` 和 `ExternalAppServiceImpl` 中使用 `GLOBAL_TENANT_ID`
- 移除 `requireOwned` 方法中的租户ID检查,更新为 `requireExisting`
- 更新前端 `ClientManagement` 和 `ExternalAppManagement` 页面,优化平台选项和状态过滤
- 添加数据字典驱动的平台分组和选项加载逻辑
- 修复前端组件中的字段名称和代理配置问题
dev_na
chenhao 2026-04-14 11:17:25 +08:00
parent 3b7ba2c47a
commit b2430abe73
5 changed files with 159 additions and 70 deletions

View File

@ -23,13 +23,14 @@ import java.nio.file.StandardCopyOption;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper, ClientDownload> implements ClientDownloadService { public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper, ClientDownload> implements ClientDownloadService {
private static final long GLOBAL_TENANT_ID = 0L;
@Value("${unisbase.app.upload-path}") @Value("${unisbase.app.upload-path}")
private String uploadPath; private String uploadPath;
@ -39,7 +40,6 @@ public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper,
@Override @Override
public List<ClientDownload> listForAdmin(LoginUser loginUser, String platformCode, Integer status) { public List<ClientDownload> listForAdmin(LoginUser loginUser, String platformCode, Integer status) {
LambdaQueryWrapper<ClientDownload> wrapper = new LambdaQueryWrapper<ClientDownload>() LambdaQueryWrapper<ClientDownload> wrapper = new LambdaQueryWrapper<ClientDownload>()
.eq(ClientDownload::getTenantId, loginUser.getTenantId())
.orderByAsc(ClientDownload::getPlatformType) .orderByAsc(ClientDownload::getPlatformType)
.orderByDesc(ClientDownload::getVersionCode) .orderByDesc(ClientDownload::getVersionCode)
.orderByDesc(ClientDownload::getId); .orderByDesc(ClientDownload::getId);
@ -58,7 +58,7 @@ public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper,
validate(dto, false); validate(dto, false);
ClientDownload entity = new ClientDownload(); ClientDownload entity = new ClientDownload();
applyDto(entity, dto, false); applyDto(entity, dto, false);
entity.setTenantId(loginUser.getTenantId()); entity.setTenantId(GLOBAL_TENANT_ID);
entity.setCreatedBy(loginUser.getUserId()); entity.setCreatedBy(loginUser.getUserId());
if (entity.getStatus() == null) { if (entity.getStatus() == null) {
entity.setStatus(1); entity.setStatus(1);
@ -74,8 +74,9 @@ public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper,
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public ClientDownload update(Long id, ClientDownloadDTO dto, LoginUser loginUser) { public ClientDownload update(Long id, ClientDownloadDTO dto, LoginUser loginUser) {
ClientDownload entity = requireOwned(id, loginUser); ClientDownload entity = requireExisting(id);
applyDto(entity, dto, true); applyDto(entity, dto, true);
entity.setTenantId(GLOBAL_TENANT_ID);
clearLatestFlagIfNeeded(entity.getTenantId(), entity.getPlatformCode(), entity.getIsLatest(), entity.getId()); clearLatestFlagIfNeeded(entity.getTenantId(), entity.getPlatformCode(), entity.getIsLatest(), entity.getId());
this.updateById(entity); this.updateById(entity);
return entity; return entity;
@ -84,7 +85,7 @@ public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper,
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void removeClient(Long id, LoginUser loginUser) { public void removeClient(Long id, LoginUser loginUser) {
ClientDownload entity = requireOwned(id, loginUser); ClientDownload entity = requireExisting(id);
this.removeById(entity.getId()); this.removeById(entity.getId());
} }
@ -188,9 +189,9 @@ public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper,
this.update(update); this.update(update);
} }
private ClientDownload requireOwned(Long id, LoginUser loginUser) { private ClientDownload requireExisting(Long id) {
ClientDownload entity = this.getById(id); ClientDownload entity = this.getById(id);
if (entity == null || !Objects.equals(entity.getTenantId(), loginUser.getTenantId())) { if (entity == null) {
throw new RuntimeException("Client version not found"); throw new RuntimeException("Client version not found");
} }
return entity; return entity;
@ -223,4 +224,4 @@ public class ClientDownloadServiceImpl extends ServiceImpl<ClientDownloadMapper,
String trimmed = value.trim(); String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed; return trimmed.isEmpty() ? null : trimmed;
} }
} }

View File

@ -35,6 +35,8 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, ExternalApp> implements ExternalAppService { public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, ExternalApp> implements ExternalAppService {
private static final long GLOBAL_TENANT_ID = 0L;
private final SysUserMapper sysUserMapper; private final SysUserMapper sysUserMapper;
@Value("${unisbase.app.upload-path}") @Value("${unisbase.app.upload-path}")
@ -46,7 +48,6 @@ public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, Exter
@Override @Override
public List<Map<String, Object>> listForAdmin(LoginUser loginUser, String appType, Integer status) { public List<Map<String, Object>> listForAdmin(LoginUser loginUser, String appType, Integer status) {
LambdaQueryWrapper<ExternalApp> wrapper = new LambdaQueryWrapper<ExternalApp>() LambdaQueryWrapper<ExternalApp> wrapper = new LambdaQueryWrapper<ExternalApp>()
.eq(ExternalApp::getTenantId, loginUser.getTenantId())
.orderByAsc(ExternalApp::getSortOrder) .orderByAsc(ExternalApp::getSortOrder)
.orderByDesc(ExternalApp::getId); .orderByDesc(ExternalApp::getId);
if (appType != null && !appType.isBlank()) { if (appType != null && !appType.isBlank()) {
@ -92,7 +93,7 @@ public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, Exter
validate(dto, false); validate(dto, false);
ExternalApp entity = new ExternalApp(); ExternalApp entity = new ExternalApp();
applyDto(entity, dto, false); applyDto(entity, dto, false);
entity.setTenantId(loginUser.getTenantId()); entity.setTenantId(GLOBAL_TENANT_ID);
entity.setCreatedBy(loginUser.getUserId()); entity.setCreatedBy(loginUser.getUserId());
if (entity.getStatus() == null) { if (entity.getStatus() == null) {
entity.setStatus(1); entity.setStatus(1);
@ -104,8 +105,9 @@ public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, Exter
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public ExternalApp update(Long id, ExternalAppDTO dto, LoginUser loginUser) { public ExternalApp update(Long id, ExternalAppDTO dto, LoginUser loginUser) {
ExternalApp entity = requireOwned(id, loginUser); ExternalApp entity = requireExisting(id);
applyDto(entity, dto, true); applyDto(entity, dto, true);
entity.setTenantId(GLOBAL_TENANT_ID);
this.updateById(entity); this.updateById(entity);
return entity; return entity;
} }
@ -113,7 +115,7 @@ public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, Exter
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void removeApp(Long id, LoginUser loginUser) { public void removeApp(Long id, LoginUser loginUser) {
ExternalApp entity = requireOwned(id, loginUser); ExternalApp entity = requireExisting(id);
this.removeById(entity.getId()); this.removeById(entity.getId());
} }
@ -179,9 +181,9 @@ public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, Exter
} }
} }
private ExternalApp requireOwned(Long id, LoginUser loginUser) { private ExternalApp requireExisting(Long id) {
ExternalApp entity = this.getById(id); ExternalApp entity = this.getById(id);
if (entity == null || !Objects.equals(entity.getTenantId(), loginUser.getTenantId())) { if (entity == null) {
throw new RuntimeException("External app not found"); throw new RuntimeException("External app not found");
} }
return entity; return entity;
@ -240,4 +242,4 @@ public class ExternalAppServiceImpl extends ServiceImpl<ExternalAppMapper, Exter
private record StoredFile(Path path, String url, long size) { private record StoredFile(Path path, String url, long size) {
} }
} }

View File

@ -41,6 +41,8 @@ unisbase:
- biz_llm_models - biz_llm_models
- biz_asr_models - biz_asr_models
- biz_prompt_templates - biz_prompt_templates
- biz_client_downloads
- biz_external_apps
security: security:
enabled: true enabled: true
mode: embedded mode: embedded

View File

@ -1,13 +1,24 @@
import { App, Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tabs, Tag, Typography, Upload } from "antd"; import { App, Button, Card, Col, Drawer, Empty, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tabs, Tag, Typography, Upload } from "antd";
import type { ColumnsType } from "antd/es/table"; import type { ColumnsType } from "antd/es/table";
import { CloudUploadOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, LaptopOutlined, MobileOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, SaveOutlined, UploadOutlined } from "@ant-design/icons"; import { CloudUploadOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, LaptopOutlined, MobileOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import PageHeader from "@/components/shared/PageHeader"; import PageHeader from "@/components/shared/PageHeader";
import { getStandardPagination } from "@/utils/pagination"; import { getStandardPagination } from "@/utils/pagination";
import { createClientDownload, deleteClientDownload, listClientDownloads, type ClientDownloadDTO, type ClientDownloadVO, updateClientDownload, uploadClientPackage } from "@/api/business/client"; import { createClientDownload, deleteClientDownload, listClientDownloads, type ClientDownloadDTO, type ClientDownloadVO, updateClientDownload, uploadClientPackage } from "@/api/business/client";
import { fetchDictItemsByTypeCode } from "@/api/dict";
import { useDict } from "@/hooks/useDict";
import type { SysDictItem } from "@/types";
const { Text } = Typography; const { Text } = Typography;
const { TextArea } = Input; const { TextArea } = Input;
const CLIENT_PLATFORM_DICT = "client_platform";
const STATUS_FILTER_OPTIONS = [
{ label: "全部状态", value: "all" },
{ label: "已启用", value: "enabled" },
{ label: "已停用", value: "disabled" },
{ label: "最新版本", value: "latest" },
] as const;
type ClientFormValues = { type ClientFormValues = {
platformCode: string; platformCode: string;
@ -22,45 +33,53 @@ type ClientFormValues = {
remark?: string; remark?: string;
}; };
type ClientPlatformType = "mobile" | "desktop" | "terminal";
type ClientPlatformOption = { type ClientPlatformOption = {
label: string; label: string;
value: string; value: string;
platformType: ClientPlatformType; childTypeCode: string;
platformType: string;
platformName: string; platformName: string;
sortOrder: number;
}; };
const PLATFORM_OPTIONS: ClientPlatformOption[] = [ type ClientPlatformGroup = {
{ label: "Android", value: "android", platformType: "mobile", platformName: "android" }, key: string;
{ label: "iOS", value: "ios", platformType: "mobile", platformName: "ios" }, label: string;
{ label: "Windows", value: "windows", platformType: "desktop", platformName: "windows" }, childTypeCode: string;
{ label: "macOS", value: "mac", platformType: "desktop", platformName: "mac" }, sortOrder: number;
{ label: "Linux", value: "linux", platformType: "desktop", platformName: "linux" }, options: ClientPlatformOption[];
{ label: "专用终端", value: "terminal_android", platformType: "terminal", platformName: "android" }, };
];
const PLATFORM_GROUPS: Array<{ key: ClientPlatformType; label: string }> = [
{ key: "mobile", label: "移动端" },
{ key: "desktop", label: "桌面端" },
{ key: "terminal", label: "专用终端" },
];
const STATUS_FILTER_OPTIONS = [
{ label: "全部状态", value: "all" },
{ label: "已启用", value: "enabled" },
{ label: "已停用", value: "disabled" },
{ label: "最新版本", value: "latest" },
] as const;
function formatFileSize(fileSize?: number) { function formatFileSize(fileSize?: number) {
if (!fileSize) return "-"; if (!fileSize) return "-";
return `${(fileSize / (1024 * 1024)).toFixed(2)} MB`; return `${(fileSize / (1024 * 1024)).toFixed(2)} MB`;
} }
function normalizeStatus(item: SysDictItem) {
return item.status === undefined || item.status === 1;
}
function derivePlatformType(childTypeCode: string) {
return childTypeCode.startsWith("client_platform_")
? childTypeCode.slice("client_platform_".length)
: childTypeCode;
}
function derivePlatformName(platformType: string, itemValue: string) {
const normalized = (itemValue || "").trim().toLowerCase();
const prefix = `${platformType}_`;
if (normalized.startsWith(prefix)) {
return normalized.slice(prefix.length);
}
return normalized;
}
export default function ClientManagement() { export default function ClientManagement() {
const { message } = App.useApp(); const { message } = App.useApp();
const [form] = Form.useForm<ClientFormValues>(); const [form] = Form.useForm<ClientFormValues>();
const { items: platformGroupItems, loading: groupLoading } = useDict(CLIENT_PLATFORM_DICT);
const [platformChildren, setPlatformChildren] = useState<Record<string, SysDictItem[]>>({});
const [platformLoading, setPlatformLoading] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
@ -68,14 +87,72 @@ export default function ClientManagement() {
const [editing, setEditing] = useState<ClientDownloadVO | null>(null); const [editing, setEditing] = useState<ClientDownloadVO | null>(null);
const [records, setRecords] = useState<ClientDownloadVO[]>([]); const [records, setRecords] = useState<ClientDownloadVO[]>([]);
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const [statusFilter, setStatusFilter] = useState<(typeof STATUS_FILTER_OPTIONS)[number]["value"]>("all"); const [statusFilter, setStatusFilter] = useState<"all" | "enabled" | "disabled" | "latest">("all");
const [activeTab, setActiveTab] = useState<ClientPlatformType | "all">("all"); const [activeTab, setActiveTab] = useState<string>("all");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
useEffect(() => {
let active = true;
const loadChildren = async () => {
const childTypeCodes = Array.from(new Set(platformGroupItems.map((item) => item.itemValue).filter(Boolean)));
if (childTypeCodes.length === 0) {
setPlatformChildren({});
return;
}
setPlatformLoading(true);
try {
const entries = await Promise.all(
childTypeCodes.map(async (typeCode) => [typeCode, await fetchDictItemsByTypeCode(typeCode)] as const)
);
if (!active) return;
setPlatformChildren(Object.fromEntries(entries));
} finally {
if (active) {
setPlatformLoading(false);
}
}
};
void loadChildren();
return () => {
active = false;
};
}, [platformGroupItems]);
const platformGroups = useMemo<ClientPlatformGroup[]>(() => {
return platformGroupItems
.filter(normalizeStatus)
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
.map((group) => {
const childTypeCode = group.itemValue;
const platformType = derivePlatformType(childTypeCode);
const options = (platformChildren[childTypeCode] || [])
.filter(normalizeStatus)
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
.map((item) => ({
label: item.itemLabel,
value: item.itemValue,
childTypeCode,
platformType,
platformName: derivePlatformName(platformType, item.itemValue),
sortOrder: item.sortOrder ?? 0,
}));
return {
key: platformType,
label: group.itemLabel,
childTypeCode,
sortOrder: group.sortOrder ?? 0,
options,
};
})
.filter((group) => group.options.length > 0);
}, [platformChildren, platformGroupItems]);
const platformMap = useMemo( const platformMap = useMemo(
() => Object.fromEntries(PLATFORM_OPTIONS.map((item) => [item.value, item])) as Record<string, ClientPlatformOption>, () => Object.fromEntries(platformGroups.flatMap((group) => group.options.map((option) => [option.value, option]))) as Record<string, ClientPlatformOption>,
[] [platformGroups]
); );
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
@ -96,7 +173,7 @@ export default function ClientManagement() {
const keyword = searchValue.trim().toLowerCase(); const keyword = searchValue.trim().toLowerCase();
return records.filter((item) => { return records.filter((item) => {
const platform = platformMap[item.platformCode]; const platform = platformMap[item.platformCode];
const platformType = item.platformType || platform?.platformType; const platformType = item.platformType || platform?.platformType || "ungrouped";
if (activeTab !== "all" && platformType !== activeTab) { if (activeTab !== "all" && platformType !== activeTab) {
return false; return false;
} }
@ -109,9 +186,7 @@ export default function ClientManagement() {
if (statusFilter === "latest" && item.isLatest !== 1) { if (statusFilter === "latest" && item.isLatest !== 1) {
return false; return false;
} }
if (!keyword) { if (!keyword) return true;
return true;
}
return [item.version, item.platformCode, platform?.label, item.minSystemVersion, item.downloadUrl, item.releaseNotes].some((field) => return [item.version, item.platformCode, platform?.label, item.minSystemVersion, item.downloadUrl, item.releaseNotes].some((field) =>
String(field || "").toLowerCase().includes(keyword) String(field || "").toLowerCase().includes(keyword)
); );
@ -131,14 +206,19 @@ export default function ClientManagement() {
total: records.length, total: records.length,
enabled: records.filter((item) => item.status === 1).length, enabled: records.filter((item) => item.status === 1).length,
latest: records.filter((item) => item.isLatest === 1).length, latest: records.filter((item) => item.isLatest === 1).length,
terminal: records.filter((item) => (item.platformType || platformMap[item.platformCode]?.platformType) === "terminal").length, groups: platformGroups.length,
}), [platformMap, records]); }), [platformGroups.length, records]);
const openCreate = () => { const openCreate = () => {
const firstOption = platformGroups[0]?.options[0];
if (!firstOption) {
message.warning("当前未配置客户端发布平台字典,请先在字典管理中维护 client_platform 及其下级类型");
return;
}
setEditing(null); setEditing(null);
form.resetFields(); form.resetFields();
form.setFieldsValue({ form.setFieldsValue({
platformCode: PLATFORM_OPTIONS[0].value, platformCode: firstOption.value,
statusEnabled: true, statusEnabled: true,
latest: false, latest: false,
}); });
@ -161,6 +241,7 @@ export default function ClientManagement() {
}); });
setDrawerOpen(true); setDrawerOpen(true);
}; };
const handleDelete = async (record: ClientDownloadVO) => { const handleDelete = async (record: ClientDownloadVO) => {
await deleteClientDownload(record.id); await deleteClientDownload(record.id);
message.success("删除成功"); message.success("删除成功");
@ -170,10 +251,15 @@ export default function ClientManagement() {
const handleSubmit = async () => { const handleSubmit = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
const platform = platformMap[values.platformCode]; const platform = platformMap[values.platformCode];
if (!platform) {
message.error("未找到所选平台字典项,请刷新后重试");
return;
}
const payload: ClientDownloadDTO = { const payload: ClientDownloadDTO = {
platformCode: values.platformCode, platformCode: values.platformCode,
platformType: platform?.platformType, platformType: platform.platformType,
platformName: platform?.platformName, platformName: platform.platformName,
version: values.version.trim(), version: values.version.trim(),
versionCode: values.versionCode, versionCode: values.versionCode,
downloadUrl: values.downloadUrl.trim(), downloadUrl: values.downloadUrl.trim(),
@ -234,10 +320,7 @@ export default function ClientManagement() {
dataIndex: "platformCode", dataIndex: "platformCode",
key: "platformCode", key: "platformCode",
width: 150, width: 150,
render: (value: string) => { render: (value: string) => <Tag color="blue">{platformMap[value]?.label || value}</Tag>,
const platform = platformMap[value];
return <Tag color="blue">{platform?.label || value}</Tag>;
},
}, },
{ {
title: "版本信息", title: "版本信息",
@ -300,10 +383,10 @@ export default function ClientManagement() {
<div style={{ height: "100%", display: "flex", flexDirection: "column", overflow: "hidden" }}> <div style={{ height: "100%", display: "flex", flexDirection: "column", overflow: "hidden" }}>
<PageHeader <PageHeader
title="客户端管理" title="客户端管理"
subtitle="统一维护移动端、桌面端与专用终端的版本发布、安装包上传和启停状态。" subtitle="发布平台由数据字典 client_platform 驱动,并按父子分组展示。"
extra={ extra={
<Space> <Space>
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}></Button> <Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading || groupLoading || platformLoading}></Button>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}></Button> <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}></Button>
</Space> </Space>
} }
@ -313,7 +396,7 @@ export default function ClientManagement() {
<Col span={6}><Card size="small"><Space><CloudUploadOutlined style={{ color: "#1677ff" }} /><div><div></div><Text strong>{stats.total}</Text></div></Space></Card></Col> <Col span={6}><Card size="small"><Space><CloudUploadOutlined style={{ color: "#1677ff" }} /><div><div></div><Text strong>{stats.total}</Text></div></Space></Card></Col>
<Col span={6}><Card size="small"><Space><LaptopOutlined style={{ color: "#52c41a" }} /><div><div></div><Text strong>{stats.enabled}</Text></div></Space></Card></Col> <Col span={6}><Card size="small"><Space><LaptopOutlined style={{ color: "#52c41a" }} /><div><div></div><Text strong>{stats.enabled}</Text></div></Space></Card></Col>
<Col span={6}><Card size="small"><Space><MobileOutlined style={{ color: "#722ed1" }} /><div><div></div><Text strong>{stats.latest}</Text></div></Space></Card></Col> <Col span={6}><Card size="small"><Space><MobileOutlined style={{ color: "#722ed1" }} /><div><div></div><Text strong>{stats.latest}</Text></div></Space></Card></Col>
<Col span={6}><Card size="small"><Space><LaptopOutlined style={{ color: "#fa8c16" }} /><div><div></div><Text strong>{stats.terminal}</Text></div></Space></Card></Col> <Col span={6}><Card size="small"><Space><LaptopOutlined style={{ color: "#fa8c16" }} /><div><div></div><Text strong>{stats.groups}</Text></div></Space></Card></Col>
</Row> </Row>
<Card className="app-page__filter-card mb-4" styles={{ body: { padding: 16 } }}> <Card className="app-page__filter-card mb-4" styles={{ body: { padding: 16 } }}>
@ -322,23 +405,23 @@ export default function ClientManagement() {
<Input placeholder="搜索平台、版本、系统要求或下载地址" prefix={<SearchOutlined />} allowClear style={{ width: 320 }} value={searchValue} onChange={(event) => setSearchValue(event.target.value)} /> <Input placeholder="搜索平台、版本、系统要求或下载地址" prefix={<SearchOutlined />} allowClear style={{ width: 320 }} value={searchValue} onChange={(event) => setSearchValue(event.target.value)} />
<Select style={{ width: 150 }} value={statusFilter} options={STATUS_FILTER_OPTIONS as unknown as { label: string; value: string }[]} onChange={(value) => setStatusFilter(value as typeof statusFilter)} /> <Select style={{ width: 150 }} value={statusFilter} options={STATUS_FILTER_OPTIONS as unknown as { label: string; value: string }[]} onChange={(value) => setStatusFilter(value as typeof statusFilter)} />
</Space> </Space>
<Tabs activeKey={activeTab} onChange={(value) => setActiveTab(value as ClientPlatformType | "all")} items={[{ key: "all", label: "全部" }, ...PLATFORM_GROUPS.map((item) => ({ key: item.key, label: item.label }))]} /> <Tabs activeKey={activeTab} onChange={setActiveTab} items={[{ key: "all", label: "全部" }, ...platformGroups.map((group) => ({ key: group.key, label: group.label }))]} />
</Space> </Space>
</Card> </Card>
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}> <Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
<Table rowKey="id" columns={columns} dataSource={pagedRecords} loading={loading} scroll={{ x: 960, y: "calc(100vh - 360px)" }} pagination={getStandardPagination(filteredRecords.length, page, pageSize, (nextPage, nextSize) => { setPage(nextPage); setPageSize(nextSize); })} /> <Table rowKey="id" columns={columns} dataSource={pagedRecords} loading={loading || groupLoading || platformLoading} locale={platformGroups.length === 0 ? { emptyText: <Empty description="未配置 client_platform 字典项" /> } : undefined} scroll={{ x: 960, y: "calc(100vh - 360px)" }} pagination={getStandardPagination(filteredRecords.length, page, pageSize, (nextPage, nextSize) => { setPage(nextPage); setPageSize(nextSize); })} />
</Card> </Card>
<Drawer title={editing ? "编辑客户端版本" : "新增客户端版本"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={680} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}></Button><Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => void handleSubmit()}></Button></div>}> <Drawer title={editing ? "编辑客户端版本" : "新增客户端版本"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={680} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}></Button><Button type="primary" icon={<UploadOutlined />} loading={saving} onClick={() => void handleSubmit()}></Button></div>}>
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Form.Item name="platformCode" label="发布平台" rules={[{ required: true, message: "请选择发布平台" }]}> <Form.Item name="platformCode" label="发布平台" rules={[{ required: true, message: "请选择发布平台" }]}>
<Select placeholder="选择平台" disabled={!!editing}> <Select placeholder="选择平台" disabled={!!editing || platformGroups.length === 0}>
{PLATFORM_GROUPS.map((group) => ( {platformGroups.map((group) => (
<Select.OptGroup key={group.key} label={group.label}> <Select.OptGroup key={group.childTypeCode} label={group.label}>
{PLATFORM_OPTIONS.filter((item) => item.platformType === group.key).map((item) => ( {group.options.map((item) => (
<Select.Option key={item.value} value={item.value}>{item.label}</Select.Option> <Select.Option key={item.value} value={item.value}>{item.label}</Select.Option>
))} ))}
</Select.OptGroup> </Select.OptGroup>

View File

@ -1,4 +1,4 @@
import { App, Avatar, Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tag, Typography, Upload } from "antd"; import { App, Avatar, Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tag, Typography, Upload } from "antd";
import type { ColumnsType } from "antd/es/table"; import type { ColumnsType } from "antd/es/table";
import { AppstoreOutlined, DeleteOutlined, EditOutlined, GlobalOutlined, LinkOutlined, PictureOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons"; import { AppstoreOutlined, DeleteOutlined, EditOutlined, GlobalOutlined, LinkOutlined, PictureOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
@ -144,8 +144,8 @@ export default function ExternalAppManagement() {
}; };
const handleDelete = async (record: ExternalAppVO) => { const handleDelete = async (record: ExternalAppVO) => {
await deleteExternalApp(record.id);
message.success("删除成功"); message.success("删除成功");
message.success("Deleted successfully");
await loadData(); await loadData();
}; };
@ -177,6 +177,7 @@ export default function ExternalAppManagement() {
setSaving(false); setSaving(false);
} }
}; };
const handleUploadApk = async (file: File) => { const handleUploadApk = async (file: File) => {
setUploadingApk(true); setUploadingApk(true);
try { try {