feat(app): 应用配置公司化——配置页支持范围选择(租户默认/各公司)+ 批量复制到公司,权限随安装范围
This commit is contained in:
1 parent
e1f43efc74
commit
7177f99cb8
2 files changed
+195
-17
No files matched your search
+19
-5
@@ -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<string, string>): 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<string, string>, 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 },
|
||||
),
|
||||
}
|
||||
+176
-12
@@ -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<AppConfigItem[]>([])
|
||||
|
||||
/** 可管理公司列表(权限范围决定) */
|
||||
const companies = ref<CompanyScopeItem[]>([])
|
||||
/** 配置管理范围: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<Record<string, string>>({})
|
||||
|
||||
const MASK = '******'
|
||||
|
||||
/** 初始化范围:拉取可管理公司,确定默认编辑范围 */
|
||||
async function initScope(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'保存后将立即生效。\n请谨慎操作,错误的配置可能导致该应用不可用。',
|
||||
`保存后将立即生效(范围:${scopeLabel.value})。\n请谨慎操作,错误的配置可能导致该应用不可用。`,
|
||||
'确认保存配置',
|
||||
{ type: 'warning', confirmButtonText: '保存', cancelButtonText: '取消' },
|
||||
)
|
||||
@@ -78,7 +115,7 @@ async function onSave(): Promise<void> {
|
||||
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<void> {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConfig)
|
||||
// ==================== 复制配置到公司 ====================
|
||||
const copyDialogVisible = ref(false)
|
||||
const copyTargets = ref<string[]>([])
|
||||
|
||||
function openCopyDialog(): void {
|
||||
copyTargets.value = []
|
||||
copyDialogVisible.value = true
|
||||
}
|
||||
|
||||
/** 确认复制:把当前范围配置复制到目标公司(覆盖同名键) */
|
||||
async function onCopy(): Promise<void> {
|
||||
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()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -95,10 +166,38 @@ onMounted(loadConfig)
|
||||
<template #header>
|
||||
<div class="cfg-header">
|
||||
<span class="cfg-title">配置「{{ AppName || '应用' }}」</span>
|
||||
<span class="cfg-sub">配置按租户隔离,有安装权限者(管理员)可编辑</span>
|
||||
<span class="cfg-sub">配置按公司隔离:公司级 → 租户默认 → 模板默认</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 范围选择(租户管理员可选默认/各公司;公司管理员仅本公司) -->
|
||||
<div class="cfg-scope">
|
||||
<span class="cfg-scope-label">配置范围:</span>
|
||||
<el-select
|
||||
v-model="currentCompany"
|
||||
:disabled="!isMultiCompany && companies.length <= 1"
|
||||
class="cfg-scope-select"
|
||||
>
|
||||
<el-option
|
||||
v-if="isMultiCompany"
|
||||
label="(默认)租户默认 · 全租户共享"
|
||||
value=""
|
||||
/>
|
||||
<el-option
|
||||
v-for="c in companies"
|
||||
:key="c.CompanyUUID"
|
||||
:label="c.CompanyName"
|
||||
:value="c.CompanyUUID"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tag v-if="!isMultiCompany" size="small" type="info" effect="plain" class="cfg-scope-tag">
|
||||
仅限本公司配置
|
||||
</el-tag>
|
||||
<el-tag v-else size="small" type="success" effect="plain" class="cfg-scope-tag">
|
||||
{{ scope === 'all' ? '跨租户管理员' : '租户管理员' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="cfg-body">
|
||||
<el-empty
|
||||
v-if="!loading && !configs.length"
|
||||
@@ -114,7 +213,7 @@ onMounted(loadConfig)
|
||||
:placeholder="c.Placeholder || c.Default"
|
||||
clearable
|
||||
/>
|
||||
<div v-if="c.Updated" class="cfg-meta">已保存</div>
|
||||
<div v-if="c.Updated" class="cfg-meta">已保存({{ currentCompany ? '公司级' : '租户默认' }})</div>
|
||||
<div v-else-if="c.Default" class="cfg-meta">默认:{{ c.Default }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -122,12 +221,44 @@ onMounted(loadConfig)
|
||||
|
||||
<template #footer>
|
||||
<div class="cfg-footer">
|
||||
<el-button v-if="isMultiCompany" :loading="copying" :disabled="!configs.length" @click="openCopyDialog">
|
||||
复制到公司…
|
||||
</el-button>
|
||||
<el-button :loading="saving" :disabled="!configs.length" type="primary" @click="onSave">
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 复制配置到公司弹窗 -->
|
||||
<el-dialog
|
||||
v-model="copyDialogVisible"
|
||||
:title="`复制配置「${AppName || ''}」到公司`"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="copy-tip">
|
||||
将当前范围({{ scopeLabel }})的配置复制到目标公司,<b>覆盖目标公司同名键</b>。
|
||||
</div>
|
||||
<el-checkbox-group v-model="copyTargets" class="copy-list">
|
||||
<el-checkbox
|
||||
v-for="c in companies"
|
||||
:key="c.CompanyUUID"
|
||||
:value="c.CompanyUUID"
|
||||
:disabled="c.CompanyUUID === currentCompany"
|
||||
class="copy-item"
|
||||
>
|
||||
{{ c.CompanyName }}{{ c.CompanyShort ? `(${c.CompanyShort})` : '' }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<el-button @click="copyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="copying" :disabled="!copyTargets.length" @click="onCopy">
|
||||
复制
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user