From 7177f99cb8b5ff1887151d842e07f89b42d8ec87 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 13 Aug 2026 08:48:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=BA=94=E7=94=A8=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=85=AC=E5=8F=B8=E5=8C=96=E2=80=94=E2=80=94=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E6=94=AF=E6=8C=81=E8=8C=83=E5=9B=B4=E9=80=89?= =?UTF-8?q?=E6=8B=A9=EF=BC=88=E7=A7=9F=E6=88=B7=E9=BB=98=E8=AE=A4/?= =?UTF-8?q?=E5=90=84=E5=85=AC=E5=8F=B8=EF=BC=89+=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E5=88=B0=E5=85=AC=E5=8F=B8=EF=BC=8C=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=9A=8F=E5=AE=89=E8=A3=85=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/app.ts | 24 +++- src/views/app/AppConfigPage.vue | 188 ++++++++++++++++++++++++++++++-- 2 files changed, 195 insertions(+), 17 deletions(-) diff --git a/src/api/app.ts b/src/api/app.ts index 8865326..188a4f1 100644 --- a/src/api/app.ts +++ b/src/api/app.ts @@ -94,9 +94,23 @@ export const appApi = { /** 租户管理员:待审批申请列表 */ pending: (): Promise<{ list: PendingItem[]; total: number }> => get<{ list: PendingItem[]; total: number }>('/API/App/Pending'), - /** 应用配置:读取(GET)/保存(POST) */ - config: (AppUUID: string): Promise<{ Configs: AppConfigItem[]; Total: number }> => - get<{ Configs: AppConfigItem[]; Total: number }>('/API/App/Config', { AppUUID }), - saveConfig: (AppUUID: string, Config: Record): Promise<{ Updated: string[] }> => - post<{ Updated: string[] }>('/API/App/Config', { AppUUID, Config }), + /** 应用配置:读取(GET,CompanyUUID 可选——空=租户默认配置)/保存(POST,可指定公司) */ + config: (AppUUID: string, CompanyUUID?: string): Promise<{ Configs: AppConfigItem[]; Total: number; Scope: string; CompanyUUID: string }> => + get<{ Configs: AppConfigItem[]; Total: number; Scope: string; CompanyUUID: string }>( + '/API/App/Config', + CompanyUUID ? { AppUUID, CompanyUUID } : { AppUUID }, + ), + saveConfig: (AppUUID: string, Config: Record, CompanyUUID?: string): Promise<{ Updated: string[] }> => + post<{ Updated: string[] }>( + '/API/App/Config', + CompanyUUID ? { AppUUID, Config, CompanyUUID } : { AppUUID, Config }, + ), + /** 配置批量复制:把源配置(空=租户默认)复制到目标公司(覆盖同名键) */ + copyConfig: (AppUUID: string, TargetCompanyUUIDs: string[], SourceCompanyUUID?: string): Promise<{ Copied: number }> => + post<{ Copied: number }>( + '/API/App/Config/Copy', + SourceCompanyUUID + ? { AppUUID, TargetCompanyUUIDs, SourceCompanyUUID } + : { AppUUID, TargetCompanyUUIDs }, + ), } diff --git a/src/views/app/AppConfigPage.vue b/src/views/app/AppConfigPage.vue index abff5e7..78e653c 100644 --- a/src/views/app/AppConfigPage.vue +++ b/src/views/app/AppConfigPage.vue @@ -2,15 +2,17 @@ /** * AppConfigPage —— 应用配置页(独立 tab) * - * 读取应用 Manifest 声明的配置模板 + 当前租户已保存值, - * 有安装权限者(管理员)可编辑保存。 - * 入口:应用市场/我的应用 → "配置"按钮(router.push 带 AppUUID/AppName query) - * 接口:GET/POST /API/App/Config + * 读取应用 Manifest 声明的配置模板 + 当前范围已保存值(租户默认/公司级)。 + * 权限: + * - 租户管理员:可查看/编辑 租户默认 + 租户内任意公司配置,可批量复制配置到公司 + * - 公司管理员:仅可查看/编辑本公司配置 + * 读取回退链:公司级配置 → 租户默认配置 → 模板默认值。 + * 接口:GET/POST /API/App/Config、POST /API/App/Config/Copy */ -import { ref, computed, onMounted } from 'vue' +import { ref, computed, watch, onMounted } from 'vue' import { useRoute } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { appApi, type AppConfigItem } from '@/api/app' +import { appApi, type AppConfigItem, type CompanyScopeItem } from '@/api/app' import { setPageTitle } from '@/utils/serverScope' const route = useRoute() @@ -21,17 +23,52 @@ setPageTitle('应用配置') const loading = ref(false) const saving = ref(false) +const copying = ref(false) const configs = ref([]) + +/** 可管理公司列表(权限范围决定) */ +const companies = ref([]) +/** 配置管理范围:all/tenant(可管全部+租户默认)/ company(仅本公司) */ +const scope = ref('') +/** 当前编辑范围:'' = 租户默认(全租户);否则为公司 UUID */ +const currentCompany = ref('') + +/** 当前范围的展示名 */ +const scopeLabel = computed(() => { + if (!currentCompany.value) return '租户默认(全租户共享)' + const c = companies.value.find((x) => x.CompanyUUID === currentCompany.value) + return c ? `公司:${c.CompanyName}` : '指定公司' +}) + +/** 是否为跨公司管理员(可看全部公司 + 复制) */ +const isMultiCompany = computed(() => scope.value === 'all' || scope.value === 'tenant') + /** 编辑副本:{Key: 用户输入值}(敏感键未改保持占位符) */ const edited = ref>({}) const MASK = '******' +/** 初始化范围:拉取可管理公司,确定默认编辑范围 */ +async function initScope(): Promise { + try { + const r = await appApi.manageCompanies() + companies.value = r.Companies || [] + scope.value = r.Scope || '' + if (scope.value === 'company') { + // 公司管理员:默认本公司(第一个可管理公司) + currentCompany.value = companies.value[0]?.CompanyUUID || '' + } else { + // 租户/跨租户管理员:默认租户默认配置 + currentCompany.value = '' + } + } catch { /* 拦截器已提示 */ } +} + async function loadConfig(): Promise { if (!AppUUID.value) return loading.value = true try { - const r = await appApi.config(AppUUID.value) + const r = await appApi.config(AppUUID.value, currentCompany.value || undefined) configs.value = r.Configs || [] edited.value = {} for (const c of configs.value) { @@ -62,7 +99,7 @@ async function onSave(): Promise { } try { await ElMessageBox.confirm( - '保存后将立即生效。\n请谨慎操作,错误的配置可能导致该应用不可用。', + `保存后将立即生效(范围:${scopeLabel.value})。\n请谨慎操作,错误的配置可能导致该应用不可用。`, '确认保存配置', { type: 'warning', confirmButtonText: '保存', cancelButtonText: '取消' }, ) @@ -78,7 +115,7 @@ async function onSave(): Promise { if (c.Secret && cur === MASK) continue payload[c.Key] = cur } - const r = await appApi.saveConfig(AppUUID.value, payload) + const r = await appApi.saveConfig(AppUUID.value, payload, currentCompany.value || undefined) ElMessage.success(`配置已保存(${(r.Updated || []).length} 项)`) await loadConfig() } catch { /* 拦截器已提示 */ } finally { @@ -86,7 +123,41 @@ async function onSave(): Promise { } } -onMounted(loadConfig) +// ==================== 复制配置到公司 ==================== +const copyDialogVisible = ref(false) +const copyTargets = ref([]) + +function openCopyDialog(): void { + copyTargets.value = [] + copyDialogVisible.value = true +} + +/** 确认复制:把当前范围配置复制到目标公司(覆盖同名键) */ +async function onCopy(): Promise { + if (!copyTargets.value.length) { + ElMessage.info('请选择目标公司') + return + } + copying.value = true + try { + const r = await appApi.copyConfig( + AppUUID.value, + copyTargets.value, + currentCompany.value || undefined, + ) + ElMessage.success(`已复制 ${r.Copied || 0} 项配置到 ${copyTargets.value.length} 家公司`) + copyDialogVisible.value = false + } catch { /* 拦截器已提示 */ } finally { + copying.value = false + } +} + +watch(currentCompany, loadConfig) + +onMounted(async () => { + await initScope() + await loadConfig() +}) @@ -152,6 +283,23 @@ onMounted(loadConfig) color: var(--el-text-color-secondary); } } +.cfg-scope { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 14px; + .cfg-scope-label { + font-size: 13px; + color: var(--el-text-color-regular); + flex-shrink: 0; + } + .cfg-scope-select { + width: 300px; + } + .cfg-scope-tag { + flex-shrink: 0; + } +} .cfg-body { min-height: 120px; } @@ -166,5 +314,21 @@ onMounted(loadConfig) .cfg-footer { display: flex; justify-content: flex-end; + gap: 8px; +} +.copy-tip { + font-size: 13px; + color: var(--el-text-color-regular); + margin-bottom: 12px; +} +.copy-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 320px; + overflow-y: auto; +} +.copy-item { + margin: 0; }