Files
GERP/src/views/app/AppConfigPage.vue
T
TermiNexus 5b221c07b7 feat(app): 应用中心公司级管理+配置页+身份徽标
- AppMarket 已安装应用改'管理':公司级安装/停用弹窗(diff 批量增删公司)
- 新增 AppConfigPage 独立配置页(AppConfig 键值表,/API/App/Config)
- 修复 Config 接口参数嵌套(params[AppUUID]→AppUUID)
- 侧边栏新增'强制刷新菜单'按钮
- 顶部导航新增当前身份徽标(角色+公司,管理员高亮,点击切换)
- 新增前端应用注册表 src/apps/index.ts(AppCode→路由/类型/旧页面映射,预留 MSYNC)
2026-08-13 07:46:59 +08:00

171 lines
4.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* AppConfigPage —— 应用配置页(独立 tab)
*
* 读取应用 Manifest 声明的配置模板 + 当前租户已保存值,
* 有安装权限者(管理员)可编辑保存。
* 入口:应用市场/我的应用 → "配置"按钮(router.push 带 AppUUID/AppName query)
* 接口:GET/POST /API/App/Config
*/
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { appApi, type AppConfigItem } from '@/api/app'
import { setPageTitle } from '@/utils/serverScope'
const route = useRoute()
const AppUUID = computed(() => (route.query.AppUUID as string) || '')
const AppName = computed(() => (route.query.AppName as string) || '')
setPageTitle('应用配置')
const loading = ref(false)
const saving = ref(false)
const configs = ref<AppConfigItem[]>([])
/** 编辑副本:{Key: 用户输入值}(敏感键未改保持占位符) */
const edited = ref<Record<string, string>>({})
const MASK = '******'
async function loadConfig(): Promise<void> {
if (!AppUUID.value) return
loading.value = true
try {
const r = await appApi.config(AppUUID.value)
configs.value = r.Configs || []
edited.value = {}
for (const c of configs.value) {
edited.value[c.Key] = c.Secret && c.Value ? MASK : (c.Value ?? '')
}
} catch { /* 拦截器已提示 */ } finally {
loading.value = false
}
}
/** 是否存在实际修改(敏感键占位符未变视为未改) */
function hasEdits(): boolean {
for (const c of configs.value) {
const cur = edited.value[c.Key] ?? ''
if (c.Secret) {
if (cur !== MASK && cur !== (c.Value ?? '')) return true
} else if (cur !== (c.Value ?? '')) {
return true
}
}
return false
}
async function onSave(): Promise<void> {
if (!hasEdits()) {
ElMessage.info('没有需要保存的修改')
return
}
try {
await ElMessageBox.confirm(
'保存后将立即生效。\n请谨慎操作,错误的配置可能导致该应用不可用。',
'确认保存配置',
{ type: 'warning', confirmButtonText: '保存', cancelButtonText: '取消' },
)
} catch {
return
}
saving.value = true
try {
const payload: Record<string, string> = {}
for (const c of configs.value) {
const cur = edited.value[c.Key] ?? ''
// 敏感键提交占位符 = 未修改,跳过
if (c.Secret && cur === MASK) continue
payload[c.Key] = cur
}
const r = await appApi.saveConfig(AppUUID.value, payload)
ElMessage.success(`配置已保存(${(r.Updated || []).length} 项)`)
await loadConfig()
} catch { /* 拦截器已提示 */ } finally {
saving.value = false
}
}
onMounted(loadConfig)
</script>
<template>
<div class="app-config-page">
<el-card shadow="never" class="cfg-card">
<template #header>
<div class="cfg-header">
<span class="cfg-title">配置「{{ AppName || '应用' }}」</span>
<span class="cfg-sub">配置按租户隔离,有安装权限者(管理员)可编辑</span>
</div>
</template>
<div v-loading="loading" class="cfg-body">
<el-empty
v-if="!loading && !configs.length"
description="该应用暂未声明配置项"
:image-size="80"
/>
<el-form v-else label-position="top" class="cfg-form">
<el-form-item v-for="c in configs" :key="c.Key" :label="c.Label">
<el-input
v-model="edited[c.Key]"
:type="c.Secret ? 'password' : 'text'"
:show-password="!!c.Secret"
:placeholder="c.Placeholder || c.Default"
clearable
/>
<div v-if="c.Updated" class="cfg-meta">已保存</div>
<div v-else-if="c.Default" class="cfg-meta">默认:{{ c.Default }}</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="cfg-footer">
<el-button :loading="saving" :disabled="!configs.length" type="primary" @click="onSave">
保存配置
</el-button>
</div>
</template>
</el-card>
</div>
</template>
<style scoped lang="scss">
.app-config-page {
padding: 16px;
box-sizing: border-box;
}
.cfg-card {
border-radius: 8px;
}
.cfg-header {
display: flex;
align-items: center;
gap: 10px;
.cfg-title {
font-weight: 600;
font-size: 15px;
}
.cfg-sub {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.cfg-body {
min-height: 120px;
}
.cfg-form {
max-width: 560px;
}
.cfg-meta {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
.cfg-footer {
display: flex;
justify-content: flex-end;
}
</style>