feat: add tenant bound restriction for IM dashboard and management pages

1. extract isTenantBound utility from user store logic
2. add disabled state to tenant select components across all IM pages
3. update policy tenant options logic to hide global option for bound tenants
4. add backend scope validation for IM admin operations
This commit is contained in:
Austin
2026-09-22 11:34:40 +08:00
parent 40ac52923e
commit 0596599db9
31 changed files with 154 additions and 77 deletions
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -40,6 +40,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: true,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -42,6 +42,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
export const tenantOptions = ref<any[]>([]);
@@ -45,6 +45,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2
View File
@@ -7,6 +7,7 @@
<n-select
v-model:value="tenantId"
:options="imQueryTenantOptions"
:disabled="isTenantBound()"
class="tenant-select"
placeholder="请选择商户"
/>
@@ -40,6 +41,7 @@
import { useMessage } from 'naive-ui';
import { Overview, Trends } from '@/api/imDashboard';
import { imQueryTenantOptions, loadImQueryTenantOptions } from '@/views/imAdminQueryShared';
import { isTenantBound } from '@/views/imTenantOptions';
const message = useMessage();
const tenantId = ref(0);
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -50,6 +50,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: true,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -46,6 +46,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2
View File
@@ -15,6 +15,7 @@
<n-select
v-model:value="selectedTenantId"
:options="tenantOptions"
:disabled="isTenantBound()"
placeholder="请选择商户"
@update:value="loadConfig"
/>
@@ -72,6 +73,7 @@
import { useMessage } from 'naive-ui';
import { usePermission } from '@/hooks/web/usePermission';
import { Get, Update } from '@/api/imFeedbackConfig';
import { isTenantBound } from '@/views/imTenantOptions';
import { loadOptions, loadTenantOptions, newState, rules, State, tenantOptions } from './model';
const message = useMessage();
+2 -1
View File
@@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -49,6 +49,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -42,6 +42,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -122,6 +122,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -52,6 +52,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: true,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2
View File
@@ -3,6 +3,7 @@ import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { imQueryTenantOptions } from '@/views/imAdminQueryShared';
import { isTenantBound } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -13,6 +14,7 @@ export const schemas = ref<FormSchema[]>([
label: '商户',
componentProps: {
clearable: false,
disabled: isTenantBound(),
options: computed(() => imQueryTenantOptions.value),
placeholder: '请选择商户',
},
+2
View File
@@ -15,6 +15,7 @@
<n-select
v-model:value="selectedTenantId"
:options="tenantOptions"
:disabled="isTenantBound()"
placeholder="请选择商户"
@update:value="loadConfig"
/>
@@ -78,6 +79,7 @@
import { useMessage } from 'naive-ui';
import { usePermission } from '@/hooks/web/usePermission';
import { Get, Update } from '@/api/imMailboxConfig';
import { isTenantBound } from '@/views/imTenantOptions';
import { loadOptions, loadTenantOptions, newState, rules, State, tenantOptions } from './model';
const message = useMessage();
+2
View File
@@ -3,6 +3,7 @@ import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { imQueryTenantOptions } from '@/views/imAdminQueryShared';
import { isTenantBound } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -14,6 +15,7 @@ export function createMessageSchemas(group: boolean) {
label: '商户',
componentProps: {
clearable: false,
disabled: isTenantBound(),
options: computed(() => imQueryTenantOptions.value),
placeholder: '请选择商户',
},
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -44,6 +44,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -77,6 +77,7 @@ export const departmentSchemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -48,6 +48,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+3 -3
View File
@@ -1,6 +1,5 @@
import { ref } from 'vue';
import { useUserStore } from '@/store/modules/user';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
export { getDefaultTenantValue } from '@/views/imTenantOptions';
@@ -9,7 +8,8 @@ const tenantOnlyOptions = ref<any[]>([]);
export async function loadPolicyTenantOptions(includeGlobal = true) {
const tenants = await loadImTenantOptions(tenantOnlyOptions, { page: 1, perPage: 100 });
const canUseGlobal = includeGlobal && useUserStore().isCompanyDept;
// 商户绑定账号不显示“全局默认”,仅平台管理员(未绑定租户)可配置全局策略
const canUseGlobal = includeGlobal && !isTenantBound();
policyTenantOptions.value = canUseGlobal
? [{ label: '全局默认', value: 0 }, ...tenants]
: tenants;
+2 -1
View File
@@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -46,6 +46,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -2,6 +2,7 @@ import { computed, ref } from 'vue';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { policyTenantOptions } from '@/views/imPolicyShared';
import { isTenantBound } from '@/views/imTenantOptions';
const dict = useDictStore();
export const schemas = ref<FormSchema[]>([
@@ -9,7 +10,7 @@ export const schemas = ref<FormSchema[]>([
field: 'tenantId',
component: 'NSelect',
label: '商户',
componentProps: { options: computed(() => policyTenantOptions.value), clearable: false },
componentProps: { options: computed(() => policyTenantOptions.value), clearable: false, disabled: isTenantBound() },
},
{
field: 'publishStatus',
+2 -1
View File
@@ -3,6 +3,7 @@ import { NTag } from 'naive-ui';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { policyTenantOptions } from '@/views/imPolicyShared';
import { isTenantBound } from '@/views/imTenantOptions';
import { renderOptionTag } from '@/utils';
const dict = useDictStore();
@@ -12,7 +13,7 @@ export const schemas = ref<FormSchema[]>([
field: 'tenantId',
component: 'NSelect',
label: '商户',
componentProps: { options: computed(() => policyTenantOptions.value), clearable: false },
componentProps: { options: computed(() => policyTenantOptions.value), clearable: false, disabled: isTenantBound() },
},
{ field: 'word', component: 'NInput', label: '敏感词', componentProps: { clearable: true } },
{
+2 -1
View File
@@ -1,7 +1,7 @@
import { computed, ref } from 'vue';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -34,6 +34,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: true,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+29 -22
View File
@@ -1,6 +1,7 @@
import { Ref } from 'vue';
import { List as TenantList } from '@/api/imTenant';
import { useUserStore } from '@/store/modules/user';
import type { FormSchema } from '@/components/Form';
export interface ImTenantOption {
label: string;
@@ -26,15 +27,10 @@ export async function loadImTenantOptions(
target: Ref<ImTenantOption[]>,
params: Record<string, any> = { page: 1, perPage: 100 }
) {
const userStore = useUserStore();
const userInfo = { ...readTokenUserInfo(userStore.getToken), ...(userStore.getUserInfo as any) };
const boundTenantId = Number(userInfo?.tenantId || userInfo?.tenantID || 0);
const boundTenantId = getBoundTenantId();
if (!userStore.isCompanyDept) {
if (boundTenantId <= 0) {
target.value = [];
return target.value;
}
// 登录用户已绑定商户租户:仅返回自己商户选项(与后端 resolveImAdminScope 强制范围一致)
if (boundTenantId > 0) {
let tenant: any = null;
try {
const res = await TenantList({ ...params, id: boundTenantId, page: 1, perPage: 1 });
@@ -42,20 +38,11 @@ export async function loadImTenantOptions(
} catch {
tenant = null;
}
target.value = [
tenant
? toTenantOption(tenant)
: {
label: formatImTenantLabel({
id: boundTenantId,
name: userInfo?.tenantName,
code: userInfo?.tenantCode,
}),
value: boundTenantId,
code: userInfo?.tenantCode,
name: userInfo?.tenantName,
},
];
if (!tenant) {
const info = readTokenUserInfo(useUserStore().getToken);
tenant = { id: boundTenantId, name: info?.tenantName, code: info?.tenantCode };
}
target.value = [toTenantOption(tenant)];
return target.value;
}
@@ -64,6 +51,26 @@ export async function loadImTenantOptions(
return target.value;
}
// 登录用户绑定的商户租户ID(按绑定租户判定,与后端 resolveImAdminScope 的范围逻辑一致)
export function getBoundTenantId(): number {
const userStore = useUserStore();
const userInfo = { ...readTokenUserInfo(userStore.getToken), ...(userStore.getUserInfo as any) };
return Number(userInfo?.tenantId || userInfo?.tenantID || 0);
}
// 登录用户是否为商户绑定账号(true=下拉框只显示并锁定自己商户)
export function isTenantBound(): boolean {
return getBoundTenantId() > 0;
}
// 商户绑定账号:锁定搜索表单中的商户下拉项(仅一项、不可清除、不可更改)
export function lockTenantSchema(schema?: FormSchema) {
if (!schema || !isTenantBound()) return;
const props: any = (schema.componentProps = schema.componentProps || {});
props.disabled = true;
props.clearable = false;
}
function toTenantOption(item: any): ImTenantOption {
return {
label: formatImTenantLabel(item),
+2 -1
View File
@@ -3,7 +3,7 @@ import { NImage, NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
import { List as PolicyGroupList } from '@/api/imUserPolicyGroup';
import defaultAvatar from '@/assets/images/schoolboy.png';
@@ -86,6 +86,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
@@ -3,6 +3,7 @@ import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { imQueryTenantOptions } from '@/views/imAdminQueryShared';
import { isTenantBound } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -13,6 +14,7 @@ export const schemas = ref<FormSchema[]>([
label: '商户',
componentProps: {
clearable: false,
disabled: isTenantBound(),
options: computed(() => imQueryTenantOptions.value),
placeholder: '请选择商户',
},
+2 -1
View File
@@ -2,6 +2,7 @@ import { computed, ref } from 'vue';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { policyTenantOptions } from '@/views/imPolicyShared';
import { isTenantBound } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -24,7 +25,7 @@ export const schemas = ref<FormSchema[]>([
field: 'tenantId',
component: 'NSelect',
label: '商户',
componentProps: { options: computed(() => policyTenantOptions.value), clearable: false },
componentProps: { options: computed(() => policyTenantOptions.value), clearable: false, disabled: isTenantBound() },
},
{
field: 'keyword',
+2 -1
View File
@@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -88,6 +88,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},
+2 -1
View File
@@ -2,7 +2,7 @@ import { computed, ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
import { loadImTenantOptions } from '@/views/imTenantOptions';
import { isTenantBound, loadImTenantOptions } from '@/views/imTenantOptions';
const dict = useDictStore();
@@ -44,6 +44,7 @@ export const schemas = ref<FormSchema[]>([
componentProps: {
clearable: false,
options: computed(() => tenantOptions.value),
disabled: isTenantBound(),
placeholder: '请选择商户',
},
},