fix(app-config): 虚拟职务绑定按公司隔离,租户默认层不展示绑定
- 新增 bindingEnabled(要求明确选中公司),租户默认层不加载虚拟职务绑定并清空已展示数据、复位脏标记 - 切换范围时仅在选中公司下重新加载绑定,避免误展示/误改他司数据 - getVirtualMapping/setVirtualMapping 注释澄清:CompanyUUID 缺省回落可管理公司
This commit is contained in:
1 parent
9d1d6dca93
commit
1c0885126d
2 files changed
+261
-130
No files matched your search
+2
-2
@@ -175,14 +175,14 @@ export const appApi = {
|
||||
setStrictVisible: (AppUUID: string, StrictPositionVisible: number): Promise<{ msg: string }> =>
|
||||
post<{ msg: string }>('/API/App/SetStrictVisible', { AppUUID, StrictPositionVisible }),
|
||||
|
||||
/** 管理员:应用虚拟职务↔真实职务映射(配置页读取;CompanyUUID 指定公司范围,空=当前公司) */
|
||||
/** 管理员:应用虚拟职务↔真实职务映射(配置页读取;CompanyUUID 指定公司范围,缺省回落可管理公司) */
|
||||
getVirtualMapping: (AppUUID: string, CompanyUUID?: string): Promise<{ VirtualPositions: VirtualMappingItem[]; AllPositions: PositionOption[] }> =>
|
||||
get<{ VirtualPositions: VirtualMappingItem[]; AllPositions: PositionOption[] }>(
|
||||
'/API/App/VirtualMapping',
|
||||
CompanyUUID ? { AppUUID, CompanyUUID } : { AppUUID },
|
||||
),
|
||||
|
||||
/** 管理员:重写虚拟职务的真实职务绑定(配置页保存;CompanyUUID 指定公司范围,空=当前公司) */
|
||||
/** 管理员:重写虚拟职务的真实职务绑定(配置页保存;CompanyUUID 指定公司范围,缺省回落可管理公司) */
|
||||
setVirtualMapping: (
|
||||
AppUUID: string,
|
||||
Mappings: { AppVirtualPositionUUID: string; PositionUUIDs: string[] }[],
|
||||
|
||||
+259
-128
@@ -44,6 +44,12 @@ const scopeLabel = computed(() => {
|
||||
/** 是否为跨公司管理员(可看全部公司 + 复制) */
|
||||
const isMultiCompany = computed(() => scope.value === 'all' || scope.value === 'tenant')
|
||||
|
||||
/**
|
||||
* 绑定面板是否可编辑:虚拟职务绑定按公司隔离、无"租户默认"层,
|
||||
* 必须落在明确选中的公司上(公司管理员恒为本公司)。
|
||||
*/
|
||||
const bindingEnabled = computed(() => !!currentCompany.value)
|
||||
|
||||
/** 编辑副本:{Key: 用户输入值}(敏感键未改保持占位符) */
|
||||
const edited = ref<Record<string, string>>({})
|
||||
|
||||
@@ -153,10 +159,16 @@ async function onCopy(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// 切换范围(租户默认/公司)→ 配置与虚拟职务绑定均按新范围重新加载
|
||||
// 切换范围(租户默认/公司)→ 配置随范围加载;虚拟职务绑定仅在有明确公司时加载
|
||||
watch(currentCompany, () => {
|
||||
loadConfig()
|
||||
loadVirtualMapping()
|
||||
if (currentCompany.value) {
|
||||
loadVirtualMapping()
|
||||
} else {
|
||||
// 租户默认层无虚拟职务绑定:清空已展示数据并复位脏标记
|
||||
vpMappings.value = []
|
||||
vpDirty.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 虚拟职务 ↔ 真实职务映射 ====================
|
||||
@@ -179,7 +191,8 @@ const positionPickerConfig: EntityPickerConfig = {
|
||||
}
|
||||
|
||||
async function loadVirtualMapping(): Promise<void> {
|
||||
if (!AppUUID.value) return
|
||||
// 虚拟职务绑定按公司隔离:未选中具体公司(租户默认)不加载,避免误展示/误改他司数据
|
||||
if (!AppUUID.value || !currentCompany.value) return
|
||||
vpLoading.value = true
|
||||
try {
|
||||
const r = await appApi.getVirtualMapping(AppUUID.value, currentCompany.value || undefined)
|
||||
@@ -373,149 +386,194 @@ async function onRemoveTemplates(): Promise<void> {
|
||||
onMounted(async () => {
|
||||
await initScope()
|
||||
await loadConfig()
|
||||
await loadVirtualMapping()
|
||||
// 虚拟职务绑定按公司隔离:仅在已默认选中本公司(公司管理员)时加载
|
||||
if (currentCompany.value) {
|
||||
await loadVirtualMapping()
|
||||
}
|
||||
await loadTemplateTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-config-page">
|
||||
<el-card shadow="never" class="cfg-card">
|
||||
<el-card shadow="never" class="cfg-card main-card">
|
||||
<template #header>
|
||||
<div class="cfg-header">
|
||||
<div class="cfg-header cfg-header-main">
|
||||
<span class="cfg-title">配置「{{ AppName || '应用' }}」</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"
|
||||
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">已保存({{ currentCompany ? '公司级' : '租户默认' }})</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 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-card v-if="AppUUID" shadow="never" class="cfg-card vp-card">
|
||||
<template #header>
|
||||
<div class="cfg-header">
|
||||
<span class="cfg-title">虚拟职务绑定</span>
|
||||
<span class="cfg-sub">把真实职务对应到应用虚拟职务(由应用清单声明可见菜单)· 绑定范围:{{ scopeLabel }}</span>
|
||||
<span class="cfg-sub">配置与虚拟职务绑定均按公司隔离 · 公司级 → 租户默认 → 模板默认</span>
|
||||
<div class="spacer" />
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
:loading="vpAutoBinding"
|
||||
@click="autoBindVirtualPositions"
|
||||
>
|
||||
一键绑定
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="vpSaving"
|
||||
:disabled="!vpDirty"
|
||||
@click="saveVirtualMapping"
|
||||
>
|
||||
保存绑定
|
||||
</el-button>
|
||||
<!-- 范围选择(租户管理员可选默认/各公司;公司管理员仅本公司) -->
|
||||
<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-if="!currentCompany" size="small" type="warning" 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>
|
||||
</template>
|
||||
<div v-loading="vpLoading" class="vp-body">
|
||||
<div v-for="vp in vpMappings" :key="vp.AppVirtualPositionUUID" class="vp-row">
|
||||
<div class="vp-info">
|
||||
<div class="vp-name-row">
|
||||
<span class="vp-name">{{ vp.Name }}</span>
|
||||
<span class="vp-code">{{ vp.Code }}</span>
|
||||
</div>
|
||||
<div class="vp-menus">
|
||||
<el-tag
|
||||
v-for="m in vp.MenuNames"
|
||||
:key="m"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>{{ m }}</el-tag>
|
||||
<span v-if="!vp.MenuNames.length" class="vp-menus-empty">未声明可见菜单</span>
|
||||
|
||||
<div class="main-body">
|
||||
<!-- 当前范围语义横幅:公司隔离在 UI 上的直接表达 -->
|
||||
<div class="scope-banner" :class="currentCompany ? 'scope-banner-company' : 'scope-banner-tenant'">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<div class="scope-banner-text">
|
||||
<template v-if="!currentCompany">
|
||||
当前为 <b>租户默认</b> 范围:配置作为<b>全租户各公司的兜底</b>(公司未覆盖的键自动回退到此);
|
||||
虚拟职务绑定<b>仅按公司生效</b>,请先在右侧选择目标公司再编辑。
|
||||
</template>
|
||||
<template v-else>
|
||||
当前为 <b>{{ scopeLabel }}</b> 范围:所有修改<b>仅影响该公司</b>,与其他公司配置相互隔离;
|
||||
未覆盖的键自动回退租户默认。
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 区块 A:配置项 ==================== -->
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<span class="section-title">配置项</span>
|
||||
<span class="section-sub">
|
||||
{{ currentCompany ? '公司级保存 · 未保存键回退:租户默认 → 模板默认' : '全租户共享的默认配置 · 各公司可独立覆盖' }}
|
||||
</span>
|
||||
</div>
|
||||
<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">已保存({{ currentCompany ? '公司级' : '租户默认' }})</div>
|
||||
<div v-else-if="c.Default" class="cfg-meta">默认:{{ c.Default }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<el-divider class="main-divider" />
|
||||
|
||||
<!-- ==================== 区块 B:虚拟职务绑定 ==================== -->
|
||||
<!-- 拥有被绑定真实职务的用户看到对应虚拟职务声明的应用菜单(按公司隔离) -->
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<span class="section-title">虚拟职务绑定</span>
|
||||
<span class="section-sub">把真实职务对应到应用虚拟职务(由应用清单声明可见菜单)· 绑定范围:{{ scopeLabel }}</span>
|
||||
<div class="spacer" />
|
||||
<el-button
|
||||
v-if="bindingEnabled"
|
||||
size="small"
|
||||
type="warning"
|
||||
:loading="vpAutoBinding"
|
||||
@click="autoBindVirtualPositions"
|
||||
>
|
||||
一键绑定
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="bindingEnabled"
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="vpSaving"
|
||||
:disabled="!vpDirty"
|
||||
@click="saveVirtualMapping"
|
||||
>
|
||||
保存绑定
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="bindingEnabled">
|
||||
<div v-loading="vpLoading" class="vp-body">
|
||||
<div v-for="vp in vpMappings" :key="vp.AppVirtualPositionUUID" class="vp-row">
|
||||
<div class="vp-info">
|
||||
<div class="vp-name-row">
|
||||
<span class="vp-name">{{ vp.Name }}</span>
|
||||
<span class="vp-code">{{ vp.Code }}</span>
|
||||
</div>
|
||||
<div class="vp-menus">
|
||||
<el-tag
|
||||
v-for="m in vp.MenuNames"
|
||||
:key="m"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>{{ m }}</el-tag>
|
||||
<span v-if="!vp.MenuNames.length" class="vp-menus-empty">未声明可见菜单</span>
|
||||
</div>
|
||||
</div>
|
||||
<MultiEntityPicker
|
||||
v-model="vp.PositionUUIDs"
|
||||
:config="positionPickerConfig"
|
||||
:label-text="vp.PositionNames ? vp.PositionNames.join('、') : ''"
|
||||
placeholder="绑定真实职务(可多选)"
|
||||
@update:modelValue="onVpChange"
|
||||
/>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!vpLoading && !vpMappings.length"
|
||||
description="该应用未声明虚拟职务"
|
||||
:image-size="60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MultiEntityPicker
|
||||
v-model="vp.PositionUUIDs"
|
||||
:config="positionPickerConfig"
|
||||
:label-text="vp.PositionNames ? vp.PositionNames.join('、') : ''"
|
||||
placeholder="绑定真实职务(可多选)"
|
||||
@update:modelValue="onVpChange"
|
||||
<el-alert
|
||||
v-else
|
||||
class="vp-lock-tip"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="虚拟职务绑定按公司隔离,不存在「租户默认」层"
|
||||
description="请在上方「配置范围」中选择目标公司后,再进行一键绑定与保存绑定。"
|
||||
/>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!vpLoading && !vpMappings.length"
|
||||
description="该应用未声明虚拟职务"
|
||||
:image-size="60"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 通用部门与职务模板同步:应用声明的通用部门/职务按名称补齐到当前租户(已有复用) -->
|
||||
<!-- 通用部门与职务模板同步:租户级资源(部门/职务无公司归属),不受上方配置范围影响 -->
|
||||
<el-card v-if="AppUUID" shadow="never" class="cfg-card pt-card">
|
||||
<template #header>
|
||||
<div class="cfg-header">
|
||||
<el-icon><OfficeBuilding /></el-icon>
|
||||
<span class="cfg-title">通用部门与职务模板</span>
|
||||
<span class="cfg-sub">应用声明的通用部门与职务(按名称匹配,同名已有则直接复用)</span>
|
||||
<el-tag size="small" type="info" effect="plain" class="pt-scope-tag">租户级 · 不随上方配置范围变化</el-tag>
|
||||
<div class="spacer" />
|
||||
<el-button size="small" type="danger" :loading="ptRemoveLoading" plain @click="openRemoveDialog">
|
||||
移除通用部门与职务
|
||||
@@ -526,7 +584,8 @@ onMounted(async () => {
|
||||
</div>
|
||||
</template>
|
||||
<div class="pt-tip">
|
||||
应用声明的通用部门与职务(按名称匹配,同名已有则直接复用)。点击"同步"补齐缺失项;点击"移除"仅删除所有员工均未持有的职务及已清空的通用部门。
|
||||
部门与职务是<b>租户级字典</b>(各公司共享同一套),同步/移除作用于<b>整个租户</b>,
|
||||
与上方「配置范围」选择的公司无关。点击"同步"补齐缺失项;点击"移除"仅删除所有员工均未持有的职务及已清空的通用部门。
|
||||
</div>
|
||||
<el-tree
|
||||
:data="ptTreeData"
|
||||
@@ -664,9 +723,6 @@ onMounted(async () => {
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.vp-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.pt-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -754,29 +810,100 @@ onMounted(async () => {
|
||||
.cfg-title {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cfg-sub {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.cfg-header-main {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.cfg-scope {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
.cfg-scope-label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cfg-scope-select {
|
||||
width: 300px;
|
||||
width: 280px;
|
||||
}
|
||||
.cfg-scope-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.main-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scope-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
.el-icon {
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.scope-banner-tenant {
|
||||
color: var(--el-text-color-primary);
|
||||
background: var(--el-color-warning-light-9);
|
||||
border: 1px solid var(--el-color-warning-light-5);
|
||||
.el-icon {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
}
|
||||
.scope-banner-company {
|
||||
color: var(--el-text-color-primary);
|
||||
background: var(--el-color-info-light-9);
|
||||
border: 1px solid var(--el-color-info-light-5);
|
||||
.el-icon {
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
}
|
||||
.main-divider {
|
||||
margin: 18px 0;
|
||||
}
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.section-sub {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.vp-lock-tip {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.pt-scope-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cfg-body {
|
||||
min-height: 120px;
|
||||
}
|
||||
@@ -791,7 +918,11 @@ onMounted(async () => {
|
||||
.cfg-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.copy-tip {
|
||||
font-size: 13px;
|
||||
|
||||
Reference in new issue
Block a user