fix: 修复文件编码问题

This commit is contained in:
Ray.Hao
2026-01-29 10:35:53 +08:00
parent 6ff4a65ec9
commit da7163f463
23 changed files with 279 additions and 128 deletions

View File

@@ -1,4 +1,4 @@
<template> <template>
<!-- 悬浮按钮 --> <!-- 悬浮按钮 -->
<div class="ai-assistant"> <div class="ai-assistant">
<!-- AI 助手图标按钮 --> <!-- AI 助手图标按钮 -->

View File

@@ -1,4 +1,4 @@
<template> <template>
<div <div
class="rounded bg-[var(--el-bg-color)] border border-[var(--el-border-color)] p-5 h-full md:flex flex-1 flex-col md:overflow-auto" class="rounded bg-[var(--el-bg-color)] border border-[var(--el-border-color)] p-5 h-full md:flex flex-1 flex-col md:overflow-auto"
> >

View File

@@ -1,4 +1,4 @@
<template> <template>
<div> <div>
<!-- drawer --> <!-- drawer -->
<template v-if="modalConfig.component === 'drawer'"> <template v-if="modalConfig.component === 'drawer'">

View File

@@ -19,7 +19,7 @@
</span> </span>
</template> </template>
<!-- èªå®šä¹æ<EFBFBD>æ§?--> <!-- 自定义插槽 -->
<slot <slot
v-if="item.type === 'custom'" v-if="item.type === 'custom'"
:name="item.slotName" :name="item.slotName"
@@ -71,14 +71,14 @@ import { ArrowUp, ArrowDown } from "@element-plus/icons-vue";
import type { FormInstance } from "element-plus"; import type { FormInstance } from "element-plus";
import InputTag from "@/components/InputTag/index.vue"; import InputTag from "@/components/InputTag/index.vue";
// 定义接收的属æ€? // 定义接收的属性
const props = defineProps<{ searchConfig: ISearchConfig }>(); const props = defineProps<{ searchConfig: ISearchConfig }>();
// 自定义事ä»? // 自定义事件
const emit = defineEmits<{ const emit = defineEmits<{
queryClick: [queryParams: IObject]; queryClick: [queryParams: IObject];
resetClick: [queryParams: IObject]; resetClick: [queryParams: IObject];
}>(); }>();
// 组件映射è¡? // 组件映射表
const componentMap = new Map<ISearchComponent, any>([ const componentMap = new Map<ISearchComponent, any>([
// @ts-ignore // @ts-ignore
["input", markRaw(ElInput)], // @ts-ignore ["input", markRaw(ElInput)], // @ts-ignore
@@ -105,7 +105,7 @@ const formItems = reactive(props.searchConfig?.formItems ?? []);
const isExpandable = ref(props.searchConfig?.isExpandable ?? true); const isExpandable = ref(props.searchConfig?.isExpandable ?? true);
// 是否已展开 // 是否已展开
const isExpand = ref(false); const isExpand = ref(false);
// 表å<EFBFBD>•项展示数é‡<EFBFBD>,è¥å<EFBFBD>¯å±•开,超出展示数é‡<EFBFBD>的表å<EFBFBD>•项éš<EFBFBD>è—? // 表单项展示数量,若可展开,超出展示数量的表单项隐藏
const showNumber = computed(() => const showNumber = computed(() =>
isExpandable.value ? (props.searchConfig?.showNumber ?? 3) : formItems.length isExpandable.value ? (props.searchConfig?.showNumber ?? 3) : formItems.length
); );
@@ -113,7 +113,7 @@ const showNumber = computed(() =>
const cardAttrs = computed<IObject>(() => { const cardAttrs = computed<IObject>(() => {
return { shadow: "never", style: { "margin-bottom": "12px" }, ...props.searchConfig?.cardAttrs }; return { shadow: "never", style: { "margin-bottom": "12px" }, ...props.searchConfig?.cardAttrs };
}); });
// 表å<EFBFBD>•组件自定义属性(labelä½<EFBFBD>ç½®ã€<EFBFBD>宽度ã€<EFBFBD>对é½<EFBFBD>æ¹å¼<EFBFBD>ç­‰ï¼? // 表单组件自定义属性label位置、宽度、对齐方式等
const formAttrs = computed<IForm>(() => { const formAttrs = computed<IForm>(() => {
return { inline: true, ...props.searchConfig?.form }; return { inline: true, ...props.searchConfig?.form };
}); });
@@ -124,7 +124,7 @@ const isGrid = computed(() =>
: "flex flex-wrap gap-x-8 gap-y-4" : "flex flex-wrap gap-x-8 gap-y-4"
); );
// 获å<EFBFBD>tooltipæ<EFBFBD><EFBFBD>示框属æ€? // 获取tooltip提示框属性
const getTooltipProps = (tips: string | IObject) => { const getTooltipProps = (tips: string | IObject) => {
return typeof tips === "string" ? { content: tips } : tips; return typeof tips === "string" ? { content: tips } : tips;
}; };

View File

@@ -1,4 +1,4 @@
<template> <template>
<el-select <el-select
v-if="type === 'select'" v-if="type === 'select'"
v-model="selectedValue" v-model="selectedValue"

View File

@@ -1,12 +1,12 @@
<!-- <!--
* 基于 ECharts çš?Vue3 å¾è¡¨ç»ä» * 基于 ECharts Vue3 图表组件
* çˆæ<EFBFBD>ƒææœ?© 2021-present æœæ<EFBFBD>¥å¼æº<EFBFBD>ç»ç»? * 版权所有 © 2021-present 有来开源组织
* *
* 开源协议https://opensource.org/licenses/MIT * 开源协议https://opensource.org/licenses/MIT
* 项目地址https://gitee.com/youlaiorg/vue3-element-admin * 项目地址https://gitee.com/youlaiorg/vue3-element-admin
* 参考https://echarts.apache.org/handbook/zh/basics/import/#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6 * 参考https://echarts.apache.org/handbook/zh/basics/import/#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6
* *
* åœ¨ä½¿ç¨æï¼Œè¯·ä¿<EFBFBD>çæ­¤æ³¨éŠï¼ŒæŸè°¢æ¨å¯¹å¼æº<EFBFBD>çšæ¯æŒ<EFBFBD>ï¼? * 在使用时请保留此注释感谢您对开源的支持
--> -->
<template> <template>
@@ -14,13 +14,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// 引入 echarts 核心模å<EFBFBD>—,核心模å<EFBFBD>—æ<EFBFBD><EFBFBD>ä¾äº† echarts 使用必须è¦<C3A8>的接å<C2A5>£ã€? // 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。
import * as echarts from "echarts/core"; import * as echarts from "echarts/core";
// 引入柱状、折线和饼图常用图表 // 引入柱状、折线和饼图常用图表
import { BarChart, LineChart, PieChart } from "echarts/charts"; import { BarChart, LineChart, PieChart } from "echarts/charts";
// 引入标题,æ<EFBFBD><EFBFBD>示框,ç´è§å<EFBFBD><EFBFBD>标系,数æ<EFBFBD>®é†ï¼Œå†…置数æ<EFBFBD>®è½¬æ<EFBFBD>¢å™¨ç»„ä»¶ï¼? // 引入标题,提示框,直角坐标系,数据集,内置数据转换器组件,
import { GridComponent, TooltipComponent, LegendComponent } from "echarts/components"; import { GridComponent, TooltipComponent, LegendComponent } from "echarts/components";
// 引入 Canvas 渲染器,注æ„<EFBFBD>引入 CanvasRenderer 或è€?SVGRenderer 是必须的一æ­? // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { CanvasRenderer } from "echarts/renderers"; import { CanvasRenderer } from "echarts/renderers";
import { useResizeObserver } from "@vueuse/core"; import { useResizeObserver } from "@vueuse/core";
@@ -45,7 +45,7 @@ const props = defineProps<{
const chartRef = ref<HTMLDivElement | null>(null); const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null; let chartInstance: echarts.ECharts | null = null;
// åˆ<EFBFBD>å§åŒå¾è¡? // 初始化图表
const initChart = () => { const initChart = () => {
if (chartRef.value) { if (chartRef.value) {
chartInstance = echarts.init(chartRef.value); chartInstance = echarts.init(chartRef.value);
@@ -55,12 +55,12 @@ const initChart = () => {
} }
}; };
// çå<EFBFBD>¬å°ºå¯¸å<EFBFBD>˜åŒï¼Œè‡ªåŠ¨è°ƒæ•? // 监听尺寸变化,自动调整
useResizeObserver(chartRef, () => { useResizeObserver(chartRef, () => {
chartInstance?.resize(); chartInstance?.resize();
}); });
// çå<EFBFBD>¬ options å<EFBFBD>˜åŒï¼Œæ´æ°å¾è¡? // 监听 options 变化,更新图表
watch( watch(
() => props.options, () => props.options,
(newOptions) => { (newOptions) => {

View File

@@ -23,7 +23,7 @@ const hamburgerClass = computed(() => {
return "hamburger--white"; return "hamburger--white";
} }
// 妿žœæ˜¯æ··å<EFBFBD>ˆå¸ƒå±€ && ä¾§è¾¹æ <C3A6>é…<C3A9>è‰²æ¹æ¡ˆæ˜¯ç»<C3A7>å…¸è“? // 如果是混合布局 && 侧边栏配色方案是经典蓝
if ( if (
layout.value === LayoutMode.MIX && layout.value === LayoutMode.MIX &&
settingsStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE settingsStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE

View File

@@ -159,7 +159,7 @@ onClickOutside(iconSelectRef, () => (popoverVisible.value = false), {
}); });
/** /**
* 清空已选图æ ? * 清空已选图标
*/ */
function clearSelectedIcon() { function clearSelectedIcon() {
selectedIcon.value = ""; selectedIcon.value = "";

View File

@@ -1,23 +1,23 @@
<!-- <!--
* 基于 wangEditor-next çšå¯Œææœ¬ç¼è¾å¨ç»ä»äºŒæ¬¡å°<EFBFBD>è£? * 基于 wangEditor-next 的富文本编辑器组件二次封装
* çˆæ<EFBFBD>ƒææœ?© 2021-present æœæ<EFBFBD>¥å¼æº<EFBFBD>ç»ç»? * 版权所属 © 2021-present 有来开源组织
* *
* 开源协议https://opensource.org/licenses/MIT * 开源协议https://opensource.org/licenses/MIT
* 项目地址https://gitee.com/youlaiorg/vue3-element-admin * 项目地址https://gitee.com/youlaiorg/vue3-element-admin
* *
* åœ¨ä½¿ç¨æï¼Œè¯·ä¿<EFBFBD>çæ­¤æ³¨éŠï¼ŒæŸè°¢æ¨å¯¹å¼æº<EFBFBD>çšæ¯æŒ<EFBFBD>ï¼? * 在使用时请保留此注释感谢您对开源的支持
--> -->
<template> <template>
<div style="z-index: 999; border: 1px solid var(--el-border-color)"> <div style="z-index: 999; border: 1px solid var(--el-border-color)">
<!-- 工巿 ?--> <!-- 工具栏 -->
<Toolbar <Toolbar
:editor="editorRef" :editor="editorRef"
mode="simple" mode="simple"
:default-config="toolbarConfig" :default-config="toolbarConfig"
style="border-bottom: 1px solid var(--el-border-color)" style="border-bottom: 1px solid var(--el-border-color)"
/> />
<!-- ç¼è¾å?--> <!-- 编辑器 -->
<Editor <Editor
v-model="modelValue" v-model="modelValue"
:style="{ height: height, overflowY: 'hidden' }" :style="{ height: height, overflowY: 'hidden' }"
@@ -51,15 +51,15 @@ const modelValue = defineModel("modelValue", {
required: false, required: false,
}); });
// 编辑器实例,必须ç”?shallowRef,é‡<C3A9>è¦<C3A8>ï¼<C3AF> // 编辑器实例,必须用 shallowRef重要
const editorRef = shallowRef(); const editorRef = shallowRef();
// 工具æ <EFBFBD>é…<EFBFBD>ç½? // 工具栏配置
const toolbarConfig = ref<Partial<IToolbarConfig>>({}); const toolbarConfig = ref<Partial<IToolbarConfig>>({});
// ç¼è¾å™¨é…<EFBFBD>ç½? // 编辑器配置
const editorConfig = ref<Partial<IEditorConfig>>({ const editorConfig = ref<Partial<IEditorConfig>>({
placeholder: "请输入内å®?..", placeholder: "请输入内容..",
MENU_CONF: { MENU_CONF: {
uploadImage: { uploadImage: {
customUpload(file: File, insertFn: InsertFnType) { customUpload(file: File, insertFn: InsertFnType) {

View File

@@ -1,6 +1,6 @@
<template> <template>
<BaseLayout> <BaseLayout>
<!-- 左侧è<EFBFBD>œå<EFBFBD>æ ?--> <!-- 左侧菜单 -->
<div class="layout__sidebar" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }"> <div class="layout__sidebar" :class="{ 'layout__sidebar--collapsed': !isSidebarOpen }">
<div :class="{ 'has-logo': showLogo }" class="layout-sidebar"> <div :class="{ 'has-logo': showLogo }" class="layout-sidebar">
<LayoutLogo v-if="showLogo" :collapse="!isSidebarOpen" /> <LayoutLogo v-if="showLogo" :collapse="!isSidebarOpen" />
@@ -91,7 +91,7 @@ const { showTagsView, showLogo, isSidebarOpen, routes } = useLayout();
} }
} }
/* 移动端样å¼?*/ /* 移动端样式*/
.mobile { .mobile {
.layout__sidebar { .layout__sidebar {
width: $sidebar-width !important; width: $sidebar-width !important;

View File

@@ -1,4 +1,4 @@
<template> <template>
<el-drawer <el-drawer
v-model="drawerVisible" v-model="drawerVisible"
size="380" size="380"

View File

@@ -1,4 +1,4 @@
<template> <template>
<div v-if="!item.meta || !item.meta.hidden"> <div v-if="!item.meta || !item.meta.hidden">
<!--叶子节点显示叶子节点或唯一子节点且父节点未配置始终显示 --> <!--叶子节点显示叶子节点或唯一子节点且父节点未配置始终显示 -->
<template <template

View File

@@ -1,4 +1,4 @@
<template> <template>
<div class="tags-container"> <div class="tags-container">
<!-- 水平滚动容器 --> <!-- 水平滚动容器 -->
<el-scrollbar <el-scrollbar

View File

@@ -21,9 +21,9 @@ export interface LogItem {
/** 日志内容 */ /** 日志内容 */
content: string; content: string;
/** 请求路径 */ /** 请求路径 */
requestUri: string; requestUri?: string;
/** 请求方法 */ /** 请求方法 */
method: string; method?: string;
/** IP地址 */ /** IP地址 */
ip: string; ip: string;
/** 地区 */ /** 地区 */
@@ -34,6 +34,10 @@ export interface LogItem {
os: string; os: string;
/** 执行时间(毫秒) */ /** 执行时间(毫秒) */
executionTime: number; executionTime: number;
/** 创建人ID */
createBy?: string;
/** 操作时间 */
createTime?: string;
/** 操作人 */ /** 操作人 */
operator: string; operator: string;
} }

View File

@@ -26,10 +26,10 @@ export interface NoticeForm {
type?: number; type?: number;
/** 通知等级 */ /** 通知等级 */
level?: string; level?: string;
/** 发布状态(0:草稿;1:已发布;2:已撤回) */ /** 发布状态(0:草稿;1:已发布;-1:已撤回) */
publishStatus?: number; status?: number;
/** 目标用户ID(多个以英文逗号(,)分割) */ /** 目标用户ID列表 */
targetUserIds?: string; targetUsers?: number[];
/** 目标类型 (1:全部,2:指定用户等) */ /** 目标类型 (1:全部,2:指定用户等) */
targetType?: number; targetType?: number;
} }

View File

@@ -12,7 +12,7 @@ const http = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000, timeout: 50000,
headers: { "Content-Type": "application/json;charset=utf-8" }, headers: { "Content-Type": "application/json;charset=utf-8" },
paramsSerializer: (params) => qs.stringify(params), paramsSerializer: (params) => qs.stringify(params, { arrayFormat: "repeat" }),
}); });
// ============================================ // ============================================
@@ -54,12 +54,6 @@ http.interceptors.response.use(
if (page != null) return { data, page }; if (page != null) return { data, page };
return data; return data;
} }
// 需要选择租户(特殊业务码,传递给调用方处理)
if (code === ApiCodeEnum.CHOOSE_TENANT) {
return Promise.reject({ code, data, msg });
}
ElMessage.error(msg || "系统出错"); ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error")); return Promise.reject(new Error(msg || "Error"));
}, },

View File

@@ -7,7 +7,7 @@
target="_blank" target="_blank"
class="mb-[20px]" class="mb-[20px]"
> >
ç¤ºä¾æº<EFBFBD>ç <EFBFBD> 请ç¹å?>>> 示例源码 请点击>>>
</el-link> </el-link>
<el-form> <el-form>
<el-form-item label="性别"> <el-form-item label="性别">

View File

@@ -1,4 +1,4 @@
<!-- 徿 éæ©å¨ç¤ºä¾?--> <!-- 图标选择器示例 -->
<template> <template>
<div class="app-container"> <div class="app-container">
<el-link <el-link
@@ -7,15 +7,15 @@
target="_blank" target="_blank"
class="mb-10" class="mb-10"
> >
ç¤ºä¾æº<EFBFBD>ç <EFBFBD> 请ç¹å?>>> 示例源码 请点击>>>
</el-link> </el-link>
<icon-select v-model="iconName" /> <icon-select v-model="iconName" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// element-plus 徿 ‡æ ¼å¼<EFBFBD>以el-icon-å¼€å¤? // element-plus 图标格式以el-icon-开头
const iconName = ref("el-icon-edit"); const iconName = ref("el-icon-edit");
// 本地SVG徿 ‡æ ¼å¼<EFBFBD>å<EFBFBD>?src/assets/icons ä¸çš„æ‡ä»¶å<C2B6><C3A5>,ä¸<C3A4>需è¦<C3A8>svgå<67>Žç¼€ // 本地SVG图标格式为src/assets/icons 下的文件名不需要svg后缀
// const iconName = ref("api"); // const iconName = ref("api");
</script> </script>

View File

@@ -1,4 +1,4 @@
<!-- åˆè¡¨éæ©å¨ç¤ºä¾?--> <!-- 列表选择器示例 -->
<template> <template>
<div class="app-container"> <div class="app-container">
<el-link <el-link
@@ -7,7 +7,7 @@
target="_blank" target="_blank"
class="mb-10" class="mb-10"
> >
ç¤ºä¾æº<EFBFBD>ç <EFBFBD> 请ç¹å?>>> 示例源码 请点击>>>
</el-link> </el-link>
<table-select :text="text" :select-config="selectConfig" @confirm-click="handleConfirm"> <table-select :text="text" :select-config="selectConfig" @confirm-click="handleConfirm">
<template #status="scope"> <template #status="scope">

View File

@@ -7,7 +7,7 @@
target="_blank" target="_blank"
class="mb-10" class="mb-10"
> >
示例源码 请点<EFBFBD><EFBFBD>?>>> 示例源码 请点>>>
</el-link> </el-link>
<el-form> <el-form>

View File

@@ -1,8 +1,8 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="filter-section">
<el-form ref="queryFormRef" :model="queryParams" :inline="true"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item prop="keywords" label="关键字"> <el-form-item prop="keywords" label="关键字">
<el-input <el-input
v-model="queryParams.keywords" v-model="queryParams.keywords"
@@ -21,7 +21,7 @@
start-placeholder="开始时间" start-placeholder="开始时间"
end-placeholder="截止时间" end-placeholder="截止时间"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 200px" style="width: 260px"
/> />
</el-form-item> </el-form-item>
@@ -40,7 +40,7 @@
border border
class="data-table__content" class="data-table__content"
> >
<el-table-column label="操作时间" prop="createTime" width="180" /> <el-table-column label="操作时间" prop="createTime" width="220" />
<el-table-column label="操作人" prop="operator" width="120" /> <el-table-column label="操作人" prop="operator" width="120" />
<el-table-column label="日志模块" prop="module" width="100" /> <el-table-column label="日志模块" prop="module" width="100" />
<el-table-column label="日志内容" prop="content" min-width="200" /> <el-table-column label="日志内容" prop="content" min-width="200" />
@@ -80,7 +80,7 @@ const queryParams = reactive<LogQueryParams>({
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
keywords: "", keywords: "",
createTime: ["", ""], createTime: undefined as [string, string] | undefined,
}); });
// 日志表格数据 // 日志表格数据

View File

@@ -3,7 +3,7 @@
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="filter-section"> <div class="filter-section">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-suffix=":"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-suffix=":">
<el-form-item label="标题123" prop="title"> <el-form-item label="标题" prop="title">
<el-input <el-input
v-model="queryParams.title" v-model="queryParams.title"
placeholder="标题" placeholder="标题"
@@ -190,8 +190,8 @@
<el-radio :value="2">指定</el-radio> <el-radio :value="2">指定</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item v-if="formData.targetType == 2" label="指定用户" prop="targetUserIds"> <el-form-item v-if="formData.targetType == 2" label="指定用户" prop="targetUsers">
<el-select v-model="formData.targetUserIds" multiple search placeholder="请选择指定用户"> <el-select v-model="formData.targetUsers" multiple search placeholder="请选择指定用户">
<el-option <el-option
v-for="item in userOptions" v-for="item in userOptions"
:key="item.value" :key="item.value"
@@ -359,10 +359,14 @@ function handleOpenDialog(id?: string) {
if (id) { if (id) {
dialog.title = "修改公告"; dialog.title = "修改公告";
NoticeAPI.getFormData(id).then((data) => { NoticeAPI.getFormData(id).then((data) => {
Object.assign(formData, data); const normalized = {
...data,
targetUsers: normalizeTargetUsers(data?.targetUsers),
};
Object.assign(formData, normalized);
}); });
} else { } else {
Object.assign(formData, { level: 0, targetType: 0 }); Object.assign(formData, { level: "L", targetType: 1, targetUsers: [] });
dialog.title = "新增公告"; dialog.title = "新增公告";
} }
} }
@@ -388,6 +392,9 @@ function handleSubmit() {
dataFormRef.value.validate((valid: any) => { dataFormRef.value.validate((valid: any) => {
if (valid) { if (valid) {
loading.value = true; loading.value = true;
if (formData.targetType !== 2) {
formData.targetUsers = [];
}
const id = formData.id; const id = formData.id;
if (id) { if (id) {
NoticeAPI.update(id, formData) NoticeAPI.update(id, formData)
@@ -416,6 +423,25 @@ function resetForm() {
dataFormRef.value.clearValidate(); dataFormRef.value.clearValidate();
formData.id = undefined; formData.id = undefined;
formData.targetType = 1; formData.targetType = 1;
formData.targetUsers = [];
}
function normalizeTargetUsers(value?: unknown) {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value;
}
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : value.split(",").filter(Boolean);
} catch {
return value.split(",").filter(Boolean);
}
}
return [];
} }
// 关闭通知公告弹窗 // 关闭通知公告弹窗

View File

@@ -96,14 +96,14 @@
<template #default="scope"> <template #default="scope">
<el-button <el-button
v-if="!isPlatformTenantId(scope.row.id)" v-if="!isPlatformTenantId(scope.row.id)"
v-hasPerm="['sys:tenant:assign']" v-hasPerm="['sys:tenant:plan-assign']"
type="primary" type="primary"
size="small" size="small"
link link
icon="menu" icon="menu"
@click="handleOpenTenantMenuDialog(scope.row)" @click="handleOpenTenantPlanDialog(scope.row)"
> >
租户菜单 设置套餐
</el-button> </el-button>
<el-button <el-button
v-hasPerm="['sys:tenant:update']" v-hasPerm="['sys:tenant:update']"
@@ -163,7 +163,11 @@
<el-input v-model="formData.domain" placeholder="demo.youlai.tech可选" /> <el-input v-model="formData.domain" placeholder="demo.youlai.tech可选" />
</el-form-item> </el-form-item>
<el-form-item v-if="!isPlatformTenant" label="租户套餐" prop="planId"> <el-form-item
v-if="!isPlatformTenant && (formData.id == null || String(formData.id) === '')"
label="租户套餐"
prop="planId"
>
<el-select v-model="formData.planId" placeholder="请选择租户套餐" style="width: 100%"> <el-select v-model="formData.planId" placeholder="请选择租户套餐" style="width: 100%">
<el-option <el-option
v-for="item in planOptions" v-for="item in planOptions"
@@ -228,13 +232,39 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 方案菜单配 --> <!-- 租户套餐设 -->
<el-drawer <el-drawer
v-model="tenantMenuDialogVisible" v-model="tenantPlanDialogVisible"
:title="'【' + checkedTenant.name + '】租户菜单配置'" :title="'【' + checkedTenant.name + '】设置套餐'"
size="600px" size="640px"
@close="handleCloseTenantMenuDialog" @close="handleCloseTenantPlanDialog"
> >
<el-form label-width="90px" class="mb-3">
<el-form-item label="租户套餐">
<el-select
v-model="tenantPlanId"
placeholder="请选择租户套餐"
style="width: 100%"
@change="handlePlanChange"
>
<el-option
v-for="item in planOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<el-alert
type="info"
show-icon
:closable="false"
title="默认展示套餐菜单,如需微调请开启自定义"
class="mb-3"
/>
<div class="flex-x-between"> <div class="flex-x-between">
<el-input v-model="menuKeywords" clearable class="w-[150px]" placeholder="菜单名称"> <el-input v-model="menuKeywords" clearable class="w-[150px]" placeholder="菜单名称">
<template #prefix> <template #prefix>
@@ -249,14 +279,25 @@
</template> </template>
{{ menuExpanded ? "收缩" : "展开" }} {{ menuExpanded ? "收缩" : "展开" }}
</el-button> </el-button>
<el-checkbox v-model="menuParentChildLinked" class="ml-5" @change="handleMenuLinkChange"> <el-checkbox
v-model="menuParentChildLinked"
class="ml-5"
:disabled="!menuCustomizeEnabled"
@change="handleMenuLinkChange"
>
父子联动 父子联动
</el-checkbox> </el-checkbox>
<el-switch
v-model="menuCustomizeEnabled"
class="ml-5"
inline-prompt
active-text="自定义"
inactive-text="默认"
:disabled="!hasPermTenantMenu"
@change="handleCustomizeToggle"
/>
<el-tooltip placement="bottom"> <el-tooltip placement="bottom">
<template #content> <template #content>开启自定义后可覆盖套餐菜单关闭则仅使用套餐默认菜单</template>
如果只需勾选菜单权限不需要勾选子菜单或者按钮权限请关闭父子联动
</template>
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer"> <el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
<QuestionFilled /> <QuestionFilled />
</el-icon> </el-icon>
@@ -268,6 +309,7 @@
ref="menuTreeRef" ref="menuTreeRef"
node-key="value" node-key="value"
show-checkbox show-checkbox
:props="menuTreeProps"
:data="menuPermOptions" :data="menuPermOptions"
:filter-node-method="handleMenuFilter" :filter-node-method="handleMenuFilter"
:default-expand-all="true" :default-expand-all="true"
@@ -282,13 +324,13 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button <el-button
v-hasPerm="['sys:tenant:assign']" v-hasPerm="['sys:tenant:update']"
type="primary" type="primary"
@click="handleTenantMenuSubmit" @click="handleTenantPlanSubmit"
> >
确定 保存设置
</el-button> </el-button>
<el-button @click="tenantMenuDialogVisible = false">取消</el-button> <el-button @click="tenantPlanDialogVisible = false">取消</el-button>
</div> </div>
</template> </template>
</el-drawer> </el-drawer>
@@ -341,11 +383,23 @@ const dialog = reactive({
visible: false, visible: false,
}); });
const tenantMenuDialogVisible = 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 tenantPlanId = ref<number | undefined>();
const menuCustomizeEnabled = ref(false);
const planMenuIds = ref<number[]>([]);
const tenantMenuIds = ref<number[]>([]);
const menuKeywords = ref(""); const menuKeywords = ref("");
const menuExpanded = ref(true); const menuExpanded = ref(true);
const menuParentChildLinked = ref(true); const menuParentChildLinked = ref(true);
const menuSourceOptions = ref<OptionItem[]>([]);
const menuTreeProps = {
children: "children",
label: "label",
disabled: "disabled",
};
const planOptions = ref<OptionItem[]>([]); const planOptions = ref<OptionItem[]>([]);
@@ -374,9 +428,10 @@ const rules = reactive({
code: [{ required: true, message: "请输入租户编码", trigger: "blur" }], code: [{ required: true, message: "请输入租户编码", trigger: "blur" }],
planId: [ planId: [
{ {
// 平台租户不绑定套餐 // 平台租户不绑定套餐,仅创建时校验
validator: (_: unknown, value: number | undefined, callback: (error?: Error) => void) => { validator: (_: unknown, value: number | undefined, callback: (error?: Error) => void) => {
if (isPlatformTenant.value) return callback(); if (isPlatformTenant.value) return callback();
if (formData.id != null && String(formData.id) !== "") return callback();
if (value == null) return callback(new Error("请选择租户套餐")); if (value == null) return callback(new Error("请选择租户套餐"));
return callback(); return callback();
}, },
@@ -386,6 +441,7 @@ const rules = reactive({
}); });
const hasPermChangeStatus = computed(() => hasPerm("sys:tenant:change-status")); const hasPermChangeStatus = computed(() => hasPerm("sys:tenant:change-status"));
const hasPermTenantMenu = computed(() => hasPerm("sys:tenant:plan-assign"));
function handleStatusChange(tenantId: string | number | undefined, val: string | number | boolean) { function handleStatusChange(tenantId: string | number | undefined, val: string | number | boolean) {
if (tenantId == null) return; if (tenantId == null) return;
@@ -415,60 +471,54 @@ function fetchData() {
}); });
} }
async function handleOpenTenantMenuDialog(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)) { if (isPlatformTenantId(tenantId)) return;
return;
}
const planId = row.planId;
if (!planId) {
ElMessage.warning("请先为租户选择套餐");
return;
}
tenantMenuDialogVisible.value = true; tenantPlanDialogVisible.value = true;
loading.value = true; loading.value = true;
menuCustomizeEnabled.value = false;
menuKeywords.value = "";
menuExpanded.value = true;
menuParentChildLinked.value = true;
checkedTenant.value = { checkedTenant.value = {
id: Number(tenantId), id: Number(tenantId),
name: row.name || String(tenantId), name: row.name || String(tenantId),
planId, planId: row.planId != null ? Number(row.planId) : undefined,
}; };
try { try {
const [menuOptions, planMenuIds, menuIds] = await Promise.all([ const [tenantForm, menuOptions, menuIds] = await Promise.all([
TenantAPI.getFormData(String(tenantId)),
MenuAPI.getOptions(false, MenuScopeEnum.TENANT), MenuAPI.getOptions(false, MenuScopeEnum.TENANT),
TenantPlanAPI.getPlanMenuIds(planId), hasPermTenantMenu.value ? TenantAPI.getTenantMenuIds(Number(tenantId)) : Promise.resolve([]),
TenantAPI.getTenantMenuIds(Number(tenantId)),
]); ]);
const normalizedPlanMenuIds = planMenuIds checkedTenantForm.value = tenantForm;
.map((menuId) => Number(menuId)) tenantPlanId.value = tenantForm.planId != null ? Number(tenantForm.planId) : undefined;
.filter((menuId) => !Number.isNaN(menuId)); menuSourceOptions.value = menuOptions;
const allowedMenuIdSet = new Set(normalizedPlanMenuIds); tenantMenuIds.value = normalizeMenuIds(menuIds);
menuPermOptions.value = allowedMenuIdSet.size await handlePlanChange(tenantPlanId.value);
? filterMenuOptionsByIds(menuOptions, allowedMenuIdSet)
: menuOptions;
const normalizedMenuIds = menuIds
.map((menuId) => Number(menuId))
.filter((menuId) => !Number.isNaN(menuId));
await nextTick();
menuTreeRef.value?.setCheckedKeys([], false);
const checkedMenuIds = allowedMenuIdSet.size
? normalizedMenuIds.filter((menuId) => allowedMenuIdSet.has(menuId))
: normalizedMenuIds;
checkedMenuIds.forEach((menuId) => menuTreeRef.value?.setChecked(menuId, true, false));
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
function handleCloseTenantMenuDialog() { function handleCloseTenantPlanDialog() {
tenantMenuDialogVisible.value = false; tenantPlanDialogVisible.value = false;
menuKeywords.value = ""; menuKeywords.value = "";
menuExpanded.value = true; menuExpanded.value = true;
menuParentChildLinked.value = true; menuParentChildLinked.value = true;
menuTreeRef.value?.setCheckedKeys([], false); menuCustomizeEnabled.value = false;
tenantPlanId.value = undefined;
planMenuIds.value = [];
tenantMenuIds.value = [];
menuSourceOptions.value = [];
menuPermOptions.value = [];
checkedTenant.value = {}; checkedTenant.value = {};
checkedTenantForm.value = null;
menuTreeRef.value?.setCheckedKeys([], false);
} }
function toggleMenuTree() { function toggleMenuTree() {
@@ -488,6 +538,51 @@ function handleMenuLinkChange(val: string | number | boolean) {
menuParentChildLinked.value = Boolean(val); menuParentChildLinked.value = Boolean(val);
} }
function handleCustomizeToggle() {
menuPermOptions.value = applyMenuOptionsDisabled(
menuPermOptions.value,
!menuCustomizeEnabled.value
);
updateCheckedMenus();
}
async function handlePlanChange(planId?: number) {
if (!planId) {
planMenuIds.value = [];
menuPermOptions.value = applyMenuOptionsDisabled(menuSourceOptions.value, true);
await nextTick();
menuTreeRef.value?.setCheckedKeys([], false);
return;
}
loading.value = true;
try {
const menuIds = await TenantPlanAPI.getPlanMenuIds(planId);
planMenuIds.value = normalizeMenuIds(menuIds);
const allowedMenuIdSet = new Set(planMenuIds.value);
const filteredOptions = allowedMenuIdSet.size
? filterMenuOptionsByIds(menuSourceOptions.value, allowedMenuIdSet)
: menuSourceOptions.value;
menuPermOptions.value = applyMenuOptionsDisabled(filteredOptions, !menuCustomizeEnabled.value);
await nextTick();
updateCheckedMenus();
} finally {
loading.value = false;
}
}
function updateCheckedMenus() {
const allowedMenuIdSet = new Set(planMenuIds.value);
const baseCheckedIds =
menuCustomizeEnabled.value && tenantMenuIds.value.length > 0
? tenantMenuIds.value
: planMenuIds.value;
const checkedMenuIds = allowedMenuIdSet.size
? baseCheckedIds.filter((menuId) => allowedMenuIdSet.has(menuId))
: baseCheckedIds;
menuTreeRef.value?.setCheckedKeys([], false);
checkedMenuIds.forEach((menuId) => menuTreeRef.value?.setChecked(menuId, true, false));
}
watch(menuKeywords, (val) => { watch(menuKeywords, (val) => {
menuTreeRef.value?.filter(val); menuTreeRef.value?.filter(val);
}); });
@@ -516,26 +611,58 @@ function filterMenuOptionsByIds(
}, []); }, []);
} }
async function handleTenantMenuSubmit() { async function handleTenantPlanSubmit() {
const tenantId = checkedTenant.value.id; const tenantId = checkedTenant.value.id;
if (!tenantId) return; if (!tenantId) return;
if (!tenantPlanId.value) {
ElMessage.warning("请选择租户套餐");
return;
}
const checkedMenuIds: number[] = menuTreeRef const tenantForm = checkedTenantForm.value;
.value!.getCheckedNodes(false, true) if (!tenantForm) return;
.map((node: any) => node.value);
loading.value = true; loading.value = true;
try { try {
await TenantAPI.updateTenantMenus(tenantId, checkedMenuIds); const payload: TenantForm = {
ElMessage.success("租户菜单配置成功"); ...tenantForm,
tenantMenuDialogVisible.value = false; planId: tenantPlanId.value,
};
await TenantAPI.update(String(tenantId), payload);
if (hasPermTenantMenu.value) {
const allowedMenuIdSet = new Set(planMenuIds.value);
const menuIds = menuCustomizeEnabled.value
? menuTreeRef.value!.getCheckedNodes(false, true).map((node: any) => node.value)
: planMenuIds.value;
const filteredMenuIds = allowedMenuIdSet.size
? menuIds.filter((menuId) => allowedMenuIdSet.has(menuId))
: menuIds;
await TenantAPI.updateTenantMenus(tenantId, filteredMenuIds);
}
ElMessage.success("套餐设置成功");
tenantPlanDialogVisible.value = false;
fetchData();
} catch { } catch {
ElMessage.error("租户菜单配置失败"); ElMessage.error("套餐设置失败");
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
function normalizeMenuIds(menuIds: Array<number | string>) {
return menuIds.map((menuId) => Number(menuId)).filter((menuId) => !Number.isNaN(menuId));
}
function applyMenuOptionsDisabled(options: OptionItem[], disabled: boolean): OptionItem[] {
return options.map((option) => ({
...option,
disabled,
children: option.children ? applyMenuOptionsDisabled(option.children, disabled) : undefined,
}));
}
function handleQuery() { function handleQuery() {
queryParams.pageNum = 1; queryParams.pageNum = 1;
fetchData(); fetchData();