- AppMarket 已安装应用改'管理':公司级安装/停用弹窗(diff 批量增删公司) - 新增 AppConfigPage 独立配置页(AppConfig 键值表,/API/App/Config) - 修复 Config 接口参数嵌套(params[AppUUID]→AppUUID) - 侧边栏新增'强制刷新菜单'按钮 - 顶部导航新增当前身份徽标(角色+公司,管理员高亮,点击切换) - 新增前端应用注册表 src/apps/index.ts(AppCode→路由/类型/旧页面映射,预留 MSYNC)
171 lines
4.7 KiB
Vue
171 lines
4.7 KiB
Vue
<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>
|