From 91e2c186c71771c3326655a26f501729dfc6c6c6 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 13 Aug 2026 06:28:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=BA=94=E7=94=A8=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E6=94=AF=E6=8C=81=E5=85=AC=E5=8F=B8=E7=BB=B4=E5=BA=A6?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E2=80=94=E2=80=94=E5=AE=89=E8=A3=85=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=8F=AF=E9=80=89=E5=85=A8=E7=A7=9F=E6=88=B7/?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=E5=85=AC=E5=8F=B8=EF=BC=8C=E5=81=9C=E7=94=A8?= =?UTF-8?q?=E5=8F=8C=E5=81=9C=EF=BC=88=E7=A7=9F=E6=88=B7+=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E5=85=AC=E5=8F=B8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/app.ts | 25 ++++-- src/views/app/AppMarket.vue | 168 ++++++++++++++++++++++++++++++++---- 2 files changed, 172 insertions(+), 21 deletions(-) diff --git a/src/api/app.ts b/src/api/app.ts index 7441ec9..c1a56a2 100644 --- a/src/api/app.ts +++ b/src/api/app.ts @@ -14,10 +14,19 @@ export interface AppItem { /** 市场页附加状态 */ TenantInstalled?: number TenantAuth?: number + /** 指定公司安装状态(当前公司维度) */ + CompanyInstalled?: number UserEnabled?: number ApplyStatus?: number } +/** 管理员可安装应用的公司范围项 */ +export interface CompanyScopeItem { + CompanyUUID: string + CompanyName: string + CompanyShort: string +} + /** 个人授权申请项(租户管理员审批台) */ export interface PendingItem { UserAppUUID: string @@ -31,17 +40,23 @@ export interface PendingItem { /** 应用/功能 API 客户端 */ export const appApi = { - /** 应用市场列表(附租户安装/个人启用/申请状态) */ + /** 应用市场列表(附租户/公司安装/个人启用/申请状态) */ market: (): Promise<{ list: AppItem[]; total: number }> => get<{ list: AppItem[]; total: number }>('/API/App/Market'), /** 我的应用(当前用户可用) */ myApps: (): Promise<{ list: AppItem[]; total: number }> => get<{ list: AppItem[]; total: number }>('/API/App/MyApps'), - /** 租户管理员:安装企业功能 */ - install: (AppUUID: string): Promise => post('/API/App/Install', { AppUUID }), + /** 管理员可安装应用的公司列表(按权限范围) */ + manageCompanies: (): Promise<{ Scope: string; Companies: CompanyScopeItem[] }> => + get<{ Scope: string; Companies: CompanyScopeItem[] }>('/API/App/ManageCompanies'), - /** 租户管理员:停用企业功能 */ - uninstall: (AppUUID: string): Promise => post('/API/App/Uninstall', { AppUUID }), + /** 管理员:安装企业功能(不传 CompanyUUIDs=全租户;传 CompanyUUIDs=指定公司) */ + install: (AppUUID: string, CompanyUUIDs?: string[]): Promise => + post('/API/App/Install', { AppUUID, CompanyUUIDs: CompanyUUIDs || [] }), + + /** 管理员:停用企业功能(不传 CompanyUUIDs=停用全租户;传 CompanyUUIDs=停用指定公司) */ + uninstall: (AppUUID: string, CompanyUUIDs?: string[]): Promise => + post('/API/App/Uninstall', { AppUUID, CompanyUUIDs: CompanyUUIDs || [] }), /** 个人:启用应用 */ enable: (AppUUID: string): Promise => post('/API/App/Enable', { AppUUID }), diff --git a/src/views/app/AppMarket.vue b/src/views/app/AppMarket.vue index 04c1c36..f39d7f8 100644 --- a/src/views/app/AppMarket.vue +++ b/src/views/app/AppMarket.vue @@ -7,9 +7,9 @@ * 接口:GET/POST /API/App/* */ import { ref, computed, onMounted } from 'vue' -import { ElMessage } from 'element-plus' +import { ElMessage, ElMessageBox } from 'element-plus' import { Grid } from '@element-plus/icons-vue' -import { appApi, type AppItem } from '@/api/app' +import { appApi, type AppItem, type CompanyScopeItem } from '@/api/app' import { useUserStore } from '@/stores/user' import { setPageTitle } from '@/utils/serverScope' @@ -21,6 +21,11 @@ const isAdmin = computed(() => userStore.isAdmin) const market = ref([]) const loading = ref(false) +/** 可管理公司列表(仅管理员) */ +const manageableCompanies = ref([]) +/** 权限范围:all=跨租户 / tenant=租户内 / company=指定公司 */ +const manageScope = ref('') + function typeLabel(t: string): { text: string; type: 'success' | 'info' | 'warning' } { if (t === 'enterprise') return { text: '企业功能', type: 'success' } if (t === 'personal') return { text: '个人应用', type: 'info' } @@ -44,18 +49,69 @@ async function loadMarket(): Promise { } } +// ==================== 安装弹窗(全租户 / 指定公司) ==================== +const installDialogVisible = ref(false) +const installDialogLoading = ref(false) +const installTargetApp = ref(null) +/** 安装粒度:tenant=全租户 / company=指定公司 */ +const installScope = ref<'tenant' | 'company'>('tenant') +/** 选中的公司 UUID 列表 */ +const selectedCompanies = ref([]) + +/** 打开安装弹窗:拉取可管理公司 */ +async function openInstallDialog(a: AppItem): Promise { + installTargetApp.value = a + installScope.value = 'tenant' + selectedCompanies.value = [] + installDialogVisible.value = true + if (isAdmin.value && !manageableCompanies.value.length) { + installDialogLoading.value = true + try { + const r = await appApi.manageCompanies() + manageableCompanies.value = r.Companies || [] + manageScope.value = r.Scope || '' + } catch { /* 拦截器已提示 */ } finally { + installDialogLoading.value = false + } + } +} + +/** 确认安装 */ +async function onConfirmInstall(): Promise { + const a = installTargetApp.value + if (!a) return + installDialogLoading.value = true + try { + const companyUUIDs = installScope.value === 'company' ? selectedCompanies.value : [] + await appApi.install(a.AppUUID, companyUUIDs) + const scopeText = installScope.value === 'company' + ? `已为 ${selectedCompanies.value.length} 家公司安装` + : '已全租户安装(全员可用)' + ElMessage.success(`「${a.AppName}」${scopeText}`) + installDialogVisible.value = false + await loadMarket() + } catch { /* 拦截器已提示 */ } finally { + installDialogLoading.value = false + } +} + /** 租户管理员:安装/停用企业功能 */ async function toggleInstall(a: AppItem): Promise { try { - if (a.TenantInstalled) { - await appApi.uninstall(a.AppUUID) + // 已安装(租户或当前公司)→ 直接停用;未安装 → 打开安装弹窗 + if (a.TenantInstalled || a.CompanyInstalled) { + await ElMessageBox.confirm( + `确定停用「${a.AppName}」吗?\n${a.TenantInstalled ? '将停用全租户安装' : '将停用当前公司安装'}`, + '停用功能', + { type: 'warning', confirmButtonText: '停用', cancelButtonText: '取消' }, + ) + await appApi.uninstall(a.AppUUID, a.TenantInstalled ? undefined : []) ElMessage.success(`已停用「${a.AppName}」`) - } else { - await appApi.install(a.AppUUID) - ElMessage.success(`已安装「${a.AppName}」,全员可用`) + await loadMarket() + return } - await loadMarket() - } catch { /* 拦截器已提示 */ } + await openInstallDialog(a) + } catch { /* 拦截器已提示或取消 */ } } /** 个人:启用/停用应用 */ @@ -84,7 +140,8 @@ async function applyAuth(a: AppItem): Promise { /** 市场卡片操作按钮(按类型 + 管理员/个人视角返回描述文案) */ function marketActionDesc(a: AppItem): string { if (a.AuthRequired && a.ApplyStatus === 1) return '授权申请中' - if (a.TenantInstalled) return '企业已安装' + if (a.TenantInstalled) return '企业已安装(全租户)' + if (a.CompanyInstalled) return '企业已安装(本公司)' if (a.UserEnabled) return '个人已启用' return '' } @@ -123,9 +180,9 @@ onMounted(loadMarket)
@@ -143,9 +200,9 @@ onMounted(loadMarket) @@ -259,4 +366,33 @@ onMounted(loadMarket) color: var(--el-color-danger); } } +/* 安装弹窗 */ +.gs-scope-group { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 10px; + margin-bottom: 14px; +} +.gs-company-list { + max-height: 260px; + overflow-y: auto; + border: 1px solid var(--el-border-color-lighter); + border-radius: 8px; + padding: 10px 14px; +} +.gs-company-item { + display: flex; + width: 100%; + margin-right: 0; + margin-bottom: 8px; + &:last-child { + margin-bottom: 0; + } +} +.gs-scope-tip { + font-size: 12px; + color: var(--el-text-color-secondary); + margin: 10px 0 0; +}