mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-01 00:05:17 +00:00
feat: 移除系统管理路由并优化路由结构
- 删除不再使用的系统管理路由模块 - 更新路由配置,确保仅保留必要的模块 - 添加Element Plus图标全局注册功能 - 优化分页组件,增加更多属性支持 - 重构表格操作列渲染逻辑,提升代码可读性和复用性 - 更新多个组件以支持新的操作列渲染方式
This commit is contained in:
@@ -10,7 +10,7 @@ VITE_PORT = 5180
|
||||
VITE_BASE_URL = /
|
||||
|
||||
# 权限模式【 frontend 仅前端路由 / backend 仅后端菜单 / mixed 后端菜单+前端路由模块合并 】
|
||||
VITE_ACCESS_MODE = mixed
|
||||
VITE_ACCESS_MODE = frontend
|
||||
|
||||
# 跨域请求时是否携带 Cookie(开启前需确保后端支持)
|
||||
VITE_WITH_CREDENTIALS = false
|
||||
|
||||
Submodule
+1
Submodule frontend/new-web/art-design-pro added at 6b6d781a63
@@ -156,7 +156,6 @@ const targets = [
|
||||
"CHANGELOG.md",
|
||||
"CHANGELOG.zh-CN.md",
|
||||
"src/views/safeguard",
|
||||
"src/views/system/nested",
|
||||
"src/views/widgets",
|
||||
"src/views/dashboard/analysis",
|
||||
"src/views/dashboard/ecommerce",
|
||||
@@ -254,73 +253,14 @@ async function cleanRouteModules() {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
// 重写 system.ts - 移除 nested 嵌套菜单
|
||||
const systemContent = `import { AppRouteRecord } from '@/types/router'
|
||||
|
||||
export const systemRoutes: AppRouteRecord = {
|
||||
path: '/art-system',
|
||||
name: 'ArtSystem',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'menus.system.title',
|
||||
icon: 'ri:user-3-line',
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'ArtTplUser',
|
||||
component: '/system/user',
|
||||
meta: {
|
||||
title: 'menus.system.user',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
name: 'ArtTplRole',
|
||||
component: '/system/role',
|
||||
meta: {
|
||||
title: 'menus.system.role',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
name: 'ArtTplMenus',
|
||||
component: '/system/menu',
|
||||
meta: {
|
||||
title: 'menus.system.menu',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER'],
|
||||
authList: [
|
||||
{ title: '新增', authMark: 'add' },
|
||||
{ title: '编辑', authMark: 'edit' },
|
||||
{ title: '删除', authMark: 'delete' }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
await fs.writeFile(path.join(modulesPath, "system.ts"), systemContent, "utf-8");
|
||||
|
||||
// 重写 index.ts - 只导入保留的模块
|
||||
// 重写 index.ts:widgets 已删则仅保留 exception
|
||||
const indexContent = `import { AppRouteRecord } from '@/types/router'
|
||||
import { systemRoutes } from './system'
|
||||
import { resultRoutes } from './result'
|
||||
import { exceptionRoutes } from './exception'
|
||||
|
||||
/**
|
||||
* 导出所有模块化路由
|
||||
* 导出所有模块化路由(业务菜单由后端动态下发)
|
||||
*/
|
||||
export const routeModules: AppRouteRecord[] = [
|
||||
systemRoutes,
|
||||
resultRoutes,
|
||||
exceptionRoutes
|
||||
]
|
||||
export const routeModules: AppRouteRecord[] = [exceptionRoutes]
|
||||
`;
|
||||
await fs.writeFile(path.join(modulesPath, "index.ts"), indexContent, "utf-8");
|
||||
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:background="background"
|
||||
:disabled="disabled"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:pager-count="pagerCount"
|
||||
:size="size"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
@@ -17,6 +20,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, type PropType } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
total: {
|
||||
type: Number as PropType<number>,
|
||||
@@ -25,7 +30,7 @@ const props = defineProps({
|
||||
pageSizes: {
|
||||
type: Array as PropType<number[]>,
|
||||
default() {
|
||||
return [10, 20, 30, 50];
|
||||
return [10, 20, 30, 50, 100];
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
@@ -36,6 +41,20 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 页码按钮数量(透传 ElPagination) */
|
||||
pagerCount: {
|
||||
type: Number as PropType<number>,
|
||||
default: undefined,
|
||||
},
|
||||
/** 分页器尺寸(透传 ElPagination) */
|
||||
size: {
|
||||
type: String as PropType<"" | "default" | "small" | "large">,
|
||||
default: undefined,
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
<!-- 渲染展开行 -->
|
||||
<ElTableColumn v-else-if="col.type === 'expand'" v-bind="cleanColumnProps(col)">
|
||||
<template #default="{ row }">
|
||||
<component :is="col.formatter ? col.formatter(row) : null" />
|
||||
<template #default="{ row: expandRow }">
|
||||
<component :is="col.formatter ? col.formatter(expandRow) : null" />
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
|
||||
<!-- 渲染普通列 -->
|
||||
<ElTableColumn v-else v-bind="cleanColumnProps(col)">
|
||||
<!-- 渲染普通列:default 插槽须紧凑书写,避免空白文本抢占 formatter(见 renderColumnFormatter 注释) -->
|
||||
<ElTableColumn v-else v-bind="cleanBodyColumnProps(col)">
|
||||
<template v-if="col.useHeaderSlot && col.prop" #header="headerScope">
|
||||
<slot
|
||||
:name="col.headerSlotName || `${col.prop}-header`"
|
||||
@@ -30,17 +30,9 @@
|
||||
{{ col.label }}
|
||||
</slot>
|
||||
</template>
|
||||
<template v-if="col.useSlot && col.prop" #default="slotScope">
|
||||
<slot
|
||||
v-if="shouldRenderSlotScope(slotScope)"
|
||||
:name="col.slotName || col.prop"
|
||||
v-bind="{
|
||||
...slotScope,
|
||||
prop: col.prop,
|
||||
value: col.prop ? slotScope.row[col.prop] : undefined,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<!-- 整段单行:插槽内兄弟节点之间的空白文本会让 EP 误判单元格内容 -->
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line, prettier/prettier -->
|
||||
<template #default="slotScope"><slot v-if="col.useSlot && col.prop && shouldRenderSlotScope(slotScope)" :name="col.slotName || col.prop" v-bind="{ ...slotScope, prop: col.prop, value: col.prop ? slotScope.row[col.prop] : undefined }" /><TableFormatterOutlet v-else-if="col.formatter && !col.useSlot && shouldRenderSlotScope(slotScope)" :column="col" :record="slotScope.row" /></template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
|
||||
@@ -58,21 +50,37 @@
|
||||
:class="mergedPaginationOptions?.align"
|
||||
ref="paginationRef"
|
||||
>
|
||||
<ElPagination
|
||||
v-bind="mergedPaginationOptions"
|
||||
:total="pagination?.total"
|
||||
:disabled="loading"
|
||||
:page-size="pagination?.size"
|
||||
:current-page="pagination?.current"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
<Pagination
|
||||
v-if="pagination"
|
||||
:page="pagination.current"
|
||||
:limit="pagination.size"
|
||||
:total="pagination.total"
|
||||
:page-sizes="mergedPaginationOptions.pageSizes"
|
||||
:layout="mergedPaginationOptions.layout"
|
||||
:background="mergedPaginationOptions.background ?? true"
|
||||
:disabled="!!loading"
|
||||
:hidden="paginationHidden"
|
||||
:pager-count="mergedPaginationOptions.pagerCount"
|
||||
:size="mergedPaginationOptions.size"
|
||||
@pagination="handlePaginationEvent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watchEffect, getCurrentInstance, useAttrs } from "vue";
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
nextTick,
|
||||
watchEffect,
|
||||
getCurrentInstance,
|
||||
useAttrs,
|
||||
isVNode,
|
||||
h,
|
||||
defineComponent,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import type { ElTable, TableProps } from "element-plus";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ColumnOption } from "@/types";
|
||||
@@ -80,6 +88,7 @@ import { useTableStore } from "@/store/modules/table.store";
|
||||
import { useCommon } from "@/hooks/core/useCommon";
|
||||
import { useTableHeight } from "@/hooks/core/useTableHeight";
|
||||
import { useResizeObserver, useWindowSize } from "@vueuse/core";
|
||||
import Pagination from "@/components/Pagination/index.vue";
|
||||
|
||||
defineOptions({ name: "ArtTable" });
|
||||
|
||||
@@ -183,6 +192,17 @@ const mergedPaginationOptions = computed(() => ({
|
||||
...props.paginationOptions,
|
||||
}));
|
||||
|
||||
/** 对齐 ElPagination hide-on-single-page,交给封装组件的 hidden */
|
||||
const paginationHidden = computed(() => {
|
||||
const p = props.pagination;
|
||||
const opts = mergedPaginationOptions.value;
|
||||
if (!p || !opts.hideOnSinglePage) return false;
|
||||
const size = p.size || 10;
|
||||
const total = p.total ?? 0;
|
||||
if (total <= 0) return false;
|
||||
return Math.ceil(total / size) <= 1;
|
||||
});
|
||||
|
||||
// 边框 (优先级:props > store)
|
||||
const border = computed(() => props.border ?? isBorder.value);
|
||||
// 斑马纹
|
||||
@@ -269,6 +289,11 @@ const mergedTableProps = computed(() => ({
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "pagination:size-change", val: number): void;
|
||||
(e: "pagination:current-change", val: number): void;
|
||||
}>();
|
||||
|
||||
// 是否显示分页器
|
||||
const showPagination = computed(() => props.pagination && !isEmpty.value);
|
||||
|
||||
@@ -278,6 +303,36 @@ const shouldRenderSlotScope = (slotScope: { $index?: number }) => {
|
||||
return slotScope.$index === undefined || slotScope.$index >= 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* ElTableColumn 若存在 default 插槽且插槽产物含任意非 Comment 的 vnode(含空白文本节点),
|
||||
* 将不会执行 formatter(见 element-plus render-helper setColumnRenders)。
|
||||
* ArtTable 中 ElTableColumn 与子节点之间的换行/缩进可能被编译进默认插槽,导致 formatter(如操作列里的按钮)永远不渲染。
|
||||
* 对声明了 formatter 且未使用 useSlot 的列,在此显式渲染 formatter 返回值。
|
||||
*/
|
||||
const renderColumnFormatter = (col: ColumnOption, row: Record<string, unknown>) => {
|
||||
if (!col.formatter) return null;
|
||||
const result = col.formatter(row as never);
|
||||
if (isVNode(result)) return result;
|
||||
if (result === null || result === undefined) return null;
|
||||
return h("span", String(result));
|
||||
};
|
||||
|
||||
/**
|
||||
* 在 render 里调用 formatter(row) 生成 VNode;勿把 VNode 当 props 传入(克隆后会失效)。
|
||||
* 与 renderColumnFormatter 同文件定义,保证闭包一致。
|
||||
*/
|
||||
const TableFormatterOutlet = defineComponent({
|
||||
name: "TableFormatterOutlet",
|
||||
props: {
|
||||
column: { type: Object as PropType<ColumnOption>, required: true },
|
||||
/** 避免 prop 名 row 与插槽解构冲突 */
|
||||
record: { type: Object as PropType<Record<string, unknown>>, required: true },
|
||||
},
|
||||
setup(props) {
|
||||
return () => renderColumnFormatter(props.column, props.record);
|
||||
},
|
||||
});
|
||||
|
||||
// 清理列属性,移除插槽相关的自定义属性,确保它们不会被 ElTableColumn 错误解释
|
||||
const cleanColumnProps = (col: ColumnOption) => {
|
||||
const columnProps = { ...col };
|
||||
@@ -289,15 +344,11 @@ const cleanColumnProps = (col: ColumnOption) => {
|
||||
return columnProps;
|
||||
};
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (val: number) => {
|
||||
emit("pagination:size-change", val);
|
||||
};
|
||||
|
||||
// 分页当前页变化
|
||||
const handleCurrentChange = (val: number) => {
|
||||
emit("pagination:current-change", val);
|
||||
scrollToTop(); // 页码改变后滚动到表格顶部
|
||||
/** 普通列:单元格已由插槽内 TableFormatterOutlet 渲染,勿再把 formatter 传给 ElTableColumn,避免与 EP 内置 renderCell 混用 */
|
||||
const cleanBodyColumnProps = (col: ColumnOption) => {
|
||||
const columnProps = cleanColumnProps(col);
|
||||
delete columnProps.formatter;
|
||||
return columnProps;
|
||||
};
|
||||
|
||||
const { scrollToTop: scrollPageToTop } = useCommon();
|
||||
@@ -310,6 +361,20 @@ const scrollToTop = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/** 对接封装分页 @pagination,保持对外仍为 size-change / current-change 事件 */
|
||||
const handlePaginationEvent = (payload: { page: number; limit: number }) => {
|
||||
const p = props.pagination;
|
||||
if (!p) return;
|
||||
if (payload.limit !== p.size) {
|
||||
emit("pagination:size-change", payload.limit);
|
||||
return;
|
||||
}
|
||||
if (payload.page !== p.current) {
|
||||
emit("pagination:current-change", payload.page);
|
||||
scrollToTop();
|
||||
}
|
||||
};
|
||||
|
||||
// 全局序号
|
||||
const getGlobalIndex = (index: number) => {
|
||||
if (!props.pagination) return index + 1;
|
||||
@@ -317,11 +382,6 @@ const getGlobalIndex = (index: number) => {
|
||||
return (current - 1) * size + index + 1;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "pagination:size-change", val: number): void;
|
||||
(e: "pagination:current-change", val: number): void;
|
||||
}>();
|
||||
|
||||
// 查找并绑定表格头部元素 - 使用 VueUse 优化
|
||||
const findTableHeader = () => {
|
||||
if (!props.showTableHeader) {
|
||||
|
||||
@@ -19,20 +19,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
import { useSettingsStore } from "@/store/modules/setting.store";
|
||||
import { useCeremony } from "@/hooks/core/useCeremony";
|
||||
import { WEB_LINKS } from "@/utils/constants";
|
||||
|
||||
defineOptions({ name: "ArtFestivalTextScroll" });
|
||||
|
||||
const route = useRoute();
|
||||
const settingStore = useSettingsStore();
|
||||
const { showFestivalText } = storeToRefs(settingStore);
|
||||
const { currentFestivalData, closeFestivalScroll } = useCeremony();
|
||||
|
||||
const isFullPageLayout = computed(() => route.matched.some((r) => r.meta?.isFullPage));
|
||||
|
||||
function versionLabel(): string {
|
||||
const v = String(import.meta.env.VITE_VERSION ?? "").trim();
|
||||
if (!v) return "";
|
||||
@@ -49,7 +45,6 @@ const showFestivalStrip = computed(
|
||||
() =>
|
||||
showFestivalText.value &&
|
||||
!!currentFestivalData.value?.scrollText &&
|
||||
currentFestivalData.value.scrollText !== "" &&
|
||||
!isFullPageLayout.value
|
||||
currentFestivalData.value.scrollText !== ""
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -114,7 +114,7 @@ export const SETTING_DEFAULT_CONFIG = {
|
||||
/** 是否显示语言切换 */
|
||||
showLanguage: true,
|
||||
/** 是否显示进度条 */
|
||||
showNprogress: false,
|
||||
showNprogress: true,
|
||||
/** 是否显示设置引导 */
|
||||
showSettingGuide: true,
|
||||
/** 是否显示节日文本 */
|
||||
|
||||
@@ -35,13 +35,13 @@ import { storeToRefs } from "pinia";
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { useAppMode } from "@/hooks/core/useAppMode";
|
||||
import type { AppRouteRecord } from "@/types/router";
|
||||
import { ROLE_ROOT } from "@/constants";
|
||||
|
||||
type AuthItem = NonNullable<AppRouteRecord["meta"]["authList"]>[number];
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
export const useAuth = () => {
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const { isFrontendMode } = useAppMode();
|
||||
const { info } = storeToRefs(userStore);
|
||||
|
||||
@@ -54,17 +54,28 @@ export const useAuth = () => {
|
||||
: [];
|
||||
|
||||
/**
|
||||
* 检查是否拥有某权限标识(前后端模式通用)
|
||||
* @param auth 权限标识
|
||||
* @returns 是否有权限
|
||||
* 检查是否拥有某权限标识(与 v-hasPerm / 表格内 ArtButtonMore 全码一致)
|
||||
* @param auth 权限标识(多为后端菜单 permission 全码,如 module_system:role:update)
|
||||
*/
|
||||
const hasAuth = (auth: string): boolean => {
|
||||
// 前端模式
|
||||
if (!auth) return true;
|
||||
|
||||
const userPrems = userStore.prems;
|
||||
const roles = userStore.basicInfo?.roles as { code?: string }[] | undefined;
|
||||
|
||||
// 与 directives/permission hasPerm 一致:超级管理员、通配
|
||||
if (roles?.some((r) => r.code === ROLE_ROOT)) return true;
|
||||
if (userPrems.includes("*:*:*")) return true;
|
||||
|
||||
/** 登录后由菜单树汇总的全局权限码(与 v-hasPerm 同源) */
|
||||
if (userPrems.includes(auth)) return true;
|
||||
|
||||
// 前端模式:兼容 info.permissions
|
||||
if (isFrontendMode.value) {
|
||||
return frontendAuthList.includes(auth);
|
||||
}
|
||||
|
||||
// 后端模式
|
||||
// 后端模式:当前路由 meta 短标识(与 v-auth 一致)
|
||||
return backendAuthList.some((item) => item?.authMark === auth);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
<!-- 布局内容 -->
|
||||
<template>
|
||||
<div
|
||||
id="app-scroll-main"
|
||||
class="layout-content"
|
||||
:class="{ 'overflow-auto': isFullPage }"
|
||||
:style="containerStyle"
|
||||
>
|
||||
<div id="app-scroll-main" class="layout-content" :style="containerStyle">
|
||||
<div id="app-content-header">
|
||||
<!-- 节日滚动 -->
|
||||
<ArtFestivalTextScroll v-if="!isFullPage" />
|
||||
<ArtFestivalTextScroll />
|
||||
|
||||
<!-- 路由信息调试 -->
|
||||
<div
|
||||
@@ -20,40 +15,28 @@
|
||||
</div>
|
||||
|
||||
<RouterView v-if="isRefresh" v-slot="{ Component, route: router }" :style="contentStyle">
|
||||
<!-- 缓存路由动画 -->
|
||||
<Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
|
||||
<KeepAlive :max="10" :exclude="keepAliveExclude">
|
||||
<Transition :name="actualTransition" mode="out-in" appear>
|
||||
<div class="route-view-shell flex min-h-0 flex-1 flex-col">
|
||||
<KeepAlive :max="10" :exclude="keepAliveExclude">
|
||||
<component
|
||||
v-if="router.meta.keepAlive === true"
|
||||
class="art-page-view min-h-0 flex-1"
|
||||
:is="Component"
|
||||
:key="router.fullPath"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<component
|
||||
class="art-page-view"
|
||||
v-if="router.meta.keepAlive !== true"
|
||||
class="art-page-view min-h-0 flex-1"
|
||||
:is="Component"
|
||||
:key="router.path"
|
||||
v-if="router.meta.keepAlive"
|
||||
:key="router.fullPath"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
|
||||
<!-- 非缓存路由动画 -->
|
||||
<Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
|
||||
<component
|
||||
class="art-page-view"
|
||||
:is="Component"
|
||||
:key="router.path"
|
||||
v-if="!router.meta.keepAlive"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
|
||||
<!-- 全屏页面切换过渡遮罩(用于提升页面切换视觉体验) -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-show="showTransitionMask"
|
||||
class="fixed top-0 left-0 z-[2000] w-screen h-screen pointer-events-none bg-box"
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
<!-- 返回顶部:宽屏 #app-scroll-main;窄屏文档滚动 target 置空 -->
|
||||
<ElBacktop
|
||||
v-if="!isFullPage"
|
||||
:key="backtopTargetKey"
|
||||
:target="backtopScrollTarget"
|
||||
:right="28"
|
||||
@@ -86,65 +69,27 @@ const { keepAliveExclude } = storeToRefs(useWorktabStore());
|
||||
|
||||
const isRefresh = shallowRef(true);
|
||||
const isOpenRouteInfo = import.meta.env.VITE_OPEN_ROUTE_INFO;
|
||||
const showTransitionMask = ref(false);
|
||||
|
||||
// 标记是否是首次加载(浏览器刷新)
|
||||
const isFirstLoad = ref(true);
|
||||
|
||||
// 检查当前路由是否需要使用无基础布局模式
|
||||
const isFullPage = computed(() => route.matched.some((r) => r.meta?.isFullPage));
|
||||
const prevIsFullPage = ref(isFullPage.value);
|
||||
|
||||
// 切换动画名称:首次加载、从全屏返回时不使用动画
|
||||
const actualTransition = computed(() => {
|
||||
if (isFirstLoad.value) return "";
|
||||
if (prevIsFullPage.value && !isFullPage.value) return "";
|
||||
return pageTransition.value;
|
||||
});
|
||||
|
||||
// 监听全屏状态变化,显示过渡遮罩
|
||||
watch(isFullPage, (val, oldVal) => {
|
||||
if (val !== oldVal) {
|
||||
showTransitionMask.value = true;
|
||||
// 延迟隐藏遮罩,给足时间让页面完成切换
|
||||
setTimeout(() => {
|
||||
showTransitionMask.value = false;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
prevIsFullPage.value = val;
|
||||
});
|
||||
});
|
||||
|
||||
const containerStyle = computed(
|
||||
(): CSSProperties =>
|
||||
isFullPage.value
|
||||
? {
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100vh",
|
||||
zIndex: 2500,
|
||||
background: "var(--default-bg-color)",
|
||||
}
|
||||
: {
|
||||
maxWidth: containerWidth.value,
|
||||
flex: "1",
|
||||
minHeight: "0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}
|
||||
(): CSSProperties => ({
|
||||
maxWidth: containerWidth.value,
|
||||
flex: "1",
|
||||
minHeight: "0",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
})
|
||||
);
|
||||
|
||||
/** 常规布局下由 `.layout-content` 承担纵向滚动,路由视图填满剩余高度 */
|
||||
const contentStyle = computed(
|
||||
(): CSSProperties =>
|
||||
isFullPage.value
|
||||
? { flex: "1", minHeight: "0", display: "flex", flexDirection: "column" }
|
||||
: { flex: "1", minHeight: "0" }
|
||||
);
|
||||
const contentStyle = computed((): CSSProperties => ({ flex: "1", minHeight: "0" }));
|
||||
|
||||
const reload = () => {
|
||||
isRefresh.value = false;
|
||||
|
||||
@@ -8,6 +8,7 @@ import "@styles/index.scss"; // 样式
|
||||
import "@utils/sys/console.ts"; // 控制台输出内容
|
||||
import { initGlobDirectives } from "./directives";
|
||||
import { setupErrorHandle } from "./utils/sys/error-handle";
|
||||
import { initElIcons } from "@/plugins/icons";
|
||||
|
||||
// 暗黑主题样式
|
||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
||||
@@ -21,6 +22,8 @@ import { createTerminal } from "vue-web-terminal";
|
||||
document.addEventListener("touchstart", function () {}, { passive: false });
|
||||
|
||||
const app = createApp(App);
|
||||
/** Element Plus 图标(@element-plus/icons-vue)全局注册,模板中可直接 <Cpu />、<QuestionFilled /> 等 */
|
||||
initElIcons(app);
|
||||
initStore(app);
|
||||
initRouter(app);
|
||||
initGlobDirectives(app);
|
||||
|
||||
@@ -121,7 +121,7 @@ export function setupBeforeEachGuard(router: Router): void {
|
||||
} catch (error) {
|
||||
console.error("[RouteGuard] 路由守卫处理失败:", error);
|
||||
closeLoading();
|
||||
next({ name: "Exception500" });
|
||||
next({ name: "500" });
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -168,7 +168,7 @@ async function handleRouteGuard(
|
||||
next();
|
||||
} else {
|
||||
// 未匹配到路由,跳转到 500 页面
|
||||
next({ name: "Exception500", replace: true });
|
||||
next({ name: "500", replace: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -204,7 +204,7 @@ async function handleRouteGuard(
|
||||
}
|
||||
|
||||
// 6. 未匹配到路由,跳转到 404
|
||||
next({ name: "Exception404" });
|
||||
next({ name: "404" });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,9 +262,8 @@ function resolveStaticChildFullPath(parentFullPath: string, segment: string): st
|
||||
function isStaticRoute(path: string): boolean {
|
||||
const checkRoute = (routes: any[], targetPath: string, parentFullPath = ""): boolean => {
|
||||
return routes.some((route) => {
|
||||
// 404 catch-all 路由不应视为可匿名访问的静态页,
|
||||
// 否则未登录时手动输入任意地址会直接落到 404,无法跳转登录页。
|
||||
if (route.name === "Exception404") {
|
||||
// 通配 404(pathMatch)不应视为免登录静态页;静态表里可能与 `/404` 同名,按 path 区分。
|
||||
if (route.path === "/:pathMatch(.*)*") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -416,7 +415,7 @@ async function handleDynamicRoutes(
|
||||
}
|
||||
|
||||
// 跳转到 500 页面,使用 replace 避免产生历史记录
|
||||
next({ name: "Exception500", replace: true });
|
||||
next({ name: "500", replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { AppRouteRecord } from "@/types/router";
|
||||
|
||||
/**
|
||||
* 「异常」菜单分组(挂载在 /exception/*,走 Layout)。
|
||||
* 与 `staticRoutes` 中独立页区分:
|
||||
* - 编程式跳转、守卫:`name` 为 `"403" | "404" | "500"`(对应 `/403`、`/404`、`/500`)
|
||||
* - 本模块子路由:`Exception403` / `Exception404` / `Exception500`(对应 `/exception/403` 等),勿与前者混用
|
||||
*/
|
||||
export const exceptionRoutes: AppRouteRecord = {
|
||||
path: "/exception",
|
||||
name: "Exception",
|
||||
@@ -17,7 +23,6 @@ export const exceptionRoutes: AppRouteRecord = {
|
||||
title: "menus.exception.forbidden",
|
||||
keepAlive: true,
|
||||
isHideTab: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -28,7 +33,6 @@ export const exceptionRoutes: AppRouteRecord = {
|
||||
title: "menus.exception.notFound",
|
||||
keepAlive: true,
|
||||
isHideTab: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -39,7 +43,6 @@ export const exceptionRoutes: AppRouteRecord = {
|
||||
title: "menus.exception.serverError",
|
||||
keepAlive: true,
|
||||
isHideTab: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { AppRouteRecord } from "@/types/router";
|
||||
import { widgetsRoutes } from "./widgets";
|
||||
import { systemRoutes } from "./system";
|
||||
import { exceptionRoutes } from "./exception";
|
||||
|
||||
/**
|
||||
* 导出所有模块化路由
|
||||
* 导出所有模块化路由(业务菜单由后端动态下发;此处仅演示组件库与异常分组)
|
||||
*/
|
||||
export const routeModules: AppRouteRecord[] = [widgetsRoutes, systemRoutes, exceptionRoutes];
|
||||
export const routeModules: AppRouteRecord[] = [widgetsRoutes, exceptionRoutes];
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { AppRouteRecord } from "@/types/router";
|
||||
|
||||
/** Art 模板自带演示路由:与后端「系统管理」路径隔离,避免合并时被覆盖或冲突 */
|
||||
export const systemRoutes: AppRouteRecord = {
|
||||
path: "/art-system",
|
||||
name: "ArtSystem",
|
||||
component: "/index/index",
|
||||
meta: {
|
||||
title: "menus.system.title",
|
||||
icon: "ri:user-3-line",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "user",
|
||||
name: "ArtTplUser",
|
||||
component: "/system/user",
|
||||
meta: {
|
||||
title: "menus.system.user",
|
||||
icon: "ri:user-line",
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "role",
|
||||
name: "ArtTplRole",
|
||||
component: "/system/role",
|
||||
meta: {
|
||||
title: "menus.system.role",
|
||||
icon: "ri:user-settings-line",
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "menu",
|
||||
name: "ArtTplMenus",
|
||||
component: "/system/menu",
|
||||
meta: {
|
||||
title: "menus.system.menu",
|
||||
icon: "ri:menu-line",
|
||||
keepAlive: true,
|
||||
authList: [
|
||||
{ title: "新增", authMark: "add" },
|
||||
{ title: "编辑", authMark: "edit" },
|
||||
{ title: "删除", authMark: "delete" },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -152,7 +152,6 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
title: "menus.dashboard.pricing",
|
||||
icon: "ri:money-cny-box-line",
|
||||
keepAlive: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -173,10 +172,10 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// 通配 404 必须置于静态路由最后
|
||||
// 通配 404 必须置于静态路由最后(name 勿与上方 `/404` 重复,否则按名跳转不稳定)
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "404",
|
||||
name: "CatchAll404",
|
||||
component: () => import("@views/exception/404/index.vue"),
|
||||
meta: { hidden: true, title: "404" },
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import type { MenuTable } from "@/api/module_system/menu";
|
||||
import type { AppRouteRecord, RouteMeta } from "@/types/router";
|
||||
import { RoutesAlias } from "@/router/routesAlias";
|
||||
import { MenuTypeEnum } from "@/enums/system/menu.enum";
|
||||
|
||||
/**
|
||||
* 后端常把「模块目录」与「其下功能菜单」写成同一绝对 route_path。
|
||||
@@ -98,7 +99,7 @@ function toComponentImportPath(componentPath: string): string {
|
||||
}
|
||||
|
||||
function mapMenuNode(item: MenuTable, depth = 0): AppRouteRecord {
|
||||
const childrenRaw = item.children?.filter((c) => c.type !== 3) ?? [];
|
||||
const childrenRaw = item.children?.filter((c) => c.type !== MenuTypeEnum.BUTTON) ?? [];
|
||||
const children = childrenRaw.length
|
||||
? childrenRaw.map((c) => mapMenuNode(c, depth + 1))
|
||||
: undefined;
|
||||
@@ -108,7 +109,7 @@ function mapMenuNode(item: MenuTable, depth = 0): AppRouteRecord {
|
||||
const redirect = item.redirect?.trim() || undefined;
|
||||
|
||||
const hasKids = !!(children && children.length > 0);
|
||||
const isDirectory = item.type === 1;
|
||||
const isDirectory = item.type === MenuTypeEnum.CATALOG;
|
||||
|
||||
let component: string | undefined;
|
||||
if (isDirectory || (hasKids && !(item.component_path ?? "").trim())) {
|
||||
@@ -144,7 +145,7 @@ function mapMenuNode(item: MenuTable, depth = 0): AppRouteRecord {
|
||||
* 过滤按钮(type=3),再递归映射为 AppRouteRecord
|
||||
*/
|
||||
export function backendMenusToAppRoutes(menus: MenuTable[]): AppRouteRecord[] {
|
||||
const roots = menus.filter((m) => m.type !== 3);
|
||||
const roots = menus.filter((m) => m.type !== MenuTypeEnum.BUTTON);
|
||||
const normalized = normalizeMenuNestedPaths(roots);
|
||||
const mapped = normalized.map((m) => mapMenuNode(m, 0));
|
||||
return normalizeAppRouteChildPaths(mapped);
|
||||
|
||||
@@ -89,7 +89,6 @@ const dashboardMenuSource: AppRouteRecord = {
|
||||
title: "menus.dashboard.pricing",
|
||||
icon: "ri:money-cny-box-line",
|
||||
keepAlive: true,
|
||||
isFullPage: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -43,7 +43,7 @@ import { useWorktabStore } from "./worktab.store";
|
||||
import { useMenuStore } from "./menu.store";
|
||||
import { AppRouteRecord } from "@/types/router";
|
||||
import { setPageTitle } from "@/utils/navigation/router";
|
||||
import { resetRouterState } from "@/router/guards/beforeEach";
|
||||
import { resetRouterState, resetRouteInitState } from "@/router/guards/beforeEach";
|
||||
import { StorageConfig } from "@/utils/storage/storage-config";
|
||||
import AuthAPI from "@/api/module_system/auth";
|
||||
import UserAPI from "@/api/module_system/user";
|
||||
@@ -277,6 +277,8 @@ export const useUserStore = defineStore(
|
||||
});
|
||||
}
|
||||
rememberMe.value = loginForm.remember;
|
||||
// 清除上次会话里「动态路由初始化失败」标记,避免重新登录后侧栏/菜单不注册
|
||||
resetRouteInitState();
|
||||
Auth.setTokens(data.access_token, data.refresh_token, rememberMe.value);
|
||||
setToken(data.access_token, data.refresh_token);
|
||||
setLoginStatus(true);
|
||||
|
||||
@@ -117,8 +117,6 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
fixedTab?: boolean;
|
||||
/** 激活菜单路径 */
|
||||
activePath?: string;
|
||||
/** 是否为全屏页面 */
|
||||
isFullPage?: boolean;
|
||||
/** 是否为权限按钮行 */
|
||||
isAuthButton?: boolean;
|
||||
/** 权限标识 */
|
||||
|
||||
@@ -30,19 +30,14 @@ export function isIframe(url: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断菜单项是否可作为默认导航落点
|
||||
* 隐藏的全屏页面虽然不展示在菜单中,但仍然可能是合法首页。
|
||||
* 判断菜单项是否可作为默认导航落点(隐藏菜单不作为首选路径)
|
||||
*/
|
||||
export const isNavigableMenuItem = (menuItem: AppRouteRecord): boolean => {
|
||||
if (!menuItem.path || !menuItem.path.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!menuItem.meta?.isHide) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return menuItem.meta?.isFullPage === true;
|
||||
return !menuItem.meta?.isHide;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ const asciiArt = `
|
||||
\x1b[0m
|
||||
\x1b[33mGitHub: https://github.com/fastapiadmin/FastapiAdmin
|
||||
\x1b[0m
|
||||
\x1b[31m技术支持(微信): 948080782,和开发者一起交流~ 群里有小伙伴实时答疑,遇到问题不用慌!
|
||||
\x1b[31m技术支持(社区群): https://service.fastapiadmin.com/about/,和开发者一起交流~ 群里有小伙伴实时答疑,遇到问题不用慌!
|
||||
\x1b[0m
|
||||
`;
|
||||
|
||||
|
||||
@@ -9,3 +9,5 @@ export * from "./tableCache";
|
||||
export * from "./tableConfig";
|
||||
|
||||
export * from "./tableUtils";
|
||||
|
||||
export * from "./renderTableOperationCell";
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 表格操作列:最多展示 `maxInline` 个 ArtButtonTable,其余收入 ArtButtonMore(与 demo 一致)。
|
||||
*/
|
||||
import { h } from "vue";
|
||||
import type { VNode } from "vue";
|
||||
import { ElTooltip } from "element-plus";
|
||||
import ArtButtonTable from "@/components/Core/forms/art-button-table/index.vue";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import type { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
|
||||
/** 默认外露操作数:超过则出现「更多」 */
|
||||
export const DEFAULT_MAX_INLINE_TABLE_OPERATIONS = 3;
|
||||
|
||||
export interface TableOperationAction {
|
||||
key: string | number;
|
||||
label: string;
|
||||
artType: "add" | "edit" | "delete" | "view" | "more";
|
||||
icon?: string;
|
||||
/** 权限码,传给 ArtButtonMore 的 auth */
|
||||
perm?: string;
|
||||
disabled?: boolean;
|
||||
/** 图标颜色(外露按钮与「更多」内图标);不传则按 artType 使用主题变量 */
|
||||
iconColor?: string;
|
||||
/** 「更多」里文字颜色;不传时对 key 为 delete 默认红色 */
|
||||
color?: string;
|
||||
run: () => void;
|
||||
}
|
||||
|
||||
export interface RenderTableOperationCellOptions {
|
||||
maxInline?: number;
|
||||
/** 包裹容器 class */
|
||||
wrapperClass?: string;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
/** 与 `ArtButtonTable` 各 type 默认图标一致,保证「更多」里每项都有图标 */
|
||||
const ART_TYPE_DEFAULT_ICONS: Record<TableOperationAction["artType"], string> = {
|
||||
add: "ri:add-fill",
|
||||
edit: "ri:pencil-line",
|
||||
delete: "ri:delete-bin-5-line",
|
||||
view: "ri:eye-line",
|
||||
more: "ri:more-2-fill",
|
||||
};
|
||||
|
||||
function iconForOperation(a: TableOperationAction): string {
|
||||
return a.icon ?? ART_TYPE_DEFAULT_ICONS[a.artType];
|
||||
}
|
||||
|
||||
/** 与 Element Plus 语义色一致,便于各操作图标区分 */
|
||||
const ART_TYPE_ICON_COLORS: Record<TableOperationAction["artType"], string> = {
|
||||
add: "var(--el-color-primary)",
|
||||
edit: "var(--el-color-success)",
|
||||
delete: "var(--el-color-danger)",
|
||||
view: "var(--el-color-info)",
|
||||
more: "var(--el-text-color-regular)",
|
||||
};
|
||||
|
||||
function iconColorForOperation(a: TableOperationAction): string | undefined {
|
||||
if (a.iconColor != null) return a.iconColor;
|
||||
return ART_TYPE_ICON_COLORS[a.artType];
|
||||
}
|
||||
|
||||
function defaultMoreItemColor(a: TableOperationAction): string | undefined {
|
||||
if (a.color != null) return a.color;
|
||||
return String(a.key) === "delete" ? "var(--el-color-danger)" : undefined;
|
||||
}
|
||||
|
||||
export function renderTableOperationCell(
|
||||
actions: TableOperationAction[],
|
||||
options?: RenderTableOperationCellOptions
|
||||
): VNode {
|
||||
const maxInline = options?.maxInline ?? DEFAULT_MAX_INLINE_TABLE_OPERATIONS;
|
||||
const wrapperClass =
|
||||
options?.wrapperClass ?? "inline-flex flex-wrap items-center justify-end gap-1";
|
||||
const emptyText = options?.emptyText ?? "—";
|
||||
|
||||
if (actions.length === 0) {
|
||||
return h("span", { class: "text-g-400" }, emptyText);
|
||||
}
|
||||
|
||||
const inline = actions.slice(0, maxInline);
|
||||
const overflow = actions.slice(maxInline);
|
||||
|
||||
const inlineNodes = inline.map((a) =>
|
||||
h(ElTooltip, { content: a.label, placement: "top" }, () =>
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
class: a.disabled ? "inline-flex opacity-40 pointer-events-none" : "inline-flex",
|
||||
},
|
||||
[
|
||||
h(ArtButtonTable, {
|
||||
type: a.artType,
|
||||
icon: iconForOperation(a),
|
||||
iconColor: iconColorForOperation(a),
|
||||
onClick: a.run,
|
||||
}),
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (overflow.length === 0) {
|
||||
return h("div", { class: wrapperClass }, inlineNodes);
|
||||
}
|
||||
|
||||
const moreDropdown = h(ArtButtonMore, {
|
||||
list: overflow.map((a) => ({
|
||||
key: a.key,
|
||||
label: a.label,
|
||||
icon: iconForOperation(a),
|
||||
auth: a.perm,
|
||||
disabled: a.disabled,
|
||||
iconColor: iconColorForOperation(a),
|
||||
color: defaultMoreItemColor(a),
|
||||
})),
|
||||
onClick: (item: ButtonMoreItem) => {
|
||||
const act = overflow.find((x) => String(x.key) === String(item.key));
|
||||
act?.run();
|
||||
},
|
||||
});
|
||||
|
||||
return h("div", { class: wrapperClass }, [...inlineNodes, moreDropdown]);
|
||||
}
|
||||
@@ -39,10 +39,10 @@ export const tableConfig = {
|
||||
recordFields: ["list", "data", "records", "items", "result", "rows"],
|
||||
// 总条数
|
||||
totalFields: ["total", "count"],
|
||||
// 当前页码
|
||||
currentFields: ["current", "page", "pageNum"],
|
||||
// 每页大小
|
||||
sizeFields: ["size", "pageSize", "limit"],
|
||||
// 当前页码(含后端常用 page_no)
|
||||
currentFields: ["current", "page", "pageNum", "page_no"],
|
||||
// 每页大小(含后端常用 page_size)
|
||||
sizeFields: ["size", "pageSize", "limit", "page_size"],
|
||||
|
||||
// 请求参数映射配置,前端发送请求时使用的分页参数名
|
||||
// useTable 组合式函数传递分页参数的时候 用 current 跟 size
|
||||
|
||||
@@ -42,6 +42,25 @@
|
||||
import type { ApiResponse } from "./tableCache";
|
||||
import { tableConfig } from "./tableConfig";
|
||||
|
||||
/**
|
||||
* 请求层若未拆包,这里拿到的是 Axios 整包:业务体在 .data 里。
|
||||
* 否则「list」后的「data」会把 { code, msg, data } 误当成一层列表容器,读不到内层 items。
|
||||
*/
|
||||
function unwrapAxiosResponseBody(response: unknown): unknown {
|
||||
if (response === null || typeof response !== "object") return response;
|
||||
const r = response as Record<string, unknown>;
|
||||
if (
|
||||
"data" in r &&
|
||||
"status" in r &&
|
||||
typeof r.status === "number" &&
|
||||
"config" in r &&
|
||||
typeof r.config === "object"
|
||||
) {
|
||||
return r.data;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// 请求参数基础接口,扩展分页参数
|
||||
export interface BaseRequestParams extends PageQuery {
|
||||
[key: string]: unknown;
|
||||
@@ -115,6 +134,8 @@ export const defaultResponseAdapter = <T>(response: unknown): ApiResponse<T> =>
|
||||
// 定义支持的字段
|
||||
const recordFields = tableConfig.recordFields;
|
||||
|
||||
response = unwrapAxiosResponseBody(response);
|
||||
|
||||
if (!response) {
|
||||
return { records: [], total: 0 };
|
||||
}
|
||||
@@ -184,7 +205,10 @@ export const updatePaginationFromResponse = <T>(
|
||||
pagination: PageQuery,
|
||||
response: ApiResponse<T>
|
||||
): void => {
|
||||
pagination.page_size = response.total ?? pagination.page_size ?? 10;
|
||||
const total = response.total;
|
||||
if (typeof total === "number") {
|
||||
(pagination as Record<string, unknown>).total = total;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -172,7 +172,7 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { h, ref, reactive, computed, nextTick } from "vue";
|
||||
import { ref, reactive, computed, nextTick } from "vue";
|
||||
import { Edit } from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import AiChatAPI, { type ChatSession, type ChatSessionDetail } from "@/api/module_ai/chat";
|
||||
@@ -184,9 +184,9 @@ import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/i
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import type { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
|
||||
type MemorySearchForm = {
|
||||
title?: string;
|
||||
@@ -212,6 +212,8 @@ const showSearchBar = ref(true);
|
||||
const searchBarRef = ref<InstanceType<typeof ArtSearchBar> | null>(null);
|
||||
const searchBarRules: Record<string, unknown> = {};
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
const memorySearchItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: "标题",
|
||||
@@ -269,17 +271,6 @@ function onTableSelectionChange(rows: ChatSession[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: ChatSession) {
|
||||
switch (item.key) {
|
||||
case "detail":
|
||||
void handleOpenDialog("detail", row.id);
|
||||
break;
|
||||
case "delete":
|
||||
void deleteSessionRow(row.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSessionRow(id: string) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
@@ -407,30 +398,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 102,
|
||||
width: 160,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
formatter: (row: ChatSession) =>
|
||||
h("div", [
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
icon: "ri:file-list-3-line",
|
||||
auth: "module_ai:chat:detail",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_ai:chat:delete",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
]),
|
||||
align: "right",
|
||||
formatter: (row: ChatSession) => formatMemoryOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -522,6 +493,37 @@ async function handleOpenDialog(type: "create" | "detail", id?: string) {
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
function buildMemoryRowActions(row: ChatSession): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_ai:chat:detail",
|
||||
run: () => {
|
||||
void handleOpenDialog("detail", row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_ai:chat:delete",
|
||||
run: () => {
|
||||
deleteSessionRow(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatMemoryOperationCell(row: ChatSession) {
|
||||
return renderTableOperationCell(buildMemoryRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 memory-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditTitle(row: ChatSession) {
|
||||
editingRowId.value = row.id;
|
||||
editingTitle.value = row.title || "";
|
||||
|
||||
@@ -269,13 +269,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref } from "vue";
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtImportDialog from "@/components/Core/modal/art-import-dialog/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import type { IContentConfig, IObject } from "@/components/Core/modal/types";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import ArtSearchBarWithAudit from "@/components/Core/forms/art-search-bar/ArtSearchBarWithAudit.vue";
|
||||
import type { AuditSearchFormParams } from "@/components/Core/forms/art-search-bar/auditSearchFormItems";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
@@ -294,6 +294,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
type DemoSearchFormParams = { name?: string; status?: string } & AuditSearchFormParams;
|
||||
|
||||
function normalizeDemoQuery(params: Record<string, unknown>): DemoPageQuery {
|
||||
@@ -436,19 +438,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 102,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
formatter: (row: DemoTable) =>
|
||||
h("div", [
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{ key: "detail", label: "详情", icon: "ri:file-list-3-line" },
|
||||
{ key: "edit", label: "编辑", icon: "ri:edit-2-line" },
|
||||
{ key: "delete", label: "删除", icon: "ri:delete-bin-4-line", color: "#f56c6c" },
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
]),
|
||||
align: "right",
|
||||
formatter: (row: DemoTable) => formatDemoOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -591,19 +584,41 @@ const onResetSearch = async () => {
|
||||
await resetSearchParams();
|
||||
};
|
||||
|
||||
const buttonMoreClick = (item: ButtonMoreItem, row: DemoTable) => {
|
||||
switch (item.key) {
|
||||
case "detail":
|
||||
openDetailDialog(row);
|
||||
break;
|
||||
case "edit":
|
||||
openEditDialog("edit", row);
|
||||
break;
|
||||
case "delete":
|
||||
deleteDemoRow(row);
|
||||
break;
|
||||
}
|
||||
};
|
||||
function buildDemoRowActions(row: DemoTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
icon: "ri:file-list-3-line",
|
||||
perm: "module_example:demo:detail",
|
||||
run: () => void openDetailDialog(row),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_example:demo:update",
|
||||
run: () => void openEditDialog("edit", row),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_example:demo:delete",
|
||||
run: () => deleteDemoRow(row),
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatDemoOperationCell(row: DemoTable) {
|
||||
return renderTableOperationCell(buildDemoRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 demo-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
async function openDetailDialog(row: DemoTable) {
|
||||
if (!row.id) return;
|
||||
|
||||
@@ -155,13 +155,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref, reactive } from "vue";
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtImportDialog from "@/components/Core/modal/art-import-dialog/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import type { IContentConfig, IObject } from "@/components/Core/modal/types";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import ArtSearchBarWithAudit from "@/components/Core/forms/art-search-bar/ArtSearchBarWithAudit.vue";
|
||||
import type { AuditSearchFormParams } from "@/components/Core/forms/art-search-bar/auditSearchFormItems";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
@@ -179,6 +179,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
type Demo01SearchFormParams = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
@@ -310,19 +312,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 120,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
formatter: (row: Demo01Table) =>
|
||||
h("div", [
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{ key: "detail", label: "详情", icon: "ri:file-list-3-line" },
|
||||
{ key: "edit", label: "编辑", icon: "ri:edit-2-line" },
|
||||
{ key: "delete", label: "删除", icon: "ri:delete-bin-4-line", color: "#f56c6c" },
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
]),
|
||||
align: "right",
|
||||
formatter: (row: Demo01Table) => formatDemo01OperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -447,19 +440,41 @@ const onResetSearch = async () => {
|
||||
await resetSearchParams();
|
||||
};
|
||||
|
||||
const buttonMoreClick = (item: ButtonMoreItem, row: Demo01Table) => {
|
||||
switch (item.key) {
|
||||
case "detail":
|
||||
openDetailDialog(row);
|
||||
break;
|
||||
case "edit":
|
||||
openEditDialog("edit", row);
|
||||
break;
|
||||
case "delete":
|
||||
deleteDemo01Row(row);
|
||||
break;
|
||||
}
|
||||
};
|
||||
function buildDemo01RowActions(row: Demo01Table): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
icon: "ri:file-list-3-line",
|
||||
perm: "module_example:demo01:detail",
|
||||
run: () => void openDetailDialog(row),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_example:demo01:update",
|
||||
run: () => void openEditDialog("edit", row),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_example:demo01:delete",
|
||||
run: () => deleteDemo01Row(row),
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatDemo01OperationCell(row: Demo01Table) {
|
||||
return renderTableOperationCell(buildDemo01RowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 demo01-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
async function openDetailDialog(row: Demo01Table) {
|
||||
if (!row.id) return;
|
||||
|
||||
@@ -138,7 +138,7 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { h, ref, reactive, computed, onActivated, watch, nextTick, provide } from "vue";
|
||||
import { ref, reactive, computed, onActivated, watch, nextTick, provide } from "vue";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { EditorConfiguration } from "codemirror";
|
||||
@@ -160,9 +160,9 @@ import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import type { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import CreateTableDialog, { type CreateTableSubmitMeta } from "./components/CreateTableDialog.vue";
|
||||
import GenCodeDrawer from "./components/GenCodeDrawer.vue";
|
||||
import ImportDbTableDialog from "./components/ImportDbTableDialog.vue";
|
||||
@@ -192,6 +192,7 @@ const listRefresh = {
|
||||
};
|
||||
|
||||
const route = useRoute();
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
provide(GENCODE_BASIC_FORM_KEY, basicInfo);
|
||||
provide(GENCODE_CM_KEY, cmRef);
|
||||
@@ -763,18 +764,42 @@ type GencodeSearchForm = {
|
||||
table_comment?: string;
|
||||
};
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: GenTableSchema) {
|
||||
switch (item.key) {
|
||||
case "preview":
|
||||
void handlePreviewTable(row);
|
||||
break;
|
||||
case "delete":
|
||||
void handleDelete(row);
|
||||
break;
|
||||
case "sync":
|
||||
void handleSynchDb(row);
|
||||
break;
|
||||
}
|
||||
function buildGencodeRowActions(row: GenTableSchema): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "preview",
|
||||
label: "代码生成",
|
||||
artType: "edit",
|
||||
icon: "ri:magic-line",
|
||||
iconColor: "var(--el-color-primary)",
|
||||
perm: "module_generator:gencode:update",
|
||||
run: () => void handlePreviewTable(row),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_generator:gencode:delete",
|
||||
run: () => void handleDelete(row),
|
||||
},
|
||||
{
|
||||
key: "sync",
|
||||
label: "同步",
|
||||
artType: "view",
|
||||
icon: "ri:refresh-line",
|
||||
iconColor: "var(--el-color-primary)",
|
||||
perm: "module_generator:db:sync",
|
||||
run: () => void handleSynchDb(row),
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatGencodeOperationCell(row: GenTableSchema) {
|
||||
return renderTableOperationCell(buildGencodeRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 gencode-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
const searchForm = ref<GencodeSearchForm>({
|
||||
@@ -862,36 +887,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 100,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
formatter: (row: GenTableSchema) =>
|
||||
h("div", [
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "preview",
|
||||
label: "代码生成",
|
||||
icon: "ri:magic-stick-line",
|
||||
auth: "module_generator:gencode:update",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_generator:gencode:delete",
|
||||
},
|
||||
{
|
||||
key: "sync",
|
||||
label: "同步",
|
||||
icon: "ri:refresh-line",
|
||||
auth: "module_generator:db:sync",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
]),
|
||||
align: "right",
|
||||
formatter: (row: GenTableSchema) => formatGencodeOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
/>
|
||||
|
||||
<ElCard
|
||||
class="flex flex-col flex-1 min-h-0 art-table-card"
|
||||
class="resource-monitor-card flex flex-col flex-1 min-h-0 art-table-card"
|
||||
:style="{ 'margin-top': showSearchBar ? '12px' : '0' }"
|
||||
>
|
||||
<div
|
||||
class="resource-breadcrumb mb-3 flex flex-wrap items-center gap-2 border-b border-g-200 pb-3"
|
||||
class="resource-breadcrumb mb-3 shrink-0 flex flex-wrap items-center gap-2 border-b border-g-200 pb-3"
|
||||
>
|
||||
<ElTooltip content="资源文件管理系统: 点击路径可以快速返回上级目录">
|
||||
<QuestionFilled class="mx-1 h-4 w-4" />
|
||||
@@ -43,9 +43,9 @@
|
||||
</div>
|
||||
|
||||
<ArtTableHeader
|
||||
class="resource-toolbar shrink-0"
|
||||
v-model:columns="columnChecks"
|
||||
v-model:showSearchBar="showSearchBar"
|
||||
layout="search,refresh,size,fullscreen"
|
||||
:loading="loading"
|
||||
@refresh="refreshData"
|
||||
>
|
||||
@@ -91,16 +91,20 @@
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
row-key="file_url"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="paginationBind"
|
||||
@selection-change="onTableSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
/>
|
||||
<!-- 面包屑与表头已占用高度:表格高度须在独立 flex 子项内计算,否则分页会被挤出卡片 -->
|
||||
<div class="resource-table-region min-h-0 flex flex-1 flex-col overflow-hidden pb-3">
|
||||
<ArtTable
|
||||
row-key="file_url"
|
||||
:show-table-header="false"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="paginationBind"
|
||||
@selection-change="onTableSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ArtDialog
|
||||
@@ -644,6 +648,13 @@ async function handleBatchDelete() {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.resource-monitor-page :deep(.resource-monitor-card.el-card > .el-card__body) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.el-breadcrumb__item) {
|
||||
&.is-link {
|
||||
color: var(--el-color-primary);
|
||||
|
||||
@@ -178,29 +178,20 @@ import DeptAPI, {
|
||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtButtonTable from "@/components/Core/forms/art-button-table/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
} from "element-plus";
|
||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { useUserStore } from "@/store";
|
||||
import { formatTree } from "@/utils/common";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
|
||||
defineOptions({
|
||||
name: "Dept",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
||||
const { hasAuth } = useAuth();
|
||||
const userStore = useUserStore();
|
||||
|
||||
@@ -219,16 +210,6 @@ function buildDeptQuery(p: DeptSearchForm): DeptPageQuery {
|
||||
};
|
||||
}
|
||||
|
||||
type RowAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
artType: "add" | "edit" | "delete" | "view" | "more";
|
||||
icon?: string;
|
||||
perm: string;
|
||||
disabled?: boolean;
|
||||
run: () => void;
|
||||
};
|
||||
|
||||
function buildDeptRowActions(
|
||||
row: DeptTable,
|
||||
ctx: {
|
||||
@@ -237,8 +218,8 @@ function buildDeptRowActions(
|
||||
onEdit: (id: number) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
): RowAction[] {
|
||||
const all: RowAction[] = [
|
||||
): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "add",
|
||||
label: "新增",
|
||||
@@ -268,74 +249,14 @@ function buildDeptRowActions(
|
||||
run: () => ctx.onDelete(row.id!),
|
||||
},
|
||||
];
|
||||
return all.filter((a) => hasAuth(a.perm));
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatDeptOperationCell(row: DeptTable, ctx: Parameters<typeof buildDeptRowActions>[1]) {
|
||||
const actions = buildDeptRowActions(row, ctx);
|
||||
if (actions.length === 0) {
|
||||
return h("span", { class: "text-g-400" }, "—");
|
||||
}
|
||||
const inline = actions.slice(0, MAX_INLINE_ROW_ACTIONS);
|
||||
const overflow = actions.slice(MAX_INLINE_ROW_ACTIONS);
|
||||
|
||||
const inlineNodes = inline.map((a) =>
|
||||
h(ElTooltip, { content: a.label, placement: "top" }, () =>
|
||||
h("span", { class: "inline-flex" }, [
|
||||
h(ArtButtonTable, {
|
||||
type: a.artType,
|
||||
icon: a.icon,
|
||||
onClick: a.run,
|
||||
}),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
if (overflow.length === 0) {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 dept-table-actions" },
|
||||
inlineNodes
|
||||
);
|
||||
}
|
||||
|
||||
const dropdown = h(
|
||||
ElDropdown,
|
||||
{ trigger: "click" },
|
||||
{
|
||||
default: () =>
|
||||
h(ElTooltip, { content: "更多", placement: "top" }, () =>
|
||||
h("span", { class: "inline-flex align-middle" }, [
|
||||
h(ArtButtonTable, {
|
||||
type: "more",
|
||||
onClick: () => {},
|
||||
}),
|
||||
])
|
||||
),
|
||||
dropdown: () =>
|
||||
h(
|
||||
ElDropdownMenu,
|
||||
null,
|
||||
overflow.map((a) =>
|
||||
h(
|
||||
ElDropdownItem,
|
||||
{
|
||||
key: a.key,
|
||||
disabled: a.disabled,
|
||||
onClick: () => a.run(),
|
||||
},
|
||||
() => a.label
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 dept-table-actions" },
|
||||
[...inlineNodes, dropdown]
|
||||
);
|
||||
return renderTableOperationCell(actions, {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 dept-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
const searchForm = ref<DeptSearchForm>({
|
||||
|
||||
@@ -267,12 +267,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref, reactive } from "vue";
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import type { IObject } from "@/components/Core/modal/types";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
@@ -284,6 +282,8 @@ import DictAPI, {
|
||||
type DictDataTable,
|
||||
} from "@/api/module_system/dict";
|
||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import { useDictStore } from "@/store";
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
@@ -328,6 +328,7 @@ const TAG_TYPE_STYLE_MAP: Record<string, { background: string; color: string; bo
|
||||
|
||||
const appStore = useAppStore();
|
||||
const dictStore = useDictStore();
|
||||
const { hasAuth } = useAuth();
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "80%" : "60%"));
|
||||
|
||||
function getTagPreviewStyle(value?: string) {
|
||||
@@ -398,7 +399,7 @@ const statusOptions = ref([
|
||||
|
||||
const dictDataSearchItems = computed<SearchFormItem[]>(() => [
|
||||
{
|
||||
label: "字典数据标签",
|
||||
label: "字典标签",
|
||||
key: "dict_label",
|
||||
type: "input",
|
||||
placeholder: "请输入字典标签",
|
||||
@@ -509,33 +510,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 120,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
formatter: (row: DictDataTable) =>
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
icon: "ri:file-list-3-line",
|
||||
auth: "module_system:dict_data:detail",
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: "ri:edit-2-line",
|
||||
auth: "module_system:dict_data:update",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_system:dict_data:delete",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
align: "right",
|
||||
formatter: (row: DictDataTable) => formatDictDataOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -667,20 +645,6 @@ function onResetSearch() {
|
||||
void resetSearchParams();
|
||||
}
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: DictDataTable) {
|
||||
switch (item.key) {
|
||||
case "detail":
|
||||
void handleOpenDialog("detail", row.id);
|
||||
break;
|
||||
case "edit":
|
||||
void handleOpenDialog("update", row.id);
|
||||
break;
|
||||
case "delete":
|
||||
if (row.id != null) deleteDictDataRow(row.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
@@ -744,6 +708,43 @@ async function handleSubmit() {
|
||||
});
|
||||
}
|
||||
|
||||
function buildDictDataRowActions(row: DictDataTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_system:dict_data:detail",
|
||||
run: () => void handleOpenDialog("detail", row.id),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_system:dict_data:update",
|
||||
run: () => void handleOpenDialog("update", row.id),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_system:dict_data:delete",
|
||||
run: () => {
|
||||
if (row.id != null) deleteDictDataRow(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatDictDataOperationCell(row: DictDataTable) {
|
||||
return renderTableOperationCell(buildDictDataRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 dict-data-drawer-actions",
|
||||
});
|
||||
}
|
||||
|
||||
function deleteDictDataRow(id: number) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 字典类型:与 demo 同一套 Art 布局;接口 DictAPI / fetchGetDictTypeList -->
|
||||
<!-- 字典类型:Art 布局;操作列最多 3 个外露 +「更多」 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<ArtSearchBar
|
||||
@@ -152,12 +152,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref, reactive } from "vue";
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import type { IObject } from "@/components/Core/modal/types";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
@@ -170,6 +168,8 @@ import DictAPI, {
|
||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||
import { useDictStore } from "@/store";
|
||||
import DataDrawer from "@/views/module_system/dict/components/DataDrawer.vue";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
|
||||
defineOptions({
|
||||
name: "Dict",
|
||||
@@ -191,6 +191,7 @@ function normalizeDictTypeQuery(params: Record<string, unknown>): DictPageQuery
|
||||
}
|
||||
|
||||
const dictStore = useDictStore();
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
const searchForm = ref<DictTypeSearchForm>({
|
||||
dict_name: undefined,
|
||||
@@ -314,39 +315,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 120,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
formatter: (row: DictTable) =>
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "dictData",
|
||||
label: "字典",
|
||||
icon: "ri:book-2-line",
|
||||
auth: "module_system:dict_data:query",
|
||||
},
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
icon: "ri:file-list-3-line",
|
||||
auth: "module_system:dict_type:detail",
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: "ri:edit-2-line",
|
||||
auth: "module_system:dict_type:update",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_system:dict_type:delete",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
align: "right",
|
||||
formatter: (row: DictTable) => formatDictOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -462,21 +434,50 @@ function onResetSearch() {
|
||||
void resetSearchParams();
|
||||
}
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: DictTable) {
|
||||
switch (item.key) {
|
||||
case "dictData":
|
||||
handleDictDataDrawer(row);
|
||||
break;
|
||||
case "detail":
|
||||
void handleOpenDialog("detail", row.id);
|
||||
break;
|
||||
case "edit":
|
||||
void handleOpenDialog("update", row.id);
|
||||
break;
|
||||
case "delete":
|
||||
if (row.id != null) deleteDictTypeRow(row.id);
|
||||
break;
|
||||
}
|
||||
function buildDictRowActions(row: DictTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "dictData",
|
||||
label: "字典",
|
||||
artType: "view",
|
||||
icon: "ri:book-2-line",
|
||||
iconColor: "var(--el-color-warning)",
|
||||
perm: "module_system:dict_data:query",
|
||||
run: () => handleDictDataDrawer(row),
|
||||
},
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_system:dict_type:detail",
|
||||
run: () => void handleOpenDialog("detail", row.id),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_system:dict_type:update",
|
||||
run: () => void handleOpenDialog("update", row.id),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_system:dict_type:delete",
|
||||
run: () => {
|
||||
if (row.id != null) deleteDictTypeRow(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatDictOperationCell(row: DictTable) {
|
||||
return renderTableOperationCell(buildDictRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 dict-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
function handleDictDataDrawer(dictTypeRow: DictTable) {
|
||||
|
||||
@@ -144,14 +144,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref, nextTick } from "vue";
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import type { IObject } from "@/components/Core/modal/types";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
@@ -160,6 +158,8 @@ import CopyButton from "@/components/CopyButton/index.vue";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import LogAPI, { type LogPageQuery, type LogTable } from "@/api/module_system/log";
|
||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
|
||||
defineOptions({
|
||||
@@ -167,6 +167,8 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
type LogSearchForm = {
|
||||
request_path?: string;
|
||||
type?: number;
|
||||
@@ -348,27 +350,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 100,
|
||||
width: 160,
|
||||
fixed: "right",
|
||||
formatter: (row: LogTable) =>
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
icon: "ri:file-list-3-line",
|
||||
auth: "module_system:log:detail",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_system:log:delete",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
align: "right",
|
||||
formatter: (row: LogTable) => formatLogOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -473,17 +458,6 @@ function onResetSearch() {
|
||||
void resetSearchParams();
|
||||
}
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: LogTable) {
|
||||
switch (item.key) {
|
||||
case "detail":
|
||||
if (row.id != null) void handleOpenDialog(row.id);
|
||||
break;
|
||||
case "delete":
|
||||
if (row.id != null) deleteLogRow(row.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetForm() {
|
||||
formData.value = {};
|
||||
}
|
||||
@@ -515,6 +489,37 @@ function deleteLogRow(id: number) {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function buildLogRowActions(row: LogTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_system:log:detail",
|
||||
run: () => {
|
||||
if (row.id != null) void handleOpenDialog(row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_system:log:delete",
|
||||
run: () => {
|
||||
if (row.id != null) deleteLogRow(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatLogOperationCell(row: LogTable) {
|
||||
return renderTableOperationCell(buildLogRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 log-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
const ids = selectedIds.value;
|
||||
if (ids.length === 0) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 菜单管理:Art + 树形表格(对齐 system/menu 模版) -->
|
||||
<!-- 菜单管理:Art + 树形表格;操作列最多 3 个外露,其余「更多」 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<ArtSearchBar
|
||||
@@ -526,22 +526,13 @@ import MenuRouteIcon from "@/components/MenuRouteIcon/index.vue";
|
||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtButtonTable from "@/components/Core/forms/art-button-table/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDrawer from "@/components/Core/modal/art-drawer/index.vue";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
} from "element-plus";
|
||||
import { ElMessage, ElMessageBox, ElTag, ElTooltip } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
|
||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
||||
const { hasAuth } = useAuth();
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
@@ -580,16 +571,6 @@ function ynTag(v: boolean | undefined, yes = "是", no = "否") {
|
||||
return h(ElTag, { type: v ? "success" : "danger" }, () => (v ? yes : no));
|
||||
}
|
||||
|
||||
type RowAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
artType: "add" | "edit" | "delete" | "view" | "more";
|
||||
icon?: string;
|
||||
perm: string;
|
||||
disabled?: boolean;
|
||||
run: () => void;
|
||||
};
|
||||
|
||||
function buildMenuRowActions(
|
||||
row: MenuTable,
|
||||
ctx: {
|
||||
@@ -598,8 +579,8 @@ function buildMenuRowActions(
|
||||
onEdit: (id: number) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
): RowAction[] {
|
||||
const actions: RowAction[] = [];
|
||||
): TableOperationAction[] {
|
||||
const actions: TableOperationAction[] = [];
|
||||
if (ctx.onAdd && (row.type === MenuTypeEnum.CATALOG || row.type === MenuTypeEnum.MENU)) {
|
||||
actions.push({
|
||||
key: "add",
|
||||
@@ -632,74 +613,14 @@ function buildMenuRowActions(
|
||||
run: () => ctx.onDelete(row.id!),
|
||||
}
|
||||
);
|
||||
return actions.filter((a) => hasAuth(a.perm));
|
||||
return actions.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatMenuOperationCell(row: MenuTable, ctx: Parameters<typeof buildMenuRowActions>[1]) {
|
||||
const actions = buildMenuRowActions(row, ctx);
|
||||
if (actions.length === 0) {
|
||||
return h("span", { class: "text-g-400" }, "—");
|
||||
}
|
||||
const inline = actions.slice(0, MAX_INLINE_ROW_ACTIONS);
|
||||
const overflow = actions.slice(MAX_INLINE_ROW_ACTIONS);
|
||||
|
||||
const inlineNodes = inline.map((a) =>
|
||||
h(ElTooltip, { content: a.label, placement: "top" }, () =>
|
||||
h("span", { class: "inline-flex" }, [
|
||||
h(ArtButtonTable, {
|
||||
type: a.artType,
|
||||
icon: a.icon,
|
||||
onClick: a.run,
|
||||
}),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
if (overflow.length === 0) {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 menu-table-actions" },
|
||||
inlineNodes
|
||||
);
|
||||
}
|
||||
|
||||
const dropdown = h(
|
||||
ElDropdown,
|
||||
{ trigger: "click" },
|
||||
{
|
||||
default: () =>
|
||||
h(ElTooltip, { content: "更多", placement: "top" }, () =>
|
||||
h("span", { class: "inline-flex align-middle" }, [
|
||||
h(ArtButtonTable, {
|
||||
type: "more",
|
||||
onClick: () => {},
|
||||
}),
|
||||
])
|
||||
),
|
||||
dropdown: () =>
|
||||
h(
|
||||
ElDropdownMenu,
|
||||
null,
|
||||
overflow.map((a) =>
|
||||
h(
|
||||
ElDropdownItem,
|
||||
{
|
||||
key: a.key,
|
||||
disabled: a.disabled,
|
||||
onClick: () => a.run(),
|
||||
},
|
||||
() => a.label
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 menu-table-actions" },
|
||||
[...inlineNodes, dropdown]
|
||||
);
|
||||
return renderTableOperationCell(actions, {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 menu-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
const menuClientTab = ref<"pc" | "app">("pc");
|
||||
@@ -997,7 +918,7 @@ const { columnChecks, columns } = useTableColumns<MenuTable>(() => [
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 260,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
align: "right",
|
||||
formatter: (row: MenuTable) => formatMenuOperationCell(row, opCtx),
|
||||
|
||||
@@ -185,14 +185,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref, reactive, nextTick, onMounted } from "vue";
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import type { IObject } from "@/components/Core/modal/types";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
@@ -203,6 +201,8 @@ import NoticeAPI, {
|
||||
type NoticeTable,
|
||||
} from "@/api/module_system/notice";
|
||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import { useDictStore, useNoticeStore } from "@/store/index";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
|
||||
@@ -213,6 +213,7 @@ defineOptions({
|
||||
|
||||
const dictStore = useDictStore();
|
||||
const noticeStore = useNoticeStore();
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
type NoticeSearchForm = {
|
||||
notice_title?: string;
|
||||
@@ -391,33 +392,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 120,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
formatter: (row: NoticeTable) =>
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
icon: "ri:file-list-3-line",
|
||||
auth: "module_system:notice:detail",
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: "ri:edit-2-line",
|
||||
auth: "module_system:notice:update",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_system:notice:delete",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
align: "right",
|
||||
formatter: (row: NoticeTable) => formatNoticeOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -555,20 +533,6 @@ function onResetSearch() {
|
||||
void resetSearchParams();
|
||||
}
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: NoticeTable) {
|
||||
switch (item.key) {
|
||||
case "detail":
|
||||
if (row.id != null) void handleOpenDialog("detail", row.id);
|
||||
break;
|
||||
case "edit":
|
||||
if (row.id != null) void handleOpenDialog("update", row.id);
|
||||
break;
|
||||
case "delete":
|
||||
if (row.id != null) deleteNoticeRow(row.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
@@ -641,6 +605,47 @@ function deleteNoticeRow(id: number) {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function buildNoticeRowActions(row: NoticeTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_system:notice:detail",
|
||||
run: () => {
|
||||
if (row.id != null) void handleOpenDialog("detail", row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_system:notice:update",
|
||||
run: () => {
|
||||
if (row.id != null) void handleOpenDialog("update", row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_system:notice:delete",
|
||||
run: () => {
|
||||
if (row.id != null) deleteNoticeRow(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatNoticeOperationCell(row: NoticeTable) {
|
||||
return renderTableOperationCell(buildNoticeRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 notice-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
const ids = selectedIds.value;
|
||||
if (ids.length === 0) return;
|
||||
|
||||
@@ -161,14 +161,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, computed, ref, reactive } from "vue";
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import type { IObject } from "@/components/Core/modal/types";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||
@@ -179,6 +177,8 @@ import ParamsAPI, {
|
||||
type ConfigTable,
|
||||
} from "@/api/module_system/params";
|
||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import { useConfigStore } from "@/store";
|
||||
|
||||
defineOptions({
|
||||
@@ -187,6 +187,7 @@ defineOptions({
|
||||
});
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
type ParamSearchForm = {
|
||||
config_name?: string;
|
||||
@@ -329,33 +330,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 120,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
formatter: (row: ConfigTable) =>
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
icon: "ri:file-list-3-line",
|
||||
auth: "module_system:param:detail",
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: "ri:edit-2-line",
|
||||
auth: "module_system:param:update",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_system:param:delete",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
align: "right",
|
||||
formatter: (row: ConfigTable) => formatParamOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -462,20 +440,6 @@ function onResetSearch() {
|
||||
void resetSearchParams();
|
||||
}
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: ConfigTable) {
|
||||
switch (item.key) {
|
||||
case "detail":
|
||||
if (row.id != null) void handleOpenDialog("detail", row.id);
|
||||
break;
|
||||
case "edit":
|
||||
if (row.id != null) void handleOpenDialog("update", row.id);
|
||||
break;
|
||||
case "delete":
|
||||
if (row.id != null) deleteParamRow(row.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
@@ -550,6 +514,47 @@ function deleteParamRow(id: number) {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function buildParamRowActions(row: ConfigTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "detail",
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_system:param:detail",
|
||||
run: () => {
|
||||
if (row.id != null) void handleOpenDialog("detail", row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_system:param:update",
|
||||
run: () => {
|
||||
if (row.id != null) void handleOpenDialog("update", row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_system:param:delete",
|
||||
run: () => {
|
||||
if (row.id != null) deleteParamRow(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatParamOperationCell(row: ConfigTable) {
|
||||
return renderTableOperationCell(buildParamRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 param-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
const ids = selectedIds.value;
|
||||
if (ids.length === 0) return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 角色管理:Art + useTable;操作列前 3 个为 ArtButtonTable,其余收入「更多」下拉(对齐 system/menu) -->
|
||||
<!-- 角色管理:Art + useTable;操作列最多 3 个外露 +「更多」 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<ArtSearchBar
|
||||
@@ -201,7 +201,7 @@ import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||
import ArtButtonTable from "@/components/Core/forms/art-button-table/index.vue";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import type { IObject } from "@/components/Core/modal/types";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
@@ -212,15 +212,7 @@ import RoleAPI, {
|
||||
type RoleTable,
|
||||
type TablePageQuery,
|
||||
} from "@/api/module_system/role";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
} from "element-plus";
|
||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { useUserStore } from "@/store";
|
||||
import PermissonDrawer from "./components/PermissonDrawer.vue";
|
||||
@@ -230,8 +222,6 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
||||
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
type RoleSearchForm = {
|
||||
@@ -292,17 +282,6 @@ function deptsCell(row: RoleTable) {
|
||||
return h("span", { class: "inline-flex flex-wrap items-center" }, tags);
|
||||
}
|
||||
|
||||
type RowAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
/** ArtButtonTable 预设类型,分配权限用 edit + 自定义图标 */
|
||||
artType: "add" | "edit" | "delete" | "view" | "more";
|
||||
icon?: string;
|
||||
perm: string;
|
||||
disabled?: boolean;
|
||||
run: () => void;
|
||||
};
|
||||
|
||||
function buildRoleRowActions(
|
||||
row: RoleTable,
|
||||
ctx: {
|
||||
@@ -311,22 +290,25 @@ function buildRoleRowActions(
|
||||
onEdit: (id: number) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
): RowAction[] {
|
||||
): TableOperationAction[] {
|
||||
const isSys = row.id === 1;
|
||||
const warn = () => {
|
||||
ElMessage.warning("系统默认角色,不可操作");
|
||||
};
|
||||
const all: RowAction[] = [
|
||||
const warnSys = () => ElMessage.warning("系统默认角色,不可操作");
|
||||
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "perm",
|
||||
label: "分配权限",
|
||||
artType: "edit",
|
||||
artType: "view",
|
||||
icon: "ri:shield-keyhole-line",
|
||||
iconColor: "var(--el-color-primary)",
|
||||
perm: "module_system:role:permission",
|
||||
disabled: isSys,
|
||||
run: () => {
|
||||
if (isSys) warn();
|
||||
else ctx.onPerm(row.id, row.name);
|
||||
if (isSys) {
|
||||
warnSys();
|
||||
return;
|
||||
}
|
||||
ctx.onPerm(row.id!, row.name);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -334,99 +316,46 @@ function buildRoleRowActions(
|
||||
label: "详情",
|
||||
artType: "view",
|
||||
perm: "module_system:role:detail",
|
||||
run: () => ctx.onDetail(row.id),
|
||||
run: () => ctx.onDetail(row.id!),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_system:role:update",
|
||||
disabled: isSys,
|
||||
run: () => {
|
||||
if (isSys) warn();
|
||||
else ctx.onEdit(row.id);
|
||||
if (isSys) {
|
||||
warnSys();
|
||||
return;
|
||||
}
|
||||
ctx.onEdit(row.id!);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_system:role:delete",
|
||||
disabled: isSys,
|
||||
run: () => {
|
||||
if (isSys) warn();
|
||||
else ctx.onDelete(row.id);
|
||||
if (isSys) {
|
||||
warnSys();
|
||||
return;
|
||||
}
|
||||
ctx.onDelete(row.id!);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => hasAuth(a.perm));
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatRoleOperationCell(row: RoleTable, ctx: Parameters<typeof buildRoleRowActions>[1]) {
|
||||
const actions = buildRoleRowActions(row, ctx);
|
||||
if (actions.length === 0) {
|
||||
return h("span", { class: "text-g-400" }, "—");
|
||||
}
|
||||
const inline = actions.slice(0, MAX_INLINE_ROW_ACTIONS);
|
||||
const overflow = actions.slice(MAX_INLINE_ROW_ACTIONS);
|
||||
|
||||
const inlineNodes = inline.map((a) =>
|
||||
h(ElTooltip, { content: a.label, placement: "top" }, () =>
|
||||
h("span", { class: "inline-flex" }, [
|
||||
h(ArtButtonTable, {
|
||||
type: a.artType,
|
||||
icon: a.icon,
|
||||
onClick: a.run,
|
||||
}),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
if (overflow.length === 0) {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 role-table-actions" },
|
||||
inlineNodes
|
||||
);
|
||||
}
|
||||
|
||||
const dropdown = h(
|
||||
ElDropdown,
|
||||
{ trigger: "click" },
|
||||
{
|
||||
default: () =>
|
||||
h(ElTooltip, { content: "更多", placement: "top" }, () =>
|
||||
h("span", { class: "inline-flex align-middle" }, [
|
||||
h(ArtButtonTable, {
|
||||
type: "more",
|
||||
onClick: () => {},
|
||||
}),
|
||||
])
|
||||
),
|
||||
dropdown: () =>
|
||||
h(
|
||||
ElDropdownMenu,
|
||||
null,
|
||||
overflow.map((a) =>
|
||||
h(
|
||||
ElDropdownItem,
|
||||
{
|
||||
key: a.key,
|
||||
disabled: a.disabled,
|
||||
onClick: () => a.run(),
|
||||
},
|
||||
() => a.label
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 role-table-actions" },
|
||||
[...inlineNodes, dropdown]
|
||||
);
|
||||
return renderTableOperationCell(buildRoleRowActions(row, ctx), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 role-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
const searchForm = ref<RoleSearchForm>({
|
||||
@@ -579,7 +508,7 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 200,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
align: "right",
|
||||
formatter: (row: RoleTable) => formatRoleOperationCell(row, opCtx),
|
||||
|
||||
@@ -329,7 +329,6 @@ import { useTable } from "@/hooks/core/useTable";
|
||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtButtonTable from "@/components/Core/forms/art-button-table/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtDrawer from "@/components/Core/modal/art-drawer/index.vue";
|
||||
@@ -348,20 +347,11 @@ import RoleAPI from "@/api/module_system/role";
|
||||
import DeptTree from "./components/DeptTree.vue";
|
||||
import UserTableSelect from "./components/UserTableSelect.vue";
|
||||
import { useUserStore } from "@/store";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
ElDropdown,
|
||||
ElDropdownMenu,
|
||||
ElDropdownItem,
|
||||
ElAvatar,
|
||||
} from "element-plus";
|
||||
import { ElMessage, ElMessageBox, ElTag, ElAvatar } from "element-plus";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
|
||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
||||
const { hasAuth } = useAuth();
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
@@ -403,16 +393,6 @@ function fetchUserTableList(params: Record<string, unknown>) {
|
||||
});
|
||||
}
|
||||
|
||||
type RowAction = {
|
||||
key: string;
|
||||
label: string;
|
||||
artType: "add" | "edit" | "delete" | "view" | "more";
|
||||
icon?: string;
|
||||
perm: string;
|
||||
disabled?: boolean;
|
||||
run: () => void;
|
||||
};
|
||||
|
||||
function buildUserRowActions(
|
||||
row: UserInfo,
|
||||
ctx: {
|
||||
@@ -421,9 +401,9 @@ function buildUserRowActions(
|
||||
onEdit: (id: number) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}
|
||||
): RowAction[] {
|
||||
): TableOperationAction[] {
|
||||
const sys = row.is_superuser === true;
|
||||
const all: RowAction[] = [
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "resetPwd",
|
||||
label: "重置密码",
|
||||
@@ -466,80 +446,14 @@ function buildUserRowActions(
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => hasAuth(a.perm));
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatUserOperationCell(row: UserInfo, ctx: Parameters<typeof buildUserRowActions>[1]) {
|
||||
const actions = buildUserRowActions(row, ctx);
|
||||
if (actions.length === 0) {
|
||||
return h("span", { class: "text-g-400" }, "—");
|
||||
}
|
||||
const inline = actions.slice(0, MAX_INLINE_ROW_ACTIONS);
|
||||
const overflow = actions.slice(MAX_INLINE_ROW_ACTIONS);
|
||||
|
||||
const inlineNodes = inline.map((a) =>
|
||||
h(ElTooltip, { content: a.label, placement: "top" }, () =>
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
class: a.disabled ? "inline-flex opacity-40 pointer-events-none" : "inline-flex",
|
||||
},
|
||||
[
|
||||
h(ArtButtonTable, {
|
||||
type: a.artType,
|
||||
icon: a.icon,
|
||||
onClick: a.run,
|
||||
}),
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (overflow.length === 0) {
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 user-table-actions" },
|
||||
inlineNodes
|
||||
);
|
||||
}
|
||||
|
||||
const dropdown = h(
|
||||
ElDropdown,
|
||||
{ trigger: "click" },
|
||||
{
|
||||
default: () =>
|
||||
h(ElTooltip, { content: "更多", placement: "top" }, () =>
|
||||
h("span", { class: "inline-flex align-middle" }, [
|
||||
h(ArtButtonTable, {
|
||||
type: "more",
|
||||
onClick: () => {},
|
||||
}),
|
||||
])
|
||||
),
|
||||
dropdown: () =>
|
||||
h(
|
||||
ElDropdownMenu,
|
||||
null,
|
||||
overflow.map((a) =>
|
||||
h(
|
||||
ElDropdownItem,
|
||||
{
|
||||
key: a.key,
|
||||
disabled: a.disabled,
|
||||
onClick: () => a.run(),
|
||||
},
|
||||
() => a.label
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
return h(
|
||||
"div",
|
||||
{ class: "inline-flex flex-wrap items-center justify-end gap-1 user-table-actions" },
|
||||
[...inlineNodes, dropdown]
|
||||
);
|
||||
return renderTableOperationCell(actions, {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 user-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
const dataFormRef = ref();
|
||||
|
||||
@@ -317,11 +317,11 @@ import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue"
|
||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import type { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import type { ColumnOption } from "@/types/component";
|
||||
import { useAuth } from "@/hooks/core/useAuth";
|
||||
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { h, computed, nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { computed, nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { vue3CronPlus } from "vue3-cron-plus";
|
||||
import "vue3-cron-plus/dist/index.css";
|
||||
@@ -332,6 +332,7 @@ import "codemirror/mode/python/python.js";
|
||||
import "codemirror/theme/dracula.css";
|
||||
|
||||
const dictStore = useDictStore();
|
||||
const { hasAuth } = useAuth();
|
||||
|
||||
const BATCH_DELETE_NODE_MSG =
|
||||
"确认删除选中的节点吗?\n" +
|
||||
@@ -389,20 +390,6 @@ function onTableSelectionChange(rows: NodeTable[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function buttonMoreClick(item: ButtonMoreItem, row: NodeTable) {
|
||||
switch (item.key) {
|
||||
case "execute":
|
||||
handleOpenExecuteDialog(row);
|
||||
break;
|
||||
case "edit":
|
||||
if (row.id != null) void handleOpenDialog("update", row.id);
|
||||
break;
|
||||
case "delete":
|
||||
void deleteNodeRow(row.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteNodeRow(id: number | undefined) {
|
||||
if (id == null) return;
|
||||
ElMessageBox.confirm("确认删除该节点吗?将从调度器移除相关任务。", "警告", {
|
||||
@@ -419,6 +406,47 @@ function deleteNodeRow(id: number | undefined) {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function buildNodeRowActions(row: NodeTable): TableOperationAction[] {
|
||||
const all: TableOperationAction[] = [
|
||||
{
|
||||
key: "execute",
|
||||
label: "调试",
|
||||
artType: "more",
|
||||
icon: "ri:play-circle-line",
|
||||
iconColor: "var(--el-color-primary)",
|
||||
perm: "module_task:cronjob:node:execute",
|
||||
run: () => handleOpenExecuteDialog(row),
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
artType: "edit",
|
||||
icon: "ri:edit-2-line",
|
||||
perm: "module_task:cronjob:node:update",
|
||||
run: () => {
|
||||
if (row.id != null) void handleOpenDialog("update", row.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
artType: "delete",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
perm: "module_task:cronjob:node:delete",
|
||||
run: () => {
|
||||
void deleteNodeRow(row.id);
|
||||
},
|
||||
},
|
||||
];
|
||||
return all.filter((a) => a.perm != null && hasAuth(a.perm));
|
||||
}
|
||||
|
||||
function formatNodeOperationCell(row: NodeTable) {
|
||||
return renderTableOperationCell(buildNodeRowActions(row), {
|
||||
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 cronjob-node-table-actions",
|
||||
});
|
||||
}
|
||||
|
||||
function handleBatchDelete() {
|
||||
const ids = selectedIds.value;
|
||||
if (ids.length === 0) return;
|
||||
@@ -498,36 +526,10 @@ const {
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 88,
|
||||
width: 220,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
formatter: (row: NodeTable) =>
|
||||
h("div", [
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "execute",
|
||||
label: "调试",
|
||||
icon: "ri:play-circle-line",
|
||||
auth: "module_task:cronjob:node:execute",
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑",
|
||||
icon: "ri:edit-2-line",
|
||||
auth: "module_task:cronjob:node:update",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
auth: "module_task:cronjob:node:delete",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
]),
|
||||
align: "right",
|
||||
formatter: (row: NodeTable) => formatNodeOperationCell(row),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,479 +0,0 @@
|
||||
<!-- 菜单管理页面 -->
|
||||
<template>
|
||||
<div class="menu-page art-full-height">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model="formFilters"
|
||||
:items="formItems"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader
|
||||
:showZebra="false"
|
||||
:loading="loading"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>添加菜单</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
{{ isExpanded ? "收起" : "展开" }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="path"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="filteredTableData"
|
||||
:stripe="false"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
/>
|
||||
|
||||
<!-- 菜单弹窗 -->
|
||||
<MenuDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:type="dialogType"
|
||||
:editData="editData"
|
||||
:lockType="lockMenuType"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatMenuTitle } from "@/utils/navigation/router";
|
||||
import ArtButtonTable from "@/components/Core/forms/art-button-table/index.vue";
|
||||
import { useTableColumns } from "@/hooks/core/useTableColumns";
|
||||
import type { AppRouteRecord } from "@/types/router";
|
||||
import MenuDialog from "./modules/menu-dialog.vue";
|
||||
import { fetchGetMenuList } from "@/api/system-manage";
|
||||
import { ElTag, ElMessageBox } from "element-plus";
|
||||
|
||||
defineOptions({ name: "Menus" });
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false);
|
||||
const isExpanded = ref(false);
|
||||
const tableRef = ref();
|
||||
|
||||
// 弹窗相关
|
||||
const dialogVisible = ref(false);
|
||||
const dialogType = ref<"menu" | "button">("menu");
|
||||
const editData = ref<AppRouteRecord | any>(null);
|
||||
const lockMenuType = ref(false);
|
||||
|
||||
// 搜索相关
|
||||
const initialSearchState = {
|
||||
name: "",
|
||||
route: "",
|
||||
};
|
||||
|
||||
const formFilters = reactive({ ...initialSearchState });
|
||||
const appliedFilters = reactive({ ...initialSearchState });
|
||||
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
label: "菜单名称",
|
||||
key: "name",
|
||||
type: "input",
|
||||
props: { clearable: true },
|
||||
},
|
||||
{
|
||||
label: "路由地址",
|
||||
key: "route",
|
||||
type: "input",
|
||||
props: { clearable: true },
|
||||
},
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
getMenuList();
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取菜单列表数据
|
||||
*/
|
||||
const getMenuList = async (): Promise<void> => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const list = await fetchGetMenuList();
|
||||
tableData.value = list;
|
||||
} catch (error) {
|
||||
throw error instanceof Error ? error : new Error("获取菜单失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取菜单类型标签颜色
|
||||
* @param row 菜单行数据
|
||||
* @returns 标签颜色类型
|
||||
*/
|
||||
const getMenuTypeTag = (
|
||||
row: AppRouteRecord
|
||||
): "primary" | "success" | "warning" | "info" | "danger" => {
|
||||
if (row.meta?.isAuthButton) return "danger";
|
||||
if (row.children?.length) return "info";
|
||||
if (row.meta?.link && row.meta?.isIframe) return "success";
|
||||
if (row.path) return "primary";
|
||||
if (row.meta?.link) return "warning";
|
||||
return "info";
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取菜单类型文本
|
||||
* @param row 菜单行数据
|
||||
* @returns 菜单类型文本
|
||||
*/
|
||||
const getMenuTypeText = (row: AppRouteRecord): string => {
|
||||
if (row.meta?.isAuthButton) return "按钮";
|
||||
if (row.children?.length) return "目录";
|
||||
if (row.meta?.link && row.meta?.isIframe) return "内嵌";
|
||||
if (row.path) return "菜单";
|
||||
if (row.meta?.link) return "外链";
|
||||
return "未知";
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const { columnChecks, columns } = useTableColumns(() => [
|
||||
{
|
||||
prop: "meta.title",
|
||||
label: "菜单名称",
|
||||
minWidth: 120,
|
||||
formatter: (row: AppRouteRecord) => formatMenuTitle(row.meta?.title),
|
||||
},
|
||||
{
|
||||
prop: "type",
|
||||
label: "菜单类型",
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(ElTag, { type: getMenuTypeTag(row) }, () => getMenuTypeText(row));
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "path",
|
||||
label: "路由",
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
if (row.meta?.isAuthButton) return "";
|
||||
return row.meta?.link || row.path || "";
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "meta.authList",
|
||||
label: "权限标识",
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
if (row.meta?.isAuthButton) {
|
||||
return row.meta?.authMark || "";
|
||||
}
|
||||
if (!row.meta?.authList?.length) return "";
|
||||
return `${row.meta.authList.length} 个权限标识`;
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "date",
|
||||
label: "编辑时间",
|
||||
formatter: () => "2022-3-12 12:00:00",
|
||||
},
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
formatter: () => h(ElTag, { type: "success" }, () => "启用"),
|
||||
},
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 180,
|
||||
align: "right",
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
const buttonStyle = { style: "text-align: right" };
|
||||
|
||||
if (row.meta?.isAuthButton) {
|
||||
return h("div", buttonStyle, [
|
||||
h(ArtButtonTable, {
|
||||
type: "edit",
|
||||
onClick: () => handleEditAuth(row),
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: "delete",
|
||||
onClick: () => handleDeleteAuth(),
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return h("div", buttonStyle, [
|
||||
h(ArtButtonTable, {
|
||||
type: "add",
|
||||
onClick: () => handleAddAuth(),
|
||||
title: "新增权限",
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: "edit",
|
||||
onClick: () => handleEditMenu(row),
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: "delete",
|
||||
onClick: () => handleDeleteMenu(),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 数据相关
|
||||
const tableData = ref<AppRouteRecord[]>([]);
|
||||
|
||||
/**
|
||||
* 重置搜索条件
|
||||
*/
|
||||
const handleReset = (): void => {
|
||||
Object.assign(formFilters, { ...initialSearchState });
|
||||
Object.assign(appliedFilters, { ...initialSearchState });
|
||||
getMenuList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行搜索
|
||||
*/
|
||||
const handleSearch = (): void => {
|
||||
Object.assign(appliedFilters, { ...formFilters });
|
||||
getMenuList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新菜单列表
|
||||
*/
|
||||
const handleRefresh = (): void => {
|
||||
getMenuList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 深度克隆对象
|
||||
* @param obj 要克隆的对象
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
const deepClone = <T,>(obj: T): T => {
|
||||
if (obj === null || typeof obj !== "object") return obj;
|
||||
if (obj instanceof Date) return new Date(obj) as T;
|
||||
if (Array.isArray(obj)) return obj.map((item) => deepClone(item)) as T;
|
||||
|
||||
const cloned = {} as T;
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
cloned[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return cloned;
|
||||
};
|
||||
|
||||
/**
|
||||
* 将权限列表转换为子节点
|
||||
* @param items 菜单项数组
|
||||
* @returns 转换后的菜单项数组
|
||||
*/
|
||||
const convertAuthListToChildren = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
return items.map((item) => {
|
||||
const clonedItem = deepClone(item);
|
||||
|
||||
if (clonedItem.children?.length) {
|
||||
clonedItem.children = convertAuthListToChildren(clonedItem.children);
|
||||
}
|
||||
|
||||
if (item.meta?.authList?.length) {
|
||||
const authChildren: AppRouteRecord[] = item.meta.authList.map(
|
||||
(auth: { title: string; authMark: string }) => ({
|
||||
path: `${item.path}_auth_${auth.authMark}`,
|
||||
name: `${String(item.name)}_auth_${auth.authMark}`,
|
||||
meta: {
|
||||
title: auth.title,
|
||||
authMark: auth.authMark,
|
||||
isAuthButton: true,
|
||||
parentPath: item.path,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
clonedItem.children = clonedItem.children?.length
|
||||
? [...clonedItem.children, ...authChildren]
|
||||
: authChildren;
|
||||
}
|
||||
|
||||
return clonedItem;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索菜单
|
||||
* @param items 菜单项数组
|
||||
* @returns 搜索结果数组
|
||||
*/
|
||||
const searchMenu = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
const results: AppRouteRecord[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const searchName = appliedFilters.name?.toLowerCase().trim() || "";
|
||||
const searchRoute = appliedFilters.route?.toLowerCase().trim() || "";
|
||||
const menuTitle = formatMenuTitle(item.meta?.title || "").toLowerCase();
|
||||
const menuPath = (item.path || "").toLowerCase();
|
||||
const nameMatch = !searchName || menuTitle.includes(searchName);
|
||||
const routeMatch = !searchRoute || menuPath.includes(searchRoute);
|
||||
|
||||
if (item.children?.length) {
|
||||
const matchedChildren = searchMenu(item.children);
|
||||
if (matchedChildren.length > 0) {
|
||||
const clonedItem = deepClone(item);
|
||||
clonedItem.children = matchedChildren;
|
||||
results.push(clonedItem);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (nameMatch && routeMatch) {
|
||||
results.push(deepClone(item));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// 过滤后的表格数据
|
||||
const filteredTableData = computed(() => {
|
||||
const searchedData = searchMenu(tableData.value);
|
||||
return convertAuthListToChildren(searchedData);
|
||||
});
|
||||
|
||||
/**
|
||||
* 添加菜单
|
||||
*/
|
||||
const handleAddMenu = (): void => {
|
||||
dialogType.value = "menu";
|
||||
editData.value = null;
|
||||
lockMenuType.value = true;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加权限按钮
|
||||
*/
|
||||
const handleAddAuth = (): void => {
|
||||
dialogType.value = "menu";
|
||||
editData.value = null;
|
||||
lockMenuType.value = false;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑菜单
|
||||
* @param row 菜单行数据
|
||||
*/
|
||||
const handleEditMenu = (row: AppRouteRecord): void => {
|
||||
dialogType.value = "menu";
|
||||
editData.value = row;
|
||||
lockMenuType.value = true;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 编辑权限按钮
|
||||
* @param row 权限行数据
|
||||
*/
|
||||
const handleEditAuth = (row: AppRouteRecord): void => {
|
||||
dialogType.value = "button";
|
||||
editData.value = {
|
||||
title: row.meta?.title,
|
||||
authMark: row.meta?.authMark,
|
||||
};
|
||||
lockMenuType.value = false;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单表单数据类型
|
||||
*/
|
||||
interface MenuFormData {
|
||||
name: string;
|
||||
path: string;
|
||||
component?: string;
|
||||
icon?: string;
|
||||
roles?: string[];
|
||||
sort?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单数据
|
||||
* @param formData 表单数据
|
||||
*/
|
||||
const handleSubmit = (formData: MenuFormData): void => {
|
||||
console.log("提交数据:", formData);
|
||||
// TODO: 调用API保存数据
|
||||
getMenuList();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*/
|
||||
const handleDeleteMenu = async (): Promise<void> => {
|
||||
try {
|
||||
await ElMessageBox.confirm("确定要删除该菜单吗?删除后无法恢复", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
ElMessage.success("删除成功");
|
||||
getMenuList();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除权限按钮
|
||||
*/
|
||||
const handleDeleteAuth = async (): Promise<void> => {
|
||||
try {
|
||||
await ElMessageBox.confirm("确定要删除该权限吗?删除后无法恢复", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
ElMessage.success("删除成功");
|
||||
getMenuList();
|
||||
} catch (error) {
|
||||
if (error !== "cancel") {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换展开/收起所有菜单
|
||||
*/
|
||||
const toggleExpand = (): void => {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
nextTick(() => {
|
||||
if (tableRef.value?.elTableRef && filteredTableData.value) {
|
||||
const processRows = (rows: AppRouteRecord[]) => {
|
||||
rows.forEach((row) => {
|
||||
if (row.children?.length) {
|
||||
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value);
|
||||
processRows(row.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
processRows(filteredTableData.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -1,384 +0,0 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:title="dialogTitle"
|
||||
:model-value="visible"
|
||||
@update:model-value="handleCancel"
|
||||
width="860px"
|
||||
align-center
|
||||
class="menu-dialog"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<ArtForm
|
||||
ref="formRef"
|
||||
v-model="form"
|
||||
:items="formItems"
|
||||
:rules="rules"
|
||||
:span="width > 640 ? 12 : 24"
|
||||
:gutter="20"
|
||||
label-width="100px"
|
||||
:show-reset="false"
|
||||
:show-submit="false"
|
||||
>
|
||||
<template #menuType>
|
||||
<ElRadioGroup v-model="form.menuType" :disabled="disableMenuType">
|
||||
<ElRadioButton value="menu" label="menu">菜单</ElRadioButton>
|
||||
<ElRadioButton value="button" label="button">按钮</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
</template>
|
||||
</ArtForm>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="handleCancel">取 消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">确 定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormRules } from "element-plus";
|
||||
import { ElIcon, ElTooltip } from "element-plus";
|
||||
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||
import { formatMenuTitle } from "@/utils/navigation/router";
|
||||
import type { AppRouteRecord } from "@/types/router";
|
||||
import type { FormItem } from "@/components/Core/forms/art-form/index.vue";
|
||||
import ArtForm from "@/components/Core/forms/art-form/index.vue";
|
||||
import { useWindowSize } from "@vueuse/core";
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
/**
|
||||
* 创建带 tooltip 的表单标签
|
||||
* @param label 标签文本
|
||||
* @param tooltip 提示文本
|
||||
* @returns 渲染函数
|
||||
*/
|
||||
const createLabelTooltip = (label: string, tooltip: string) => {
|
||||
return () =>
|
||||
h("span", { class: "flex items-center" }, [
|
||||
h("span", label),
|
||||
h(
|
||||
ElTooltip,
|
||||
{
|
||||
content: tooltip,
|
||||
placement: "top",
|
||||
},
|
||||
() => h(ElIcon, { class: "ml-0.5 cursor-help" }, () => h(QuestionFilled))
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
interface MenuFormData {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
label: string;
|
||||
component: string;
|
||||
icon: string;
|
||||
isEnable: boolean;
|
||||
sort: number;
|
||||
isMenu: boolean;
|
||||
keepAlive: boolean;
|
||||
isHide: boolean;
|
||||
isHideTab: boolean;
|
||||
link: string;
|
||||
isIframe: boolean;
|
||||
showBadge: boolean;
|
||||
showTextBadge: string;
|
||||
fixedTab: boolean;
|
||||
activePath: string;
|
||||
roles: string[];
|
||||
isFullPage: boolean;
|
||||
authName: string;
|
||||
authLabel: string;
|
||||
authIcon: string;
|
||||
authSort: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
editData?: AppRouteRecord | any;
|
||||
type?: "menu" | "button";
|
||||
lockType?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "submit", data: MenuFormData): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
type: "menu",
|
||||
lockType: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
|
||||
const form = reactive<MenuFormData & { menuType: "menu" | "button" }>({
|
||||
menuType: "menu",
|
||||
id: 0,
|
||||
name: "",
|
||||
path: "",
|
||||
label: "",
|
||||
component: "",
|
||||
icon: "",
|
||||
isEnable: true,
|
||||
sort: 1,
|
||||
isMenu: true,
|
||||
keepAlive: true,
|
||||
isHide: false,
|
||||
isHideTab: false,
|
||||
link: "",
|
||||
isIframe: false,
|
||||
showBadge: false,
|
||||
showTextBadge: "",
|
||||
fixedTab: false,
|
||||
activePath: "",
|
||||
roles: [],
|
||||
isFullPage: false,
|
||||
authName: "",
|
||||
authLabel: "",
|
||||
authIcon: "",
|
||||
authSort: 1,
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: "请输入菜单名称", trigger: "blur" },
|
||||
{ min: 2, max: 20, message: "长度在 2 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
path: [{ required: true, message: "请输入路由地址", trigger: "blur" }],
|
||||
label: [{ required: true, message: "输入权限标识", trigger: "blur" }],
|
||||
authName: [{ required: true, message: "请输入权限名称", trigger: "blur" }],
|
||||
authLabel: [{ required: true, message: "请输入权限标识", trigger: "blur" }],
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单项配置
|
||||
*/
|
||||
const formItems = computed<FormItem[]>(() => {
|
||||
const baseItems: FormItem[] = [{ label: "菜单类型", key: "menuType", span: 24 }];
|
||||
|
||||
// Switch 组件的 span:小屏幕 12,大屏幕 6
|
||||
const switchSpan = width.value < 640 ? 12 : 6;
|
||||
|
||||
if (form.menuType === "menu") {
|
||||
return [
|
||||
...baseItems,
|
||||
{ label: "菜单名称", key: "name", type: "input", props: { placeholder: "菜单名称" } },
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
"路由地址",
|
||||
"一级菜单:以 / 开头的绝对路径(如 /dashboard)\n二级及以下:相对路径(如 console、user)"
|
||||
),
|
||||
key: "path",
|
||||
type: "input",
|
||||
props: { placeholder: "如:/dashboard 或 console" },
|
||||
},
|
||||
{ label: "权限标识", key: "label", type: "input", props: { placeholder: "如:User" } },
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
"组件路径",
|
||||
"一级父级菜单:填写 /index/index\n具体页面:填写组件路径(如 /system/user)\n目录菜单:留空"
|
||||
),
|
||||
key: "component",
|
||||
type: "input",
|
||||
props: { placeholder: "如:/system/user 或留空" },
|
||||
},
|
||||
{ label: "图标", key: "icon", type: "input", props: { placeholder: "如:ri:user-line" } },
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
"角色权限",
|
||||
"仅用于前端权限模式:配置角色标识(如 R_SUPER、R_ADMIN)\n后端权限模式:无需配置"
|
||||
),
|
||||
key: "roles",
|
||||
type: "inputtag",
|
||||
props: { placeholder: "输入角色标识后按回车,如:R_SUPER" },
|
||||
},
|
||||
{
|
||||
label: "菜单排序",
|
||||
key: "sort",
|
||||
type: "number",
|
||||
props: { min: 1, controlsPosition: "right", style: { width: "100%" } },
|
||||
},
|
||||
{
|
||||
label: "外部链接",
|
||||
key: "link",
|
||||
type: "input",
|
||||
props: { placeholder: "如:https://www.example.com" },
|
||||
},
|
||||
{
|
||||
label: "文本徽章",
|
||||
key: "showTextBadge",
|
||||
type: "input",
|
||||
props: { placeholder: "如:New、Hot" },
|
||||
},
|
||||
{
|
||||
label: createLabelTooltip(
|
||||
"激活路径",
|
||||
'用于详情页等隐藏菜单,指定高亮显示的父级菜单路径\n例如:用户详情页高亮显示"用户管理"菜单'
|
||||
),
|
||||
key: "activePath",
|
||||
type: "input",
|
||||
props: { placeholder: "如:/system/user" },
|
||||
},
|
||||
{ label: "是否启用", key: "isEnable", type: "switch", span: switchSpan },
|
||||
{ label: "页面缓存", key: "keepAlive", type: "switch", span: switchSpan },
|
||||
{ label: "隐藏菜单", key: "isHide", type: "switch", span: switchSpan },
|
||||
{ label: "是否内嵌", key: "isIframe", type: "switch", span: switchSpan },
|
||||
{ label: "显示徽章", key: "showBadge", type: "switch", span: switchSpan },
|
||||
{ label: "固定标签", key: "fixedTab", type: "switch", span: switchSpan },
|
||||
{ label: "标签隐藏", key: "isHideTab", type: "switch", span: switchSpan },
|
||||
{ label: "全屏页面", key: "isFullPage", type: "switch", span: switchSpan },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...baseItems,
|
||||
{
|
||||
label: "权限名称",
|
||||
key: "authName",
|
||||
type: "input",
|
||||
props: { placeholder: "如:新增、编辑、删除" },
|
||||
},
|
||||
{
|
||||
label: "权限标识",
|
||||
key: "authLabel",
|
||||
type: "input",
|
||||
props: { placeholder: "如:add、edit、delete" },
|
||||
},
|
||||
{
|
||||
label: "权限排序",
|
||||
key: "authSort",
|
||||
type: "number",
|
||||
props: { min: 1, controlsPosition: "right", style: { width: "100%" } },
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const type = form.menuType === "menu" ? "菜单" : "按钮";
|
||||
return isEdit.value ? `编辑${type}` : `新建${type}`;
|
||||
});
|
||||
|
||||
/**
|
||||
* 是否禁用菜单类型切换
|
||||
*/
|
||||
const disableMenuType = computed(() => {
|
||||
if (isEdit.value) return true;
|
||||
if (!isEdit.value && form.menuType === "menu" && props.lockType) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* 重置表单数据
|
||||
*/
|
||||
const resetForm = (): void => {
|
||||
formRef.value?.reset();
|
||||
form.menuType = "menu";
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载表单数据(编辑模式)
|
||||
*/
|
||||
const loadFormData = (): void => {
|
||||
if (!props.editData) return;
|
||||
|
||||
isEdit.value = true;
|
||||
|
||||
if (form.menuType === "menu") {
|
||||
const row = props.editData;
|
||||
form.id = row.id || 0;
|
||||
form.name = formatMenuTitle(row.meta?.title || "");
|
||||
form.path = row.path || "";
|
||||
form.label = row.name || "";
|
||||
form.component = row.component || "";
|
||||
form.icon = row.meta?.icon || "";
|
||||
form.sort = row.meta?.sort || 1;
|
||||
form.isMenu = row.meta?.isMenu ?? true;
|
||||
form.keepAlive = row.meta?.keepAlive ?? false;
|
||||
form.isHide = row.meta?.isHide ?? false;
|
||||
form.isHideTab = row.meta?.isHideTab ?? false;
|
||||
form.isEnable = row.meta?.isEnable ?? true;
|
||||
form.link = row.meta?.link || "";
|
||||
form.isIframe = row.meta?.isIframe ?? false;
|
||||
form.showBadge = row.meta?.showBadge ?? false;
|
||||
form.showTextBadge = row.meta?.showTextBadge || "";
|
||||
form.fixedTab = row.meta?.fixedTab ?? false;
|
||||
form.activePath = row.meta?.activePath || "";
|
||||
form.roles = row.meta?.roles || [];
|
||||
form.isFullPage = row.meta?.isFullPage ?? false;
|
||||
} else {
|
||||
const row = props.editData;
|
||||
form.authName = row.title || "";
|
||||
form.authLabel = row.authMark || "";
|
||||
form.authIcon = row.icon || "";
|
||||
form.authSort = row.sort || 1;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async (): Promise<void> => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
emit("submit", { ...form });
|
||||
ElMessage.success(`${isEdit.value ? "编辑" : "新增"}成功`);
|
||||
handleCancel();
|
||||
} catch {
|
||||
ElMessage.error("表单校验失败,请检查输入");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消操作
|
||||
*/
|
||||
const handleCancel = (): void => {
|
||||
emit("update:visible", false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 对话框关闭后的回调
|
||||
*/
|
||||
const handleClosed = (): void => {
|
||||
resetForm();
|
||||
isEdit.value = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听对话框显示状态
|
||||
*/
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
form.menuType = props.type;
|
||||
nextTick(() => {
|
||||
if (props.editData) {
|
||||
loadFormData();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听菜单类型变化
|
||||
*/
|
||||
watch(
|
||||
() => props.type,
|
||||
(newType) => {
|
||||
if (props.visible) {
|
||||
form.menuType = newType;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -1,239 +0,0 @@
|
||||
<!-- 角色管理页面 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<RoleSearch
|
||||
v-show="showSearchBar"
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></RoleSearch>
|
||||
|
||||
<ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
|
||||
<ArtTableHeader
|
||||
v-model:columns="columnChecks"
|
||||
v-model:showSearchBar="showSearchBar"
|
||||
:loading="loading"
|
||||
@refresh="refreshData"
|
||||
>
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
></ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 角色编辑弹窗 -->
|
||||
<RoleEditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:role-data="currentRoleData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
|
||||
<!-- 菜单权限弹窗 -->
|
||||
<RolePermissionDialog
|
||||
v-model="permissionDialog"
|
||||
:role-data="currentRoleData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonMoreItem } from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { fetchGetRoleList } from "@/api/system-manage";
|
||||
import ArtButtonMore from "@/components/Core/forms/art-button-more/index.vue";
|
||||
import RoleSearch from "./modules/role-search.vue";
|
||||
import RoleEditDialog from "./modules/role-edit-dialog.vue";
|
||||
import RolePermissionDialog from "./modules/role-permission-dialog.vue";
|
||||
import { ElTag, ElMessageBox } from "element-plus";
|
||||
|
||||
defineOptions({ name: "Role" });
|
||||
|
||||
type RoleListItem = RoleListItem;
|
||||
type RoleSearchFormParams = RoleSearchParams & {
|
||||
daterange?: string[];
|
||||
};
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref<RoleSearchFormParams>({
|
||||
roleName: undefined,
|
||||
roleCode: undefined,
|
||||
description: undefined,
|
||||
enabled: undefined,
|
||||
daterange: undefined,
|
||||
});
|
||||
|
||||
const showSearchBar = ref(false);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const permissionDialog = ref(false);
|
||||
const currentRoleData = ref<RoleListItem | undefined>(undefined);
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
replaceSearchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData,
|
||||
} = useTable({
|
||||
// 核心配置
|
||||
core: {
|
||||
apiFn: fetchGetRoleList,
|
||||
apiParams: {
|
||||
current: 1,
|
||||
size: 20,
|
||||
},
|
||||
// 排除 apiParams 中的属性
|
||||
excludeParams: ["daterange"],
|
||||
columnsFactory: () => [
|
||||
{
|
||||
prop: "roleId",
|
||||
label: "角色ID",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
prop: "roleName",
|
||||
label: "角色名称",
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
prop: "roleCode",
|
||||
label: "角色编码",
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
prop: "description",
|
||||
label: "角色描述",
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
prop: "enabled",
|
||||
label: "角色状态",
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
const statusConfig = row.enabled
|
||||
? { type: "success", text: "启用" }
|
||||
: { type: "warning", text: "禁用" };
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: statusConfig.type as "success" | "warning" },
|
||||
() => statusConfig.text
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "createTime",
|
||||
label: "创建日期",
|
||||
width: 180,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 80,
|
||||
fixed: "right",
|
||||
formatter: (row) =>
|
||||
h("div", [
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: "permission",
|
||||
label: "菜单权限",
|
||||
icon: "ri:user-3-line",
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: "编辑角色",
|
||||
icon: "ri:edit-2-line",
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: "删除角色",
|
||||
icon: "ri:delete-bin-4-line",
|
||||
color: "#f56c6c",
|
||||
},
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const dialogType = ref<"add" | "edit">("add");
|
||||
|
||||
const showDialog = (type: "add" | "edit", row?: RoleListItem) => {
|
||||
dialogVisible.value = true;
|
||||
dialogType.value = type;
|
||||
currentRoleData.value = row;
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索处理
|
||||
* @param params 搜索参数
|
||||
*/
|
||||
const handleSearch = (params: RoleSearchFormParams) => {
|
||||
// 处理日期区间参数,把 daterange 转换为 startTime 和 endTime
|
||||
const { daterange, ...filtersParams } = params;
|
||||
const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null];
|
||||
|
||||
replaceSearchParams({ ...filtersParams, startTime, endTime });
|
||||
getData();
|
||||
};
|
||||
|
||||
const buttonMoreClick = (item: ButtonMoreItem, row: RoleListItem) => {
|
||||
switch (item.key) {
|
||||
case "permission":
|
||||
showPermissionDialog(row);
|
||||
break;
|
||||
case "edit":
|
||||
showDialog("edit", row);
|
||||
break;
|
||||
case "delete":
|
||||
deleteRole(row);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const showPermissionDialog = (row?: RoleListItem) => {
|
||||
permissionDialog.value = true;
|
||||
currentRoleData.value = row;
|
||||
};
|
||||
|
||||
const deleteRole = (row: RoleListItem) => {
|
||||
ElMessageBox.confirm(`确定删除角色"${row.roleName}"吗?此操作不可恢复!`, "删除确认", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
// TODO: 调用删除接口
|
||||
ElMessage.success("删除成功");
|
||||
refreshData();
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info("已取消删除");
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -1,162 +0,0 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||
width="30%"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="角色名称" prop="roleName">
|
||||
<ElInput v-model="form.roleName" placeholder="请输入角色名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色编码" prop="roleCode">
|
||||
<ElInput v-model="form.roleCode" placeholder="请输入角色编码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入角色描述"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启用">
|
||||
<ElSwitch v-model="form.enabled" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="handleClose">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
|
||||
type RoleListItem = RoleListItem;
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
dialogType: "add" | "edit";
|
||||
roleData?: RoleListItem;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "success"): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: "add",
|
||||
roleData: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
roleName: [
|
||||
{ required: true, message: "请输入角色名称", trigger: "blur" },
|
||||
{ min: 2, max: 20, message: "长度在 2 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
roleCode: [
|
||||
{ required: true, message: "请输入角色编码", trigger: "blur" },
|
||||
{ min: 2, max: 50, message: "长度在 2 到 50 个字符", trigger: "blur" },
|
||||
],
|
||||
description: [{ required: true, message: "请输入角色描述", trigger: "blur" }],
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const form = reactive<RoleListItem>({
|
||||
roleId: 0,
|
||||
roleName: "",
|
||||
roleCode: "",
|
||||
description: "",
|
||||
createTime: "",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) initForm();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 监听角色数据变化,更新表单
|
||||
*/
|
||||
watch(
|
||||
() => props.roleData,
|
||||
(newData) => {
|
||||
if (newData && props.modelValue) initForm();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
* 根据弹窗类型填充表单或重置表单
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.dialogType === "edit" && props.roleData) {
|
||||
Object.assign(form, props.roleData);
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
roleId: 0,
|
||||
roleName: "",
|
||||
roleCode: "",
|
||||
description: "",
|
||||
createTime: "",
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
* 验证通过后调用接口保存数据
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
// TODO: 调用新增/编辑接口
|
||||
const message = props.dialogType === "add" ? "新增成功" : "修改成功";
|
||||
ElMessage.success(message);
|
||||
emit("success");
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.log("表单验证失败:", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,254 +0,0 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
title="菜单权限"
|
||||
width="520px"
|
||||
align-center
|
||||
class="el-dialog-border"
|
||||
@close="handleClose"
|
||||
>
|
||||
<ElScrollbar height="70vh">
|
||||
<ElTree
|
||||
ref="treeRef"
|
||||
:data="processedMenuList"
|
||||
show-checkbox
|
||||
node-key="name"
|
||||
:default-expand-all="isExpandAll"
|
||||
:default-checked-keys="[1, 2, 3]"
|
||||
:props="defaultProps"
|
||||
@check="handleTreeCheck"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span v-if="data.isAuth">
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-else>{{ defaultProps.label(data) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
<template #footer>
|
||||
<ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton>
|
||||
|
||||
<ElButton @click="toggleExpandAll">{{ isExpandAll ? "全部收起" : "全部展开" }}</ElButton>
|
||||
<ElButton @click="toggleSelectAll" style="margin-left: 8px">
|
||||
{{ isSelectAll ? "取消全选" : "全部选择" }}
|
||||
</ElButton>
|
||||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMenuStore } from "@/store/modules/menu.store";
|
||||
import { formatMenuTitle } from "@/utils/navigation/router";
|
||||
|
||||
type RoleListItem = RoleListItem;
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
roleData?: RoleListItem;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
(e: "success"): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
roleData: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { menuList } = storeToRefs(useMenuStore());
|
||||
const treeRef = ref();
|
||||
const isExpandAll = ref(true);
|
||||
const isSelectAll = ref(false);
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
/**
|
||||
* 菜单节点类型
|
||||
*/
|
||||
interface MenuNode {
|
||||
id?: string | number;
|
||||
name?: string;
|
||||
label?: string;
|
||||
meta?: {
|
||||
title?: string;
|
||||
authList?: Array<{
|
||||
authMark: string;
|
||||
title: string;
|
||||
checked?: boolean;
|
||||
}>;
|
||||
};
|
||||
children?: MenuNode[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理菜单数据,将 authList 转换为树形子节点
|
||||
* 递归处理菜单树,将权限列表展开为可选择的子节点
|
||||
*/
|
||||
const processedMenuList = computed(() => {
|
||||
const processNode = (node: MenuNode): MenuNode => {
|
||||
const processed = { ...node };
|
||||
|
||||
// 如果有 authList,将其转换为子节点
|
||||
if (node.meta?.authList?.length) {
|
||||
const authNodes = node.meta.authList.map((auth) => ({
|
||||
id: `${node.id}_${auth.authMark}`,
|
||||
name: `${node.name}_${auth.authMark}`,
|
||||
label: auth.title,
|
||||
authMark: auth.authMark,
|
||||
isAuth: true,
|
||||
checked: auth.checked || false,
|
||||
}));
|
||||
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes;
|
||||
}
|
||||
|
||||
// 递归处理子节点
|
||||
if (processed.children) {
|
||||
processed.children = processed.children.map(processNode);
|
||||
}
|
||||
|
||||
return processed;
|
||||
};
|
||||
|
||||
return (menuList.value as any[]).map(processNode);
|
||||
});
|
||||
|
||||
/**
|
||||
* 树形组件配置
|
||||
*/
|
||||
const defaultProps = {
|
||||
children: "children",
|
||||
label: (data: any) => formatMenuTitle(data.meta?.title) || data.label || "",
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化权限数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal && props.roleData) {
|
||||
// TODO: 根据角色加载对应的权限数据
|
||||
console.log("设置权限:", props.roleData);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 关闭弹窗并清空选中状态
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
treeRef.value?.setCheckedKeys([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存权限配置
|
||||
*/
|
||||
const savePermission = () => {
|
||||
// TODO: 调用保存权限接口
|
||||
ElMessage.success("权限保存成功");
|
||||
emit("success");
|
||||
handleClose();
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换全部展开/收起状态
|
||||
*/
|
||||
const toggleExpandAll = () => {
|
||||
const tree = treeRef.value;
|
||||
if (!tree) return;
|
||||
|
||||
const nodes = tree.store.nodesMap;
|
||||
// 这里保留 any,因为 Element Plus 的内部节点类型较复杂
|
||||
Object.values(nodes).forEach((node: any) => {
|
||||
node.expanded = !isExpandAll.value;
|
||||
});
|
||||
|
||||
isExpandAll.value = !isExpandAll.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换全选/取消全选状态
|
||||
*/
|
||||
const toggleSelectAll = () => {
|
||||
const tree = treeRef.value;
|
||||
if (!tree) return;
|
||||
|
||||
if (!isSelectAll.value) {
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value);
|
||||
tree.setCheckedKeys(allKeys);
|
||||
} else {
|
||||
tree.setCheckedKeys([]);
|
||||
}
|
||||
|
||||
isSelectAll.value = !isSelectAll.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归获取所有节点的 key
|
||||
* @param nodes 节点列表
|
||||
* @returns 所有节点的 key 数组
|
||||
*/
|
||||
const getAllNodeKeys = (nodes: MenuNode[]): string[] => {
|
||||
const keys: string[] = [];
|
||||
const traverse = (nodeList: MenuNode[]): void => {
|
||||
nodeList.forEach((node) => {
|
||||
if (node.name) keys.push(node.name);
|
||||
if (node.children?.length) traverse(node.children);
|
||||
});
|
||||
};
|
||||
traverse(nodes);
|
||||
return keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理树节点选中状态变化
|
||||
* 同步更新全选按钮状态
|
||||
*/
|
||||
const handleTreeCheck = () => {
|
||||
const tree = treeRef.value;
|
||||
if (!tree) return;
|
||||
|
||||
const checkedKeys = tree.getCheckedKeys();
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value);
|
||||
|
||||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 输出选中的权限数据到控制台
|
||||
* 用于调试和查看当前选中的权限配置
|
||||
*/
|
||||
const outputSelectedData = () => {
|
||||
const tree = treeRef.value;
|
||||
if (!tree) return;
|
||||
|
||||
const selectedData = {
|
||||
checkedKeys: tree.getCheckedKeys(),
|
||||
halfCheckedKeys: tree.getHalfCheckedKeys(),
|
||||
checkedNodes: tree.getCheckedNodes(),
|
||||
halfCheckedNodes: tree.getHalfCheckedNodes(),
|
||||
totalChecked: tree.getCheckedKeys().length,
|
||||
totalHalfChecked: tree.getHalfCheckedKeys().length,
|
||||
};
|
||||
|
||||
console.log("=== 选中的权限数据 ===", selectedData);
|
||||
ElMessage.success(`已输出选中数据到控制台,共选中 ${selectedData.totalChecked} 个节点`);
|
||||
};
|
||||
</script>
|
||||
@@ -1,124 +0,0 @@
|
||||
<template>
|
||||
<ArtSearchBar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:items="formItems"
|
||||
:rules="rules"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type RoleSearchFormParams = RoleSearchParams & {
|
||||
daterange?: string[];
|
||||
};
|
||||
|
||||
interface Props {
|
||||
modelValue: RoleSearchFormParams;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: RoleSearchFormParams): void;
|
||||
(e: "search", params: RoleSearchFormParams): void;
|
||||
(e: "reset"): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const searchBarRef = ref();
|
||||
|
||||
/**
|
||||
* 表单数据双向绑定
|
||||
*/
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单校验规则
|
||||
*/
|
||||
const rules = {};
|
||||
|
||||
/**
|
||||
* 角色状态选项
|
||||
*/
|
||||
const statusOptions = ref([
|
||||
{ label: "启用", value: true },
|
||||
{ label: "禁用", value: false },
|
||||
]);
|
||||
|
||||
/**
|
||||
* 搜索表单配置项
|
||||
*/
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
label: "角色名称",
|
||||
key: "roleName",
|
||||
type: "input",
|
||||
placeholder: "请输入角色名称",
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
label: "角色编码",
|
||||
key: "roleCode",
|
||||
type: "input",
|
||||
placeholder: "请输入角色编码",
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
label: "角色描述",
|
||||
key: "description",
|
||||
type: "input",
|
||||
placeholder: "请输入角色描述",
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
label: "角色状态",
|
||||
key: "enabled",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择状态",
|
||||
options: statusOptions.value,
|
||||
clearable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "创建日期",
|
||||
key: "daterange",
|
||||
type: "datetime",
|
||||
props: {
|
||||
style: { width: "100%" },
|
||||
placeholder: "请选择日期范围",
|
||||
type: "daterange",
|
||||
rangeSeparator: "至",
|
||||
startPlaceholder: "开始日期",
|
||||
endPlaceholder: "结束日期",
|
||||
valueFormat: "YYYY-MM-DD",
|
||||
shortcuts: [
|
||||
{ text: "今日", value: [new Date(), new Date()] },
|
||||
{ text: "最近一周", value: [new Date(Date.now() - 604800000), new Date()] },
|
||||
{ text: "最近一个月", value: [new Date(Date.now() - 2592000000), new Date()] },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
/**
|
||||
* 处理重置事件
|
||||
*/
|
||||
const handleReset = () => {
|
||||
emit("reset");
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理搜索事件
|
||||
* 验证表单后触发搜索
|
||||
*/
|
||||
const handleSearch = async (params: RoleSearchFormParams) => {
|
||||
await searchBarRef.value.validate();
|
||||
emit("search", params);
|
||||
};
|
||||
</script>
|
||||
@@ -1,258 +0,0 @@
|
||||
<!-- 用户管理页面 -->
|
||||
<!-- art-full-height 自动计算出页面剩余高度 -->
|
||||
<!-- art-table-card 一个符合系统样式的 class,同时自动撑满剩余高度 -->
|
||||
<!-- 更多 useTable 使用示例请移步至 功能示例 下面的高级表格示例或者查看官方文档 -->
|
||||
<!-- useTable 文档:https://www.artd.pro/docs/zh/guide/hooks/use-table.html -->
|
||||
<template>
|
||||
<div class="user-page art-full-height">
|
||||
<!-- 搜索栏 -->
|
||||
<UserSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></UserSearch>
|
||||
|
||||
<ElCard class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增用户</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
></ArtTable>
|
||||
|
||||
<!-- 用户弹窗 -->
|
||||
<UserDialog
|
||||
v-model:visible="dialogVisible"
|
||||
:type="dialogType"
|
||||
:user-data="currentUserData"
|
||||
@submit="handleDialogSubmit"
|
||||
/>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtButtonTable from "@/components/Core/forms/art-button-table/index.vue";
|
||||
import { ACCOUNT_TABLE_DATA } from "@/mock/temp/formData";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { fetchGetUserList } from "@/api/system-manage";
|
||||
import UserSearch from "./modules/user-search.vue";
|
||||
import UserDialog from "./modules/user-dialog.vue";
|
||||
import { ElTag, ElMessageBox, ElImage } from "element-plus";
|
||||
import { DialogType } from "@/types";
|
||||
|
||||
defineOptions({ name: "User" });
|
||||
|
||||
type UserListItem = UserListItem;
|
||||
|
||||
// 弹窗相关
|
||||
const dialogType = ref<DialogType>("add");
|
||||
const dialogVisible = ref(false);
|
||||
const currentUserData = ref<Partial<UserListItem>>({});
|
||||
|
||||
// 选中行
|
||||
const selectedRows = ref<UserListItem[]>([]);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
userName: undefined,
|
||||
userGender: undefined,
|
||||
userPhone: undefined,
|
||||
userEmail: undefined,
|
||||
status: "1",
|
||||
});
|
||||
|
||||
// 用户状态配置
|
||||
const USER_STATUS_CONFIG = {
|
||||
"1": { type: "success" as const, text: "在线" },
|
||||
"2": { type: "info" as const, text: "离线" },
|
||||
"3": { type: "warning" as const, text: "异常" },
|
||||
"4": { type: "danger" as const, text: "注销" },
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 获取用户状态配置
|
||||
*/
|
||||
const getUserStatusConfig = (status: string) => {
|
||||
return (
|
||||
USER_STATUS_CONFIG[status as keyof typeof USER_STATUS_CONFIG] || {
|
||||
type: "info" as const,
|
||||
text: "未知",
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
replaceSearchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData,
|
||||
} = useTable({
|
||||
// 核心配置
|
||||
core: {
|
||||
apiFn: fetchGetUserList,
|
||||
apiParams: {
|
||||
current: 1,
|
||||
size: 20,
|
||||
...searchForm.value,
|
||||
},
|
||||
// 自定义分页字段映射,未设置时将使用全局配置 tableConfig.ts 中的 paginationKey
|
||||
// paginationKey: {
|
||||
// current: 'pageNum',
|
||||
// size: 'pageSize'
|
||||
// },
|
||||
columnsFactory: () => [
|
||||
{ type: "selection" }, // 勾选列
|
||||
{ type: "index", width: 60, label: "序号" }, // 序号
|
||||
{
|
||||
prop: "userInfo",
|
||||
label: "用户名",
|
||||
width: 280,
|
||||
// visible: false, // 默认是否显示列
|
||||
formatter: (row) => {
|
||||
return h("div", { class: "user flex-c" }, [
|
||||
h(ElImage, {
|
||||
class: "size-9.5 rounded-md",
|
||||
src: row.avatar,
|
||||
previewSrcList: [row.avatar],
|
||||
// 图片预览是否插入至 body 元素上,用于解决表格内部图片预览样式异常
|
||||
previewTeleported: true,
|
||||
}),
|
||||
h("div", { class: "ml-2" }, [
|
||||
h("p", { class: "user-name" }, row.userName),
|
||||
h("p", { class: "email" }, row.userEmail),
|
||||
]),
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "userGender",
|
||||
label: "性别",
|
||||
sortable: true,
|
||||
formatter: (row) => row.userGender,
|
||||
},
|
||||
{ prop: "userPhone", label: "手机号" },
|
||||
{
|
||||
prop: "status",
|
||||
label: "状态",
|
||||
formatter: (row) => {
|
||||
const statusConfig = getUserStatusConfig(row.status);
|
||||
return h(ElTag, { type: statusConfig.type }, () => statusConfig.text);
|
||||
},
|
||||
},
|
||||
{
|
||||
prop: "createTime",
|
||||
label: "创建日期",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
prop: "operation",
|
||||
label: "操作",
|
||||
width: 120,
|
||||
fixed: "right", // 固定列
|
||||
formatter: (row) =>
|
||||
h("div", [
|
||||
h(ArtButtonTable, {
|
||||
type: "edit",
|
||||
onClick: () => showDialog("edit", row),
|
||||
}),
|
||||
h(ArtButtonTable, {
|
||||
type: "delete",
|
||||
onClick: () => deleteUser(row),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 数据处理
|
||||
transform: {
|
||||
// 数据转换器 - 替换头像
|
||||
dataTransformer: (records) => {
|
||||
// 类型守卫检查
|
||||
if (!Array.isArray(records)) {
|
||||
console.warn("数据转换器: 期望数组类型,实际收到:", typeof records);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 使用本地头像替换接口返回的头像
|
||||
return records.map((item, index: number) => {
|
||||
return {
|
||||
...item,
|
||||
avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 搜索处理
|
||||
* @param params 参数
|
||||
*/
|
||||
const handleSearch = (params: UserSearchParams) => {
|
||||
replaceSearchParams(params);
|
||||
getData();
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示用户弹窗
|
||||
*/
|
||||
const showDialog = (type: DialogType, row?: UserListItem): void => {
|
||||
console.log("打开弹窗:", { type, row });
|
||||
dialogType.value = type;
|
||||
currentUserData.value = row || {};
|
||||
nextTick(() => {
|
||||
dialogVisible.value = true;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
const deleteUser = (row: UserListItem): void => {
|
||||
console.log("删除用户:", row);
|
||||
ElMessageBox.confirm(`确定要注销该用户吗?`, "注销用户", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "error",
|
||||
}).then(() => {
|
||||
ElMessage.success("注销成功");
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理弹窗提交事件
|
||||
*/
|
||||
const handleDialogSubmit = async () => {
|
||||
try {
|
||||
dialogVisible.value = false;
|
||||
currentUserData.value = {};
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理表格行选择变化
|
||||
*/
|
||||
const handleSelectionChange = (selection: UserListItem[]): void => {
|
||||
selectedRows.value = selection;
|
||||
console.log("选中行数据:", selectedRows.value);
|
||||
};
|
||||
</script>
|
||||
@@ -1,143 +0,0 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '添加用户' : '编辑用户'"
|
||||
width="30%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
|
||||
<ElFormItem label="用户名" prop="username">
|
||||
<ElInput v-model="formData.username" placeholder="请输入用户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="手机号" prop="phone">
|
||||
<ElInput v-model="formData.phone" placeholder="请输入手机号" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="gender">
|
||||
<ElSelect v-model="formData.gender">
|
||||
<ElOption label="男" value="男" />
|
||||
<ElOption label="女" value="女" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="角色" prop="role">
|
||||
<ElSelect v-model="formData.role" multiple>
|
||||
<ElOption
|
||||
v-for="role in roleList"
|
||||
:key="role.roleCode"
|
||||
:value="role.roleCode"
|
||||
:label="role.roleName"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ROLE_LIST_DATA } from "@/mock/temp/formData";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
type: string;
|
||||
userData?: Partial<UserListItem>;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "submit"): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 角色列表数据
|
||||
const roleList = ref(ROLE_LIST_DATA);
|
||||
|
||||
// 对话框显示控制
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit("update:visible", value),
|
||||
});
|
||||
|
||||
const dialogType = computed(() => props.type);
|
||||
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
username: "",
|
||||
phone: "",
|
||||
gender: "男",
|
||||
role: [] as string[],
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: "请输入用户名", trigger: "blur" },
|
||||
{ min: 2, max: 20, message: "长度在 2 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: "请输入手机号", trigger: "blur" },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号格式", trigger: "blur" },
|
||||
],
|
||||
gender: [{ required: true, message: "请选择性别", trigger: "blur" }],
|
||||
role: [{ required: true, message: "请选择角色", trigger: "blur" }],
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
* 根据对话框类型(新增/编辑)填充表单
|
||||
*/
|
||||
const initFormData = () => {
|
||||
const isEdit = props.type === "edit" && props.userData;
|
||||
const row = props.userData;
|
||||
|
||||
Object.assign(formData, {
|
||||
username: isEdit && row ? row.userName || "" : "",
|
||||
phone: isEdit && row ? row.userPhone || "" : "",
|
||||
gender: isEdit && row ? row.userGender || "男" : "男",
|
||||
role: isEdit && row ? (Array.isArray(row.userRoles) ? row.userRoles : []) : [],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听对话框状态变化
|
||||
* 当对话框打开时初始化表单数据并清除验证状态
|
||||
*/
|
||||
watch(
|
||||
() => [props.visible, props.type, props.userData],
|
||||
([visible]) => {
|
||||
if (visible) {
|
||||
initFormData();
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
* 验证通过后触发提交事件
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
ElMessage.success(dialogType.value === "add" ? "添加成功" : "更新成功");
|
||||
dialogVisible.value = false;
|
||||
emit("submit");
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -1,111 +0,0 @@
|
||||
<template>
|
||||
<ArtSearchBar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:items="formItems"
|
||||
:rules="rules"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: UserSearchParams;
|
||||
}
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: UserSearchParams): void;
|
||||
(e: "search", params: UserSearchParams): void;
|
||||
(e: "reset"): void;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref();
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit("update:modelValue", val),
|
||||
});
|
||||
|
||||
// 校验规则
|
||||
const rules = {
|
||||
// userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
|
||||
};
|
||||
|
||||
// 动态 options
|
||||
const statusOptions = ref<{ label: string; value: string; disabled?: boolean }[]>([]);
|
||||
|
||||
// 模拟接口返回状态数据
|
||||
function fetchStatusOptions(): Promise<typeof statusOptions.value> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{ label: "在线", value: "1" },
|
||||
{ label: "离线", value: "2" },
|
||||
{ label: "异常", value: "3" },
|
||||
{ label: "注销", value: "4" },
|
||||
]);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
statusOptions.value = await fetchStatusOptions();
|
||||
});
|
||||
|
||||
// 表单配置
|
||||
const formItems = computed(() => [
|
||||
{
|
||||
label: "用户名",
|
||||
key: "userName",
|
||||
type: "input",
|
||||
placeholder: "请输入用户名",
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
label: "手机号",
|
||||
key: "userPhone",
|
||||
type: "input",
|
||||
props: { placeholder: "请输入手机号", maxlength: "11" },
|
||||
},
|
||||
{
|
||||
label: "邮箱",
|
||||
key: "userEmail",
|
||||
type: "input",
|
||||
props: { placeholder: "请输入邮箱" },
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
key: "status",
|
||||
type: "select",
|
||||
props: {
|
||||
placeholder: "请选择状态",
|
||||
options: statusOptions.value,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "性别",
|
||||
key: "userGender",
|
||||
type: "radiogroup",
|
||||
props: {
|
||||
options: [
|
||||
{ label: "男", value: "1" },
|
||||
{ label: "女", value: "2" },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 事件
|
||||
function handleReset() {
|
||||
console.log("重置表单");
|
||||
emit("reset");
|
||||
}
|
||||
|
||||
async function handleSearch(params: UserSearchParams) {
|
||||
await searchBarRef.value.validate();
|
||||
emit("search", params);
|
||||
console.log("表单数据", params);
|
||||
}
|
||||
</script>
|
||||
@@ -21,7 +21,19 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-grow min-w-0">
|
||||
<UserSearch v-model="defaultFilter" />
|
||||
<ArtSearchBar
|
||||
ref="searchBarRef"
|
||||
v-model="defaultFilter"
|
||||
:items="treeSearchItems"
|
||||
:rules="searchBarRules"
|
||||
:is-expand="false"
|
||||
:show-expand="true"
|
||||
:show-reset="true"
|
||||
:show-search="true"
|
||||
:default-expanded="false"
|
||||
@search="onTreeSearch"
|
||||
@reset="onTreeSearchReset"
|
||||
/>
|
||||
|
||||
<ElCard class="flex flex-col flex-1 min-h-0 art-table-card">
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
@@ -53,13 +65,52 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useTable } from "@/hooks/core/useTable";
|
||||
import { fetchGetUserList } from "@/api/system-manage";
|
||||
import UserSearch from "@/views/system/user/modules/user-search.vue";
|
||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||
|
||||
defineOptions({ name: "TreeTable" });
|
||||
|
||||
const showButtons = ref(false);
|
||||
const searchBarRef = ref<InstanceType<typeof ArtSearchBar> | null>(null);
|
||||
const searchBarRules: Record<string, unknown> = {};
|
||||
|
||||
const statusOptions = ref([
|
||||
{ label: "在线", value: "1" },
|
||||
{ label: "离线", value: "2" },
|
||||
{ label: "异常", value: "3" },
|
||||
{ label: "注销", value: "4" },
|
||||
]);
|
||||
|
||||
const treeSearchItems = computed<SearchFormItem[]>(() => [
|
||||
{ label: "用户名", key: "userName", type: "input", placeholder: "请输入用户名", clearable: true },
|
||||
{
|
||||
label: "手机号",
|
||||
key: "userPhone",
|
||||
type: "input",
|
||||
props: { placeholder: "请输入手机号", maxlength: "11" },
|
||||
},
|
||||
{ label: "邮箱", key: "userEmail", type: "input", props: { placeholder: "请输入邮箱" } },
|
||||
{
|
||||
label: "状态",
|
||||
key: "status",
|
||||
type: "select",
|
||||
props: { placeholder: "请选择状态", options: statusOptions.value },
|
||||
},
|
||||
{
|
||||
label: "性别",
|
||||
key: "userGender",
|
||||
type: "radiogroup",
|
||||
props: {
|
||||
options: [
|
||||
{ label: "男", value: "1" },
|
||||
{ label: "女", value: "2" },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 树形数据 - 组织架构示例
|
||||
const treeData = ref([
|
||||
@@ -229,6 +280,15 @@ const {
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
function onTreeSearch() {
|
||||
void searchBarRef.value?.validate?.();
|
||||
void refreshData();
|
||||
}
|
||||
|
||||
function onTreeSearchReset() {
|
||||
void refreshData();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user