refactor: 页面优化
This commit is contained in:
@@ -424,6 +424,7 @@ function resetForm() {
|
|||||||
formData.id = undefined;
|
formData.id = undefined;
|
||||||
formData.targetType = 1;
|
formData.targetType = 1;
|
||||||
formData.targetUsers = [];
|
formData.targetUsers = [];
|
||||||
|
formData.content = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTargetUsers(value?: unknown) {
|
function normalizeTargetUsers(value?: unknown) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
@@ -63,48 +63,62 @@
|
|||||||
align="center"
|
align="center"
|
||||||
:selectable="isTenantSelectable"
|
:selectable="isTenantSelectable"
|
||||||
/>
|
/>
|
||||||
<el-table-column label="租户名称" prop="name" min-width="160" />
|
<el-table-column label="租户名称" prop="name" min-width="140" />
|
||||||
<el-table-column label="租户编码" prop="code" width="140" />
|
<el-table-column label="租户编码" prop="code" width="120" />
|
||||||
<el-table-column label="租户套餐" min-width="140">
|
<el-table-column label="租户套餐" min-width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span>{{ resolvePlanLabel(scope.row.planId) }}</span>
|
<span>{{ resolvePlanLabel(scope.row.planId) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="域名" prop="domain" min-width="160" />
|
<el-table-column label="域名" prop="domain" min-width="140" />
|
||||||
<el-table-column label="联系人" prop="contactName" width="120" />
|
<el-table-column label="联系人" prop="contactName" width="100" />
|
||||||
<el-table-column label="电话" prop="contactPhone" width="140" />
|
<el-table-column label="电话" prop="contactPhone" width="120" />
|
||||||
<el-table-column label="状态" width="120" align="center">
|
<el-table-column label="状态" width="90" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-switch
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'">
|
||||||
v-if="hasPermChangeStatus"
|
|
||||||
v-model="scope.row.status"
|
|
||||||
inline-prompt
|
|
||||||
active-text="正常"
|
|
||||||
inactive-text="禁用"
|
|
||||||
:active-value="1"
|
|
||||||
:inactive-value="0"
|
|
||||||
@change="handleStatusChange(scope.row.id, $event)"
|
|
||||||
/>
|
|
||||||
<el-tag v-else :type="scope.row.status === 1 ? 'success' : 'info'">
|
|
||||||
{{ scope.row.status === 1 ? "正常" : "禁用" }}
|
{{ scope.row.status === 1 ? "正常" : "禁用" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="过期时间" prop="expireTime" width="180" />
|
<el-table-column label="过期时间" prop="expireTime" width="160" />
|
||||||
<el-table-column label="创建时间" prop="createTime" width="180" />
|
<el-table-column label="创建时间" prop="createTime" width="160" />
|
||||||
<el-table-column fixed="right" label="操作" width="260">
|
<el-table-column fixed="right" label="操作" width="320">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-tooltip
|
||||||
v-if="!isPlatformTenantId(scope.row.id)"
|
v-if="!isPlatformTenantId(scope.row.id)"
|
||||||
|
content="更换租户套餐(将影响可用功能)"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
v-hasPerm="['sys:tenant:plan-assign']"
|
v-hasPerm="['sys:tenant:plan-assign']"
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
link
|
link
|
||||||
icon="menu"
|
icon="menu"
|
||||||
|
title="更换租户套餐(将影响可用功能)"
|
||||||
@click="handleOpenTenantPlanDialog(scope.row)"
|
@click="handleOpenTenantPlanDialog(scope.row)"
|
||||||
>
|
>
|
||||||
设置套餐
|
更换套餐
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip
|
||||||
|
v-if="!isPlatformTenantId(scope.row.id)"
|
||||||
|
content="在当前套餐范围内配置租户可用功能"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-hasPerm="['sys:tenant:plan-assign']"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
link
|
||||||
|
icon="setting"
|
||||||
|
:disabled="!scope.row.planId"
|
||||||
|
title="在当前套餐范围内配置租户可用功能"
|
||||||
|
@click="handleOpenTenantCustomizeDialog(scope.row)"
|
||||||
|
>
|
||||||
|
套餐功能配置
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['sys:tenant:update']"
|
v-hasPerm="['sys:tenant:update']"
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -232,15 +246,19 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 租户套餐设置 -->
|
<!-- 选择套餐(弹窗) -->
|
||||||
<el-drawer
|
<el-dialog
|
||||||
v-model="tenantPlanDialogVisible"
|
v-model="tenantPlanSelectVisible"
|
||||||
:title="'【' + checkedTenant.name + '】设置套餐'"
|
title="更换租户套餐"
|
||||||
size="640px"
|
width="520px"
|
||||||
@close="handleCloseTenantPlanDialog"
|
@close="handleCloseTenantPlanSelectDialog"
|
||||||
>
|
>
|
||||||
<el-form label-width="90px" class="mb-3">
|
<el-form label-width="90px" class="mb-3">
|
||||||
<el-form-item label="租户套餐">
|
<el-form-item label="当前套餐">
|
||||||
|
<el-input :model-value="resolvePlanLabel(checkedTenant.planId)" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="目标套餐">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="tenantPlanId"
|
v-model="tenantPlanId"
|
||||||
placeholder="请选择租户套餐"
|
placeholder="请选择租户套餐"
|
||||||
@@ -257,78 +275,76 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="mb-2 font-medium">升级场景</div>
|
||||||
|
<div class="mb-3">升级套餐后,将解锁更多功能菜单,需在套餐功能配置中手动启用。</div>
|
||||||
|
<div class="mb-2 font-medium">降级场景(红色 / warning)</div>
|
||||||
|
<el-alert
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
title="⚠ 降级套餐将导致超出套餐范围的功能被移除,相关功能将立即不可用。"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button
|
||||||
|
v-hasPerm="['sys:tenant:plan-assign']"
|
||||||
|
type="primary"
|
||||||
|
:disabled="!tenantPlanId || isPlanMenuEmpty"
|
||||||
|
@click="handleTenantPlanSelectSubmit"
|
||||||
|
>
|
||||||
|
确认更换
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="tenantPlanSelectVisible = false">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 套餐功能配置(抽屉) -->
|
||||||
|
<el-drawer
|
||||||
|
v-model="tenantPlanDialogVisible"
|
||||||
|
title="套餐功能配置"
|
||||||
|
size="640px"
|
||||||
|
@close="handleCloseTenantPlanDialog"
|
||||||
|
>
|
||||||
<el-alert
|
<el-alert
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
|
||||||
:closable="false"
|
:closable="false"
|
||||||
title="默认展示套餐菜单,如需微调请开启自定义"
|
show-icon
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
|
:title="`当前功能基于「${resolvePlanLabel(tenantPlanId)}」,你可以关闭不需要的功能。如需增加功能,请升级套餐。`"
|
||||||
/>
|
/>
|
||||||
|
<div class="text-xs text-gray-500 mb-2">仅展示当前套餐包含的功能</div>
|
||||||
|
|
||||||
<div class="flex-x-between">
|
<el-scrollbar class="h-[60vh]">
|
||||||
<el-input v-model="menuKeywords" clearable class="w-[150px]" placeholder="菜单名称">
|
|
||||||
<template #prefix>
|
|
||||||
<Search />
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
|
|
||||||
<div class="flex-center ml-5">
|
|
||||||
<el-button type="primary" size="small" plain @click="toggleMenuTree">
|
|
||||||
<template #icon>
|
|
||||||
<Switch />
|
|
||||||
</template>
|
|
||||||
{{ menuExpanded ? "收缩" : "展开" }}
|
|
||||||
</el-button>
|
|
||||||
<el-checkbox
|
|
||||||
v-model="menuParentChildLinked"
|
|
||||||
class="ml-5"
|
|
||||||
:disabled="!menuCustomizeEnabled"
|
|
||||||
@change="handleMenuLinkChange"
|
|
||||||
>
|
|
||||||
父子联动
|
|
||||||
</el-checkbox>
|
|
||||||
<el-switch
|
|
||||||
v-model="menuCustomizeEnabled"
|
|
||||||
class="ml-5"
|
|
||||||
inline-prompt
|
|
||||||
active-text="自定义"
|
|
||||||
inactive-text="默认"
|
|
||||||
:disabled="!hasPermTenantMenu"
|
|
||||||
@change="handleCustomizeToggle"
|
|
||||||
/>
|
|
||||||
<el-tooltip placement="bottom">
|
|
||||||
<template #content>开启自定义后可覆盖套餐菜单;关闭则仅使用套餐默认菜单</template>
|
|
||||||
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
|
|
||||||
<QuestionFilled />
|
|
||||||
</el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="menuTreeRef"
|
ref="menuTreeRef"
|
||||||
node-key="value"
|
node-key="value"
|
||||||
show-checkbox
|
show-checkbox
|
||||||
:props="menuTreeProps"
|
:props="menuTreeProps"
|
||||||
:data="menuPermOptions"
|
:data="menuPermOptions"
|
||||||
:filter-node-method="handleMenuFilter"
|
:default-expanded-keys="menuExpandedKeys"
|
||||||
:default-expand-all="true"
|
:default-expand-all="false"
|
||||||
:check-strictly="!menuParentChildLinked"
|
|
||||||
class="mt-5"
|
class="mt-5"
|
||||||
|
@check="handleMenuCheckedChange"
|
||||||
>
|
>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
{{ data.label }}
|
{{ data.label }}
|
||||||
</template>
|
</template>
|
||||||
</el-tree>
|
</el-tree>
|
||||||
|
</el-scrollbar>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button
|
<el-button
|
||||||
v-hasPerm="['sys:tenant:update']"
|
v-hasPerm="['sys:tenant:plan-assign']"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
:disabled="isPlanMenuEmpty"
|
||||||
@click="handleTenantPlanSubmit"
|
@click="handleTenantPlanSubmit"
|
||||||
>
|
>
|
||||||
保存设置
|
保存配置
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="tenantPlanDialogVisible = false">取消</el-button>
|
<el-button @click="tenantPlanDialogVisible = false">取消</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,6 +379,7 @@ import { isPlatformTenantId } from "@/utils/tenant";
|
|||||||
const queryFormRef = ref();
|
const queryFormRef = ref();
|
||||||
const dataFormRef = ref();
|
const dataFormRef = ref();
|
||||||
const menuTreeRef = ref();
|
const menuTreeRef = ref();
|
||||||
|
const planPreviewTreeRef = ref();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const ids = ref<number[]>([]);
|
const ids = ref<number[]>([]);
|
||||||
@@ -376,6 +393,7 @@ const queryParams = reactive<TenantQueryParams>({
|
|||||||
|
|
||||||
const pageData = ref<TenantItem[]>([]);
|
const pageData = ref<TenantItem[]>([]);
|
||||||
|
|
||||||
|
// 菜单树数据(已根据套餐过滤)
|
||||||
const menuPermOptions = ref<OptionItem[]>([]);
|
const menuPermOptions = ref<OptionItem[]>([]);
|
||||||
|
|
||||||
const dialog = reactive({
|
const dialog = reactive({
|
||||||
@@ -383,17 +401,26 @@ const dialog = reactive({
|
|||||||
visible: false,
|
visible: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tenantPlanSelectVisible = ref(false);
|
||||||
const tenantPlanDialogVisible = ref(false);
|
const tenantPlanDialogVisible = ref(false);
|
||||||
const checkedTenant = ref<{ id?: number; name?: string; planId?: number }>({});
|
const checkedTenant = ref<{ id?: number; name?: string; planId?: number }>({});
|
||||||
const checkedTenantForm = ref<TenantForm | null>(null);
|
const checkedTenantForm = ref<TenantForm | null>(null);
|
||||||
const tenantPlanId = ref<number | undefined>();
|
const tenantPlanId = ref<number | undefined>();
|
||||||
const menuCustomizeEnabled = ref(false);
|
// 套餐菜单与租户菜单(用于勾选)
|
||||||
const planMenuIds = ref<number[]>([]);
|
const planMenuIds = ref<number[]>([]);
|
||||||
const tenantMenuIds = ref<number[]>([]);
|
const tenantMenuIds = ref<number[]>([]);
|
||||||
const menuKeywords = ref("");
|
|
||||||
const menuExpanded = ref(true);
|
|
||||||
const menuParentChildLinked = ref(true);
|
|
||||||
const menuSourceOptions = ref<OptionItem[]>([]);
|
const menuSourceOptions = ref<OptionItem[]>([]);
|
||||||
|
const planPreviewOptions = ref<OptionItem[]>([]);
|
||||||
|
const menuCheckedCount = ref(0);
|
||||||
|
const menuKeywords = ref("");
|
||||||
|
const planPreviewKeywords = ref("");
|
||||||
|
const menuExpanded = ref(false);
|
||||||
|
const planPreviewExpanded = ref(false);
|
||||||
|
const menuParentChildLinked = ref(true);
|
||||||
|
const originalEnabledMenuIds = ref<number[]>([]);
|
||||||
|
|
||||||
|
// 计算当前可见菜单的默认展开节点
|
||||||
|
const menuExpandedKeys = computed(() => menuPermOptions.value.map((item) => item.value));
|
||||||
|
|
||||||
const menuTreeProps = {
|
const menuTreeProps = {
|
||||||
children: "children",
|
children: "children",
|
||||||
@@ -403,6 +430,11 @@ const menuTreeProps = {
|
|||||||
|
|
||||||
const planOptions = ref<OptionItem[]>([]);
|
const planOptions = ref<OptionItem[]>([]);
|
||||||
|
|
||||||
|
// 目标套餐未配置菜单时提示禁止提交
|
||||||
|
const isPlanMenuEmpty = computed(
|
||||||
|
() => tenantPlanId.value != null && planMenuIds.value.length === 0
|
||||||
|
);
|
||||||
|
|
||||||
const formData = reactive<TenantForm & TenantCreateForm>({
|
const formData = reactive<TenantForm & TenantCreateForm>({
|
||||||
id: undefined,
|
id: undefined,
|
||||||
name: "",
|
name: "",
|
||||||
@@ -418,6 +450,7 @@ const formData = reactive<TenantForm & TenantCreateForm>({
|
|||||||
adminUsername: "",
|
adminUsername: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 当前表单是否为平台租户
|
||||||
const isPlatformTenant = computed(() => isPlatformTenantId(formData.id));
|
const isPlatformTenant = computed(() => isPlatformTenantId(formData.id));
|
||||||
|
|
||||||
// 平台租户不允许批量删除
|
// 平台租户不允许批量删除
|
||||||
@@ -440,22 +473,21 @@ const rules = reactive({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasPermChangeStatus = computed(() => hasPerm("sys:tenant:change-status"));
|
|
||||||
const hasPermTenantMenu = computed(() => hasPerm("sys:tenant:plan-assign"));
|
const hasPermTenantMenu = computed(() => hasPerm("sys:tenant:plan-assign"));
|
||||||
|
|
||||||
function handleStatusChange(tenantId: string | number | undefined, val: string | number | boolean) {
|
// 说明:
|
||||||
if (tenantId == null) return;
|
// 1. 套餐决定功能上限
|
||||||
if (pageData.value.length > 0) {
|
// 2. 功能配置仅支持在套餐范围内关闭功能
|
||||||
handleChangeStatus(String(tenantId), Number(val));
|
// 3. 不允许在功能配置页切换套餐
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 根据套餐 ID 解析显示名称
|
||||||
function resolvePlanLabel(planId?: number) {
|
function resolvePlanLabel(planId?: number) {
|
||||||
if (planId == null) return "-";
|
if (planId == null) return "-";
|
||||||
const matched = planOptions.value.find((item) => Number(item.value) === planId);
|
const matched = planOptions.value.find((item) => Number(item.value) === planId);
|
||||||
return matched?.label || String(planId);
|
return matched?.label || String(planId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询租户列表
|
||||||
function fetchData() {
|
function fetchData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
TenantAPI.getPage(queryParams)
|
TenantAPI.getPage(queryParams)
|
||||||
@@ -471,16 +503,18 @@ function fetchData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开更换套餐弹窗并初始化数据
|
||||||
async function handleOpenTenantPlanDialog(row: TenantItem) {
|
async function handleOpenTenantPlanDialog(row: TenantItem) {
|
||||||
const tenantId = row.id;
|
const tenantId = row.id;
|
||||||
if (tenantId == null || tenantId === "") return;
|
if (tenantId == null || tenantId === "") return;
|
||||||
if (isPlatformTenantId(tenantId)) return;
|
if (isPlatformTenantId(tenantId)) return;
|
||||||
|
|
||||||
tenantPlanDialogVisible.value = true;
|
tenantPlanSelectVisible.value = true;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
menuCustomizeEnabled.value = false;
|
planPreviewKeywords.value = "";
|
||||||
|
planPreviewExpanded.value = false;
|
||||||
menuKeywords.value = "";
|
menuKeywords.value = "";
|
||||||
menuExpanded.value = true;
|
menuExpanded.value = false;
|
||||||
menuParentChildLinked.value = true;
|
menuParentChildLinked.value = true;
|
||||||
|
|
||||||
checkedTenant.value = {
|
checkedTenant.value = {
|
||||||
@@ -500,58 +534,55 @@ async function handleOpenTenantPlanDialog(row: TenantItem) {
|
|||||||
menuSourceOptions.value = menuOptions;
|
menuSourceOptions.value = menuOptions;
|
||||||
tenantMenuIds.value = normalizeMenuIds(menuIds);
|
tenantMenuIds.value = normalizeMenuIds(menuIds);
|
||||||
await handlePlanChange(tenantPlanId.value);
|
await handlePlanChange(tenantPlanId.value);
|
||||||
|
originalEnabledMenuIds.value =
|
||||||
|
tenantMenuIds.value.length > 0 ? [...tenantMenuIds.value] : [...planMenuIds.value];
|
||||||
|
updateCheckedMenus();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭套餐选择弹窗
|
||||||
|
function handleCloseTenantPlanSelectDialog() {
|
||||||
|
resetTenantPlanState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭套餐功能配置抽屉
|
||||||
function handleCloseTenantPlanDialog() {
|
function handleCloseTenantPlanDialog() {
|
||||||
|
resetTenantPlanState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置套餐相关的所有状态
|
||||||
|
function resetTenantPlanState() {
|
||||||
tenantPlanDialogVisible.value = false;
|
tenantPlanDialogVisible.value = false;
|
||||||
|
tenantPlanSelectVisible.value = false;
|
||||||
|
planPreviewKeywords.value = "";
|
||||||
|
planPreviewExpanded.value = false;
|
||||||
menuKeywords.value = "";
|
menuKeywords.value = "";
|
||||||
menuExpanded.value = true;
|
menuExpanded.value = false;
|
||||||
menuParentChildLinked.value = true;
|
menuParentChildLinked.value = true;
|
||||||
menuCustomizeEnabled.value = false;
|
originalEnabledMenuIds.value = [];
|
||||||
tenantPlanId.value = undefined;
|
tenantPlanId.value = undefined;
|
||||||
planMenuIds.value = [];
|
planMenuIds.value = [];
|
||||||
tenantMenuIds.value = [];
|
tenantMenuIds.value = [];
|
||||||
menuSourceOptions.value = [];
|
planPreviewOptions.value = [];
|
||||||
menuPermOptions.value = [];
|
menuPermOptions.value = [];
|
||||||
|
menuSourceOptions.value = [];
|
||||||
|
menuCheckedCount.value = 0;
|
||||||
checkedTenant.value = {};
|
checkedTenant.value = {};
|
||||||
checkedTenantForm.value = null;
|
checkedTenantForm.value = null;
|
||||||
menuTreeRef.value?.setCheckedKeys([], false);
|
menuTreeRef.value?.setCheckedKeys([], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMenuTree() {
|
// 切换目标套餐时更新可用菜单
|
||||||
menuExpanded.value = !menuExpanded.value;
|
|
||||||
if (menuTreeRef.value) {
|
|
||||||
Object.values(menuTreeRef.value.store.nodesMap).forEach((node: any) => {
|
|
||||||
if (menuExpanded.value) {
|
|
||||||
node.expand();
|
|
||||||
} else {
|
|
||||||
node.collapse();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMenuLinkChange(val: string | number | boolean) {
|
|
||||||
menuParentChildLinked.value = Boolean(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCustomizeToggle() {
|
|
||||||
menuPermOptions.value = applyMenuOptionsDisabled(
|
|
||||||
menuPermOptions.value,
|
|
||||||
!menuCustomizeEnabled.value
|
|
||||||
);
|
|
||||||
updateCheckedMenus();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handlePlanChange(planId?: number) {
|
async function handlePlanChange(planId?: number) {
|
||||||
if (!planId) {
|
if (!planId) {
|
||||||
planMenuIds.value = [];
|
planMenuIds.value = [];
|
||||||
menuPermOptions.value = applyMenuOptionsDisabled(menuSourceOptions.value, true);
|
planPreviewOptions.value = [];
|
||||||
|
menuPermOptions.value = applyMenuOptionsDisabled([], false);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
menuTreeRef.value?.setCheckedKeys([], false);
|
menuTreeRef.value?.setCheckedKeys([], false);
|
||||||
|
menuCheckedCount.value = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -561,8 +592,9 @@ async function handlePlanChange(planId?: number) {
|
|||||||
const allowedMenuIdSet = new Set(planMenuIds.value);
|
const allowedMenuIdSet = new Set(planMenuIds.value);
|
||||||
const filteredOptions = allowedMenuIdSet.size
|
const filteredOptions = allowedMenuIdSet.size
|
||||||
? filterMenuOptionsByIds(menuSourceOptions.value, allowedMenuIdSet)
|
? filterMenuOptionsByIds(menuSourceOptions.value, allowedMenuIdSet)
|
||||||
: menuSourceOptions.value;
|
: [];
|
||||||
menuPermOptions.value = applyMenuOptionsDisabled(filteredOptions, !menuCustomizeEnabled.value);
|
planPreviewOptions.value = filteredOptions;
|
||||||
|
menuPermOptions.value = applyMenuOptionsDisabled(filteredOptions, false);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
updateCheckedMenus();
|
updateCheckedMenus();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -570,28 +602,137 @@ async function handlePlanChange(planId?: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据套餐范围同步勾选菜单
|
||||||
function updateCheckedMenus() {
|
function updateCheckedMenus() {
|
||||||
const allowedMenuIdSet = new Set(planMenuIds.value);
|
const allowedMenuIdSet = new Set(planMenuIds.value);
|
||||||
const baseCheckedIds =
|
const checkedMenuIds =
|
||||||
menuCustomizeEnabled.value && tenantMenuIds.value.length > 0
|
tenantMenuIds.value.length > 0
|
||||||
? tenantMenuIds.value
|
? tenantMenuIds.value.filter((menuId) => allowedMenuIdSet.has(menuId))
|
||||||
: planMenuIds.value;
|
: planMenuIds.value;
|
||||||
const checkedMenuIds = allowedMenuIdSet.size
|
|
||||||
? baseCheckedIds.filter((menuId) => allowedMenuIdSet.has(menuId))
|
|
||||||
: baseCheckedIds;
|
|
||||||
menuTreeRef.value?.setCheckedKeys([], false);
|
menuTreeRef.value?.setCheckedKeys([], false);
|
||||||
checkedMenuIds.forEach((menuId) => menuTreeRef.value?.setChecked(menuId, true, false));
|
checkedMenuIds.forEach((menuId) => menuTreeRef.value?.setChecked(menuId, true, false));
|
||||||
|
menuCheckedCount.value = checkedMenuIds.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新已勾选菜单数量
|
||||||
|
function handleMenuCheckedChange() {
|
||||||
|
const checkedKeys = menuTreeRef.value?.getCheckedKeys(false) || [];
|
||||||
|
menuCheckedCount.value = checkedKeys.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开套餐功能配置抽屉并初始化数据
|
||||||
|
async function handleOpenTenantCustomizeDialog(row?: TenantItem) {
|
||||||
|
const tenantId = row?.id ?? checkedTenant.value.id;
|
||||||
|
if (!tenantId) return;
|
||||||
|
if (isPlatformTenantId(tenantId)) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
planPreviewKeywords.value = "";
|
||||||
|
planPreviewExpanded.value = false;
|
||||||
|
menuKeywords.value = "";
|
||||||
|
menuExpanded.value = false;
|
||||||
|
menuParentChildLinked.value = true;
|
||||||
|
|
||||||
|
checkedTenant.value = {
|
||||||
|
id: Number(tenantId),
|
||||||
|
name: row?.name || checkedTenant.value.name || String(tenantId),
|
||||||
|
planId: row?.planId != null ? Number(row.planId) : checkedTenant.value.planId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [tenantForm, menuOptions, menuIds] = await Promise.all([
|
||||||
|
TenantAPI.getFormData(String(tenantId)),
|
||||||
|
MenuAPI.getOptions(false, MenuScopeEnum.TENANT),
|
||||||
|
hasPermTenantMenu.value ? TenantAPI.getTenantMenuIds(Number(tenantId)) : Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
checkedTenantForm.value = tenantForm;
|
||||||
|
tenantPlanId.value = tenantForm.planId != null ? Number(tenantForm.planId) : undefined;
|
||||||
|
if (!tenantPlanId.value) {
|
||||||
|
ElMessage.warning("请先选择套餐");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menuSourceOptions.value = menuOptions;
|
||||||
|
tenantMenuIds.value = normalizeMenuIds(menuIds);
|
||||||
|
await handlePlanChange(tenantPlanId.value);
|
||||||
|
menuPermOptions.value = applyMenuOptionsDisabled(planPreviewOptions.value, false);
|
||||||
|
tenantPlanDialogVisible.value = true;
|
||||||
|
tenantPlanSelectVisible.value = false;
|
||||||
|
await nextTick();
|
||||||
|
updateCheckedMenus();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交更换套餐操作
|
||||||
|
async function handleTenantPlanSelectSubmit() {
|
||||||
|
const tenantId = checkedTenant.value.id;
|
||||||
|
if (!tenantId) return;
|
||||||
|
if (!tenantPlanId.value) {
|
||||||
|
ElMessage.warning("请选择租户套餐");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isPlanMenuEmpty.value) {
|
||||||
|
ElMessage.warning("该套餐未配置菜单");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPlanLabel = resolvePlanLabel(checkedTenant.value.planId);
|
||||||
|
const targetPlanLabel = resolvePlanLabel(tenantPlanId.value);
|
||||||
|
const tenantName = checkedTenant.value.name || "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确认将租户「${tenantName}」\n从「${currentPlanLabel}」更换为「${targetPlanLabel}」?\n\n该操作将立即生效,并影响租户可用功能。`,
|
||||||
|
"确认更换套餐",
|
||||||
|
{
|
||||||
|
type: "warning",
|
||||||
|
confirmButtonText: "确认更换",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantForm = checkedTenantForm.value;
|
||||||
|
if (!tenantForm) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const payload: TenantForm = {
|
||||||
|
...tenantForm,
|
||||||
|
planId: tenantPlanId.value,
|
||||||
|
};
|
||||||
|
await TenantAPI.update(String(tenantId), payload);
|
||||||
|
if (hasPermTenantMenu.value) {
|
||||||
|
const targetPlanMenuIdSet = new Set(planMenuIds.value);
|
||||||
|
const keepMenuIds = originalEnabledMenuIds.value.filter((menuId) =>
|
||||||
|
targetPlanMenuIdSet.has(menuId)
|
||||||
|
);
|
||||||
|
await TenantAPI.updateTenantMenus(tenantId, keepMenuIds);
|
||||||
|
}
|
||||||
|
ElMessage.success("套餐已更换,请确认租户功能配置");
|
||||||
|
checkedTenant.value.planId = tenantPlanId.value;
|
||||||
|
tenantPlanSelectVisible.value = false;
|
||||||
|
fetchData();
|
||||||
|
} catch {
|
||||||
|
ElMessage.error("套餐选择失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单搜索关键字联动过滤树
|
||||||
watch(menuKeywords, (val) => {
|
watch(menuKeywords, (val) => {
|
||||||
menuTreeRef.value?.filter(val);
|
menuTreeRef.value?.filter(val);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleMenuFilter(value: string, data: { [key: string]: any }) {
|
// 套餐预览搜索关键字联动过滤树
|
||||||
if (!value) return true;
|
watch(planPreviewKeywords, (val) => {
|
||||||
return data.label.includes(value);
|
planPreviewTreeRef.value?.filter(val);
|
||||||
}
|
});
|
||||||
|
// 过滤菜单树,仅保留套餐允许的节点
|
||||||
function filterMenuOptionsByIds(
|
function filterMenuOptionsByIds(
|
||||||
options: OptionItem[],
|
options: OptionItem[],
|
||||||
allowedMenuIdSet: Set<number>
|
allowedMenuIdSet: Set<number>
|
||||||
@@ -611,6 +752,7 @@ function filterMenuOptionsByIds(
|
|||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提交套餐功能配置更新
|
||||||
async function handleTenantPlanSubmit() {
|
async function handleTenantPlanSubmit() {
|
||||||
const tenantId = checkedTenant.value.id;
|
const tenantId = checkedTenant.value.id;
|
||||||
if (!tenantId) return;
|
if (!tenantId) return;
|
||||||
@@ -632,29 +774,33 @@ async function handleTenantPlanSubmit() {
|
|||||||
|
|
||||||
if (hasPermTenantMenu.value) {
|
if (hasPermTenantMenu.value) {
|
||||||
const allowedMenuIdSet = new Set(planMenuIds.value);
|
const allowedMenuIdSet = new Set(planMenuIds.value);
|
||||||
const menuIds = menuCustomizeEnabled.value
|
const menuIds: number[] = menuTreeRef.value
|
||||||
? menuTreeRef.value!.getCheckedNodes(false, true).map((node: any) => node.value)
|
? (menuTreeRef
|
||||||
|
.value!.getCheckedNodes(false, true)
|
||||||
|
.map((node: any) => Number(node.value)) as number[]) || []
|
||||||
: planMenuIds.value;
|
: planMenuIds.value;
|
||||||
const filteredMenuIds = allowedMenuIdSet.size
|
const filteredMenuIds: number[] = allowedMenuIdSet.size
|
||||||
? menuIds.filter((menuId) => allowedMenuIdSet.has(menuId))
|
? menuIds.filter((menuId: number) => allowedMenuIdSet.has(menuId))
|
||||||
: menuIds;
|
: menuIds;
|
||||||
await TenantAPI.updateTenantMenus(tenantId, filteredMenuIds);
|
await TenantAPI.updateTenantMenus(tenantId, filteredMenuIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
ElMessage.success("套餐设置成功");
|
ElMessage.success("租户功能配置已更新");
|
||||||
tenantPlanDialogVisible.value = false;
|
tenantPlanDialogVisible.value = false;
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error("套餐设置失败");
|
ElMessage.error("菜单微调失败");
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一菜单 ID 为 number 并过滤无效值
|
||||||
function normalizeMenuIds(menuIds: Array<number | string>) {
|
function normalizeMenuIds(menuIds: Array<number | string>) {
|
||||||
return menuIds.map((menuId) => Number(menuId)).filter((menuId) => !Number.isNaN(menuId));
|
return menuIds.map((menuId) => Number(menuId)).filter((menuId) => !Number.isNaN(menuId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 递归设置菜单节点禁用状态
|
||||||
function applyMenuOptionsDisabled(options: OptionItem[], disabled: boolean): OptionItem[] {
|
function applyMenuOptionsDisabled(options: OptionItem[], disabled: boolean): OptionItem[] {
|
||||||
return options.map((option) => ({
|
return options.map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
@@ -663,21 +809,25 @@ function applyMenuOptionsDisabled(options: OptionItem[], disabled: boolean): Opt
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 执行搜索
|
||||||
function handleQuery() {
|
function handleQuery() {
|
||||||
queryParams.pageNum = 1;
|
queryParams.pageNum = 1;
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置搜索条件
|
||||||
function handleResetQuery() {
|
function handleResetQuery() {
|
||||||
queryFormRef.value?.resetFields();
|
queryFormRef.value?.resetFields();
|
||||||
queryParams.pageNum = 1;
|
queryParams.pageNum = 1;
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录表格勾选项
|
||||||
function handleSelectionChange(selection: any) {
|
function handleSelectionChange(selection: any) {
|
||||||
ids.value = selection.map((item: any) => Number(item.id));
|
ids.value = selection.map((item: any) => Number(item.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开新增/编辑租户弹窗
|
||||||
async function handleOpenDialog(tenantId?: string) {
|
async function handleOpenDialog(tenantId?: string) {
|
||||||
dialog.visible = true;
|
dialog.visible = true;
|
||||||
if (tenantId != null && tenantId !== "") {
|
if (tenantId != null && tenantId !== "") {
|
||||||
@@ -708,6 +858,7 @@ async function handleOpenDialog(tenantId?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭租户弹窗并重置表单
|
||||||
function handleCloseDialog() {
|
function handleCloseDialog() {
|
||||||
dialog.visible = false;
|
dialog.visible = false;
|
||||||
dataFormRef.value?.resetFields();
|
dataFormRef.value?.resetFields();
|
||||||
@@ -728,6 +879,7 @@ function handleCloseDialog() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提交租户表单(新增/编辑)
|
||||||
const handleSubmit = useDebounceFn(async () => {
|
const handleSubmit = useDebounceFn(async () => {
|
||||||
const valid = await dataFormRef.value?.validate().catch(() => false);
|
const valid = await dataFormRef.value?.validate().catch(() => false);
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
@@ -777,6 +929,7 @@ const handleSubmit = useDebounceFn(async () => {
|
|||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
// 删除单个或批量租户
|
||||||
function handleDelete(tenantId?: string) {
|
function handleDelete(tenantId?: string) {
|
||||||
const tenantIds = tenantId != null && tenantId !== "" ? tenantId : ids.value.join(",");
|
const tenantIds = tenantId != null && tenantId !== "" ? tenantId : ids.value.join(",");
|
||||||
if (!tenantIds) {
|
if (!tenantIds) {
|
||||||
@@ -804,22 +957,13 @@ function handleDelete(tenantId?: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleChangeStatus(id: string | undefined, status: number) {
|
// 页面初始化
|
||||||
if (id == null || id === "") return;
|
|
||||||
try {
|
|
||||||
await TenantAPI.updateStatus(String(id), status);
|
|
||||||
ElMessage.success("状态更新成功");
|
|
||||||
} catch {
|
|
||||||
ElMessage.error("状态更新失败");
|
|
||||||
fetchData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
fetchPlanOptions();
|
fetchPlanOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 拉取租户套餐选项
|
||||||
async function fetchPlanOptions() {
|
async function fetchPlanOptions() {
|
||||||
try {
|
try {
|
||||||
const options = await TenantPlanAPI.getOptions();
|
const options = await TenantPlanAPI.getOptions();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- 用户管理 -->
|
<!-- 用户管理 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
@@ -107,6 +107,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="部门" width="120" align="center" prop="deptName" />
|
<el-table-column label="部门" width="120" align="center" prop="deptName" />
|
||||||
|
<el-table-column label="角色" align="center" prop="roleNames" min-width="160" />
|
||||||
<el-table-column label="手机号码" align="center" prop="mobile" width="120" />
|
<el-table-column label="手机号码" align="center" prop="mobile" width="120" />
|
||||||
<el-table-column label="邮箱" align="center" prop="email" width="160" />
|
<el-table-column label="邮箱" align="center" prop="email" width="160" />
|
||||||
<el-table-column label="状态" align="center" prop="status" width="80">
|
<el-table-column label="状态" align="center" prop="status" width="80">
|
||||||
|
|||||||
Reference in New Issue
Block a user