refactor: 统一使用全局租户ID并优化客户端和外部应用管理页面
- 在 `ClientDownloadServiceImpl` 和 `ExternalAppServiceImpl` 中使用 `GLOBAL_TENANT_ID` - 移除 `requireOwned` 方法中的租户ID检查,更新为 `requireExisting` - 更新前端 `ClientManagement` 和 `ExternalAppManagement` 页面,优化平台选项和状态过滤 - 添加数据字典驱动的平台分组和选项加载逻辑 - 修复前端组件中的字段名称和代理配置问题dev_na
parent
3b7ba2c47a
commit
b2430abe73
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue