mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-23 21:18:09 +00:00
feat: 移除系统管理路由并优化路由结构
- 删除不再使用的系统管理路由模块 - 更新路由配置,确保仅保留必要的模块 - 添加Element Plus图标全局注册功能 - 优化分页组件,增加更多属性支持 - 重构表格操作列渲染逻辑,提升代码可读性和复用性 - 更新多个组件以支持新的操作列渲染方式
This commit is contained in:
@@ -10,7 +10,7 @@ VITE_PORT = 5180
|
|||||||
VITE_BASE_URL = /
|
VITE_BASE_URL = /
|
||||||
|
|
||||||
# 权限模式【 frontend 仅前端路由 / backend 仅后端菜单 / mixed 后端菜单+前端路由模块合并 】
|
# 权限模式【 frontend 仅前端路由 / backend 仅后端菜单 / mixed 后端菜单+前端路由模块合并 】
|
||||||
VITE_ACCESS_MODE = mixed
|
VITE_ACCESS_MODE = frontend
|
||||||
|
|
||||||
# 跨域请求时是否携带 Cookie(开启前需确保后端支持)
|
# 跨域请求时是否携带 Cookie(开启前需确保后端支持)
|
||||||
VITE_WITH_CREDENTIALS = false
|
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.md",
|
||||||
"CHANGELOG.zh-CN.md",
|
"CHANGELOG.zh-CN.md",
|
||||||
"src/views/safeguard",
|
"src/views/safeguard",
|
||||||
"src/views/system/nested",
|
|
||||||
"src/views/widgets",
|
"src/views/widgets",
|
||||||
"src/views/dashboard/analysis",
|
"src/views/dashboard/analysis",
|
||||||
"src/views/dashboard/ecommerce",
|
"src/views/dashboard/ecommerce",
|
||||||
@@ -254,73 +253,14 @@ async function cleanRouteModules() {
|
|||||||
// 忽略
|
// 忽略
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重写 system.ts - 移除 nested 嵌套菜单
|
// 重写 index.ts:widgets 已删则仅保留 exception
|
||||||
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 - 只导入保留的模块
|
|
||||||
const indexContent = `import { AppRouteRecord } from '@/types/router'
|
const indexContent = `import { AppRouteRecord } from '@/types/router'
|
||||||
import { systemRoutes } from './system'
|
|
||||||
import { resultRoutes } from './result'
|
|
||||||
import { exceptionRoutes } from './exception'
|
import { exceptionRoutes } from './exception'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出所有模块化路由
|
* 导出所有模块化路由(业务菜单由后端动态下发)
|
||||||
*/
|
*/
|
||||||
export const routeModules: AppRouteRecord[] = [
|
export const routeModules: AppRouteRecord[] = [exceptionRoutes]
|
||||||
systemRoutes,
|
|
||||||
resultRoutes,
|
|
||||||
exceptionRoutes
|
|
||||||
]
|
|
||||||
`;
|
`;
|
||||||
await fs.writeFile(path.join(modulesPath, "index.ts"), indexContent, "utf-8");
|
await fs.writeFile(path.join(modulesPath, "index.ts"), indexContent, "utf-8");
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,11 @@
|
|||||||
v-model:current-page="currentPage"
|
v-model:current-page="currentPage"
|
||||||
v-model:page-size="pageSize"
|
v-model:page-size="pageSize"
|
||||||
:background="background"
|
:background="background"
|
||||||
|
:disabled="disabled"
|
||||||
:layout="layout"
|
:layout="layout"
|
||||||
:page-sizes="pageSizes"
|
:page-sizes="pageSizes"
|
||||||
|
:pager-count="pagerCount"
|
||||||
|
:size="size"
|
||||||
:total="total"
|
:total="total"
|
||||||
@size-change="handleSizeChange"
|
@size-change="handleSizeChange"
|
||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
@@ -17,6 +20,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { watch, type PropType } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
total: {
|
total: {
|
||||||
type: Number as PropType<number>,
|
type: Number as PropType<number>,
|
||||||
@@ -25,7 +30,7 @@ const props = defineProps({
|
|||||||
pageSizes: {
|
pageSizes: {
|
||||||
type: Array as PropType<number[]>,
|
type: Array as PropType<number[]>,
|
||||||
default() {
|
default() {
|
||||||
return [10, 20, 30, 50];
|
return [10, 20, 30, 50, 100];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
@@ -36,6 +41,20 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
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: {
|
autoScroll: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
<!-- 渲染展开行 -->
|
<!-- 渲染展开行 -->
|
||||||
<ElTableColumn v-else-if="col.type === 'expand'" v-bind="cleanColumnProps(col)">
|
<ElTableColumn v-else-if="col.type === 'expand'" v-bind="cleanColumnProps(col)">
|
||||||
<template #default="{ row }">
|
<template #default="{ row: expandRow }">
|
||||||
<component :is="col.formatter ? col.formatter(row) : null" />
|
<component :is="col.formatter ? col.formatter(expandRow) : null" />
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
|
|
||||||
<!-- 渲染普通列 -->
|
<!-- 渲染普通列:default 插槽须紧凑书写,避免空白文本抢占 formatter(见 renderColumnFormatter 注释) -->
|
||||||
<ElTableColumn v-else v-bind="cleanColumnProps(col)">
|
<ElTableColumn v-else v-bind="cleanBodyColumnProps(col)">
|
||||||
<template v-if="col.useHeaderSlot && col.prop" #header="headerScope">
|
<template v-if="col.useHeaderSlot && col.prop" #header="headerScope">
|
||||||
<slot
|
<slot
|
||||||
:name="col.headerSlotName || `${col.prop}-header`"
|
:name="col.headerSlotName || `${col.prop}-header`"
|
||||||
@@ -30,17 +30,9 @@
|
|||||||
{{ col.label }}
|
{{ col.label }}
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="col.useSlot && col.prop" #default="slotScope">
|
<!-- 整段单行:插槽内兄弟节点之间的空白文本会让 EP 误判单元格内容 -->
|
||||||
<slot
|
<!-- eslint-disable-next-line vue/max-attributes-per-line, prettier/prettier -->
|
||||||
v-if="shouldRenderSlotScope(slotScope)"
|
<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>
|
||||||
:name="col.slotName || col.prop"
|
|
||||||
v-bind="{
|
|
||||||
...slotScope,
|
|
||||||
prop: col.prop,
|
|
||||||
value: col.prop ? slotScope.row[col.prop] : undefined,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -58,21 +50,37 @@
|
|||||||
:class="mergedPaginationOptions?.align"
|
:class="mergedPaginationOptions?.align"
|
||||||
ref="paginationRef"
|
ref="paginationRef"
|
||||||
>
|
>
|
||||||
<ElPagination
|
<Pagination
|
||||||
v-bind="mergedPaginationOptions"
|
v-if="pagination"
|
||||||
:total="pagination?.total"
|
:page="pagination.current"
|
||||||
:disabled="loading"
|
:limit="pagination.size"
|
||||||
:page-size="pagination?.size"
|
:total="pagination.total"
|
||||||
:current-page="pagination?.current"
|
:page-sizes="mergedPaginationOptions.pageSizes"
|
||||||
@size-change="handleSizeChange"
|
:layout="mergedPaginationOptions.layout"
|
||||||
@current-change="handleCurrentChange"
|
:background="mergedPaginationOptions.background ?? true"
|
||||||
|
:disabled="!!loading"
|
||||||
|
:hidden="paginationHidden"
|
||||||
|
:pager-count="mergedPaginationOptions.pagerCount"
|
||||||
|
:size="mergedPaginationOptions.size"
|
||||||
|
@pagination="handlePaginationEvent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 type { ElTable, TableProps } from "element-plus";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { ColumnOption } from "@/types";
|
import { ColumnOption } from "@/types";
|
||||||
@@ -80,6 +88,7 @@ import { useTableStore } from "@/store/modules/table.store";
|
|||||||
import { useCommon } from "@/hooks/core/useCommon";
|
import { useCommon } from "@/hooks/core/useCommon";
|
||||||
import { useTableHeight } from "@/hooks/core/useTableHeight";
|
import { useTableHeight } from "@/hooks/core/useTableHeight";
|
||||||
import { useResizeObserver, useWindowSize } from "@vueuse/core";
|
import { useResizeObserver, useWindowSize } from "@vueuse/core";
|
||||||
|
import Pagination from "@/components/Pagination/index.vue";
|
||||||
|
|
||||||
defineOptions({ name: "ArtTable" });
|
defineOptions({ name: "ArtTable" });
|
||||||
|
|
||||||
@@ -183,6 +192,17 @@ const mergedPaginationOptions = computed(() => ({
|
|||||||
...props.paginationOptions,
|
...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)
|
// 边框 (优先级:props > store)
|
||||||
const border = computed(() => props.border ?? isBorder.value);
|
const border = computed(() => props.border ?? isBorder.value);
|
||||||
// 斑马纹
|
// 斑马纹
|
||||||
@@ -269,6 +289,11 @@ const mergedTableProps = computed(() => ({
|
|||||||
: undefined,
|
: 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);
|
const showPagination = computed(() => props.pagination && !isEmpty.value);
|
||||||
|
|
||||||
@@ -278,6 +303,36 @@ const shouldRenderSlotScope = (slotScope: { $index?: number }) => {
|
|||||||
return slotScope.$index === undefined || slotScope.$index >= 0;
|
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 错误解释
|
// 清理列属性,移除插槽相关的自定义属性,确保它们不会被 ElTableColumn 错误解释
|
||||||
const cleanColumnProps = (col: ColumnOption) => {
|
const cleanColumnProps = (col: ColumnOption) => {
|
||||||
const columnProps = { ...col };
|
const columnProps = { ...col };
|
||||||
@@ -289,15 +344,11 @@ const cleanColumnProps = (col: ColumnOption) => {
|
|||||||
return columnProps;
|
return columnProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分页大小变化
|
/** 普通列:单元格已由插槽内 TableFormatterOutlet 渲染,勿再把 formatter 传给 ElTableColumn,避免与 EP 内置 renderCell 混用 */
|
||||||
const handleSizeChange = (val: number) => {
|
const cleanBodyColumnProps = (col: ColumnOption) => {
|
||||||
emit("pagination:size-change", val);
|
const columnProps = cleanColumnProps(col);
|
||||||
};
|
delete columnProps.formatter;
|
||||||
|
return columnProps;
|
||||||
// 分页当前页变化
|
|
||||||
const handleCurrentChange = (val: number) => {
|
|
||||||
emit("pagination:current-change", val);
|
|
||||||
scrollToTop(); // 页码改变后滚动到表格顶部
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { scrollToTop: scrollPageToTop } = useCommon();
|
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) => {
|
const getGlobalIndex = (index: number) => {
|
||||||
if (!props.pagination) return index + 1;
|
if (!props.pagination) return index + 1;
|
||||||
@@ -317,11 +382,6 @@ const getGlobalIndex = (index: number) => {
|
|||||||
return (current - 1) * size + index + 1;
|
return (current - 1) * size + index + 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "pagination:size-change", val: number): void;
|
|
||||||
(e: "pagination:current-change", val: number): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// 查找并绑定表格头部元素 - 使用 VueUse 优化
|
// 查找并绑定表格头部元素 - 使用 VueUse 优化
|
||||||
const findTableHeader = () => {
|
const findTableHeader = () => {
|
||||||
if (!props.showTableHeader) {
|
if (!props.showTableHeader) {
|
||||||
|
|||||||
@@ -19,20 +19,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from "vue-router";
|
|
||||||
import { useSettingsStore } from "@/store/modules/setting.store";
|
import { useSettingsStore } from "@/store/modules/setting.store";
|
||||||
import { useCeremony } from "@/hooks/core/useCeremony";
|
import { useCeremony } from "@/hooks/core/useCeremony";
|
||||||
import { WEB_LINKS } from "@/utils/constants";
|
import { WEB_LINKS } from "@/utils/constants";
|
||||||
|
|
||||||
defineOptions({ name: "ArtFestivalTextScroll" });
|
defineOptions({ name: "ArtFestivalTextScroll" });
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const settingStore = useSettingsStore();
|
const settingStore = useSettingsStore();
|
||||||
const { showFestivalText } = storeToRefs(settingStore);
|
const { showFestivalText } = storeToRefs(settingStore);
|
||||||
const { currentFestivalData, closeFestivalScroll } = useCeremony();
|
const { currentFestivalData, closeFestivalScroll } = useCeremony();
|
||||||
|
|
||||||
const isFullPageLayout = computed(() => route.matched.some((r) => r.meta?.isFullPage));
|
|
||||||
|
|
||||||
function versionLabel(): string {
|
function versionLabel(): string {
|
||||||
const v = String(import.meta.env.VITE_VERSION ?? "").trim();
|
const v = String(import.meta.env.VITE_VERSION ?? "").trim();
|
||||||
if (!v) return "";
|
if (!v) return "";
|
||||||
@@ -49,7 +45,6 @@ const showFestivalStrip = computed(
|
|||||||
() =>
|
() =>
|
||||||
showFestivalText.value &&
|
showFestivalText.value &&
|
||||||
!!currentFestivalData.value?.scrollText &&
|
!!currentFestivalData.value?.scrollText &&
|
||||||
currentFestivalData.value.scrollText !== "" &&
|
currentFestivalData.value.scrollText !== ""
|
||||||
!isFullPageLayout.value
|
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const SETTING_DEFAULT_CONFIG = {
|
|||||||
/** 是否显示语言切换 */
|
/** 是否显示语言切换 */
|
||||||
showLanguage: true,
|
showLanguage: true,
|
||||||
/** 是否显示进度条 */
|
/** 是否显示进度条 */
|
||||||
showNprogress: false,
|
showNprogress: true,
|
||||||
/** 是否显示设置引导 */
|
/** 是否显示设置引导 */
|
||||||
showSettingGuide: true,
|
showSettingGuide: true,
|
||||||
/** 是否显示节日文本 */
|
/** 是否显示节日文本 */
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ import { storeToRefs } from "pinia";
|
|||||||
import { useUserStore } from "@/store/modules/user.store";
|
import { useUserStore } from "@/store/modules/user.store";
|
||||||
import { useAppMode } from "@/hooks/core/useAppMode";
|
import { useAppMode } from "@/hooks/core/useAppMode";
|
||||||
import type { AppRouteRecord } from "@/types/router";
|
import type { AppRouteRecord } from "@/types/router";
|
||||||
|
import { ROLE_ROOT } from "@/constants";
|
||||||
|
|
||||||
type AuthItem = NonNullable<AppRouteRecord["meta"]["authList"]>[number];
|
type AuthItem = NonNullable<AppRouteRecord["meta"]["authList"]>[number];
|
||||||
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const userStore = useUserStore();
|
||||||
const { isFrontendMode } = useAppMode();
|
const { isFrontendMode } = useAppMode();
|
||||||
const { info } = storeToRefs(userStore);
|
const { info } = storeToRefs(userStore);
|
||||||
|
|
||||||
@@ -54,17 +54,28 @@ export const useAuth = () => {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否拥有某权限标识(前后端模式通用)
|
* 检查是否拥有某权限标识(与 v-hasPerm / 表格内 ArtButtonMore 全码一致)
|
||||||
* @param auth 权限标识
|
* @param auth 权限标识(多为后端菜单 permission 全码,如 module_system:role:update)
|
||||||
* @returns 是否有权限
|
|
||||||
*/
|
*/
|
||||||
const hasAuth = (auth: string): boolean => {
|
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) {
|
if (isFrontendMode.value) {
|
||||||
return frontendAuthList.includes(auth);
|
return frontendAuthList.includes(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后端模式
|
// 后端模式:当前路由 meta 短标识(与 v-auth 一致)
|
||||||
return backendAuthList.some((item) => item?.authMark === auth);
|
return backendAuthList.some((item) => item?.authMark === auth);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
<!-- 布局内容 -->
|
<!-- 布局内容 -->
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div id="app-scroll-main" class="layout-content" :style="containerStyle">
|
||||||
id="app-scroll-main"
|
|
||||||
class="layout-content"
|
|
||||||
:class="{ 'overflow-auto': isFullPage }"
|
|
||||||
:style="containerStyle"
|
|
||||||
>
|
|
||||||
<div id="app-content-header">
|
<div id="app-content-header">
|
||||||
<!-- 节日滚动 -->
|
<!-- 节日滚动 -->
|
||||||
<ArtFestivalTextScroll v-if="!isFullPage" />
|
<ArtFestivalTextScroll />
|
||||||
|
|
||||||
<!-- 路由信息调试 -->
|
<!-- 路由信息调试 -->
|
||||||
<div
|
<div
|
||||||
@@ -20,40 +15,28 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RouterView v-if="isRefresh" v-slot="{ Component, route: router }" :style="contentStyle">
|
<RouterView v-if="isRefresh" v-slot="{ Component, route: router }" :style="contentStyle">
|
||||||
<!-- 缓存路由动画 -->
|
<Transition :name="actualTransition" mode="out-in" appear>
|
||||||
<Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
|
<div class="route-view-shell flex min-h-0 flex-1 flex-col">
|
||||||
<KeepAlive :max="10" :exclude="keepAliveExclude">
|
<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
|
<component
|
||||||
class="art-page-view"
|
v-if="router.meta.keepAlive !== true"
|
||||||
|
class="art-page-view min-h-0 flex-1"
|
||||||
:is="Component"
|
:is="Component"
|
||||||
:key="router.path"
|
:key="router.fullPath"
|
||||||
v-if="router.meta.keepAlive"
|
|
||||||
/>
|
/>
|
||||||
</KeepAlive>
|
</div>
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<!-- 非缓存路由动画 -->
|
|
||||||
<Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
|
|
||||||
<component
|
|
||||||
class="art-page-view"
|
|
||||||
:is="Component"
|
|
||||||
:key="router.path"
|
|
||||||
v-if="!router.meta.keepAlive"
|
|
||||||
/>
|
|
||||||
</Transition>
|
</Transition>
|
||||||
</RouterView>
|
</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 置空 -->
|
<!-- 返回顶部:宽屏 #app-scroll-main;窄屏文档滚动 target 置空 -->
|
||||||
<ElBacktop
|
<ElBacktop
|
||||||
v-if="!isFullPage"
|
|
||||||
:key="backtopTargetKey"
|
:key="backtopTargetKey"
|
||||||
:target="backtopScrollTarget"
|
:target="backtopScrollTarget"
|
||||||
:right="28"
|
:right="28"
|
||||||
@@ -86,65 +69,27 @@ const { keepAliveExclude } = storeToRefs(useWorktabStore());
|
|||||||
|
|
||||||
const isRefresh = shallowRef(true);
|
const isRefresh = shallowRef(true);
|
||||||
const isOpenRouteInfo = import.meta.env.VITE_OPEN_ROUTE_INFO;
|
const isOpenRouteInfo = import.meta.env.VITE_OPEN_ROUTE_INFO;
|
||||||
const showTransitionMask = ref(false);
|
|
||||||
|
|
||||||
// 标记是否是首次加载(浏览器刷新)
|
// 标记是否是首次加载(浏览器刷新)
|
||||||
const isFirstLoad = ref(true);
|
const isFirstLoad = ref(true);
|
||||||
|
|
||||||
// 检查当前路由是否需要使用无基础布局模式
|
|
||||||
const isFullPage = computed(() => route.matched.some((r) => r.meta?.isFullPage));
|
|
||||||
const prevIsFullPage = ref(isFullPage.value);
|
|
||||||
|
|
||||||
// 切换动画名称:首次加载、从全屏返回时不使用动画
|
|
||||||
const actualTransition = computed(() => {
|
const actualTransition = computed(() => {
|
||||||
if (isFirstLoad.value) return "";
|
if (isFirstLoad.value) return "";
|
||||||
if (prevIsFullPage.value && !isFullPage.value) return "";
|
|
||||||
return pageTransition.value;
|
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(
|
const containerStyle = computed(
|
||||||
(): CSSProperties =>
|
(): CSSProperties => ({
|
||||||
isFullPage.value
|
maxWidth: containerWidth.value,
|
||||||
? {
|
flex: "1",
|
||||||
position: "fixed",
|
minHeight: "0",
|
||||||
top: 0,
|
display: "flex",
|
||||||
left: 0,
|
flexDirection: "column",
|
||||||
width: "100%",
|
})
|
||||||
height: "100vh",
|
|
||||||
zIndex: 2500,
|
|
||||||
background: "var(--default-bg-color)",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
maxWidth: containerWidth.value,
|
|
||||||
flex: "1",
|
|
||||||
minHeight: "0",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 常规布局下由 `.layout-content` 承担纵向滚动,路由视图填满剩余高度 */
|
/** 常规布局下由 `.layout-content` 承担纵向滚动,路由视图填满剩余高度 */
|
||||||
const contentStyle = computed(
|
const contentStyle = computed((): CSSProperties => ({ flex: "1", minHeight: "0" }));
|
||||||
(): CSSProperties =>
|
|
||||||
isFullPage.value
|
|
||||||
? { flex: "1", minHeight: "0", display: "flex", flexDirection: "column" }
|
|
||||||
: { flex: "1", minHeight: "0" }
|
|
||||||
);
|
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
isRefresh.value = false;
|
isRefresh.value = false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import "@styles/index.scss"; // 样式
|
|||||||
import "@utils/sys/console.ts"; // 控制台输出内容
|
import "@utils/sys/console.ts"; // 控制台输出内容
|
||||||
import { initGlobDirectives } from "./directives";
|
import { initGlobDirectives } from "./directives";
|
||||||
import { setupErrorHandle } from "./utils/sys/error-handle";
|
import { setupErrorHandle } from "./utils/sys/error-handle";
|
||||||
|
import { initElIcons } from "@/plugins/icons";
|
||||||
|
|
||||||
// 暗黑主题样式
|
// 暗黑主题样式
|
||||||
import "element-plus/theme-chalk/dark/css-vars.css";
|
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 });
|
document.addEventListener("touchstart", function () {}, { passive: false });
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
/** Element Plus 图标(@element-plus/icons-vue)全局注册,模板中可直接 <Cpu />、<QuestionFilled /> 等 */
|
||||||
|
initElIcons(app);
|
||||||
initStore(app);
|
initStore(app);
|
||||||
initRouter(app);
|
initRouter(app);
|
||||||
initGlobDirectives(app);
|
initGlobDirectives(app);
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function setupBeforeEachGuard(router: Router): void {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[RouteGuard] 路由守卫处理失败:", error);
|
console.error("[RouteGuard] 路由守卫处理失败:", error);
|
||||||
closeLoading();
|
closeLoading();
|
||||||
next({ name: "Exception500" });
|
next({ name: "500" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -168,7 +168,7 @@ async function handleRouteGuard(
|
|||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
// 未匹配到路由,跳转到 500 页面
|
// 未匹配到路由,跳转到 500 页面
|
||||||
next({ name: "Exception500", replace: true });
|
next({ name: "500", replace: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ async function handleRouteGuard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. 未匹配到路由,跳转到 404
|
// 6. 未匹配到路由,跳转到 404
|
||||||
next({ name: "Exception404" });
|
next({ name: "404" });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -262,9 +262,8 @@ function resolveStaticChildFullPath(parentFullPath: string, segment: string): st
|
|||||||
function isStaticRoute(path: string): boolean {
|
function isStaticRoute(path: string): boolean {
|
||||||
const checkRoute = (routes: any[], targetPath: string, parentFullPath = ""): boolean => {
|
const checkRoute = (routes: any[], targetPath: string, parentFullPath = ""): boolean => {
|
||||||
return routes.some((route) => {
|
return routes.some((route) => {
|
||||||
// 404 catch-all 路由不应视为可匿名访问的静态页,
|
// 通配 404(pathMatch)不应视为免登录静态页;静态表里可能与 `/404` 同名,按 path 区分。
|
||||||
// 否则未登录时手动输入任意地址会直接落到 404,无法跳转登录页。
|
if (route.path === "/:pathMatch(.*)*") {
|
||||||
if (route.name === "Exception404") {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +415,7 @@ async function handleDynamicRoutes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到 500 页面,使用 replace 避免产生历史记录
|
// 跳转到 500 页面,使用 replace 避免产生历史记录
|
||||||
next({ name: "Exception500", replace: true });
|
next({ name: "500", replace: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { AppRouteRecord } from "@/types/router";
|
import { AppRouteRecord } from "@/types/router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「异常」菜单分组(挂载在 /exception/*,走 Layout)。
|
||||||
|
* 与 `staticRoutes` 中独立页区分:
|
||||||
|
* - 编程式跳转、守卫:`name` 为 `"403" | "404" | "500"`(对应 `/403`、`/404`、`/500`)
|
||||||
|
* - 本模块子路由:`Exception403` / `Exception404` / `Exception500`(对应 `/exception/403` 等),勿与前者混用
|
||||||
|
*/
|
||||||
export const exceptionRoutes: AppRouteRecord = {
|
export const exceptionRoutes: AppRouteRecord = {
|
||||||
path: "/exception",
|
path: "/exception",
|
||||||
name: "Exception",
|
name: "Exception",
|
||||||
@@ -17,7 +23,6 @@ export const exceptionRoutes: AppRouteRecord = {
|
|||||||
title: "menus.exception.forbidden",
|
title: "menus.exception.forbidden",
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isHideTab: true,
|
isHideTab: true,
|
||||||
isFullPage: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -28,7 +33,6 @@ export const exceptionRoutes: AppRouteRecord = {
|
|||||||
title: "menus.exception.notFound",
|
title: "menus.exception.notFound",
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isHideTab: true,
|
isHideTab: true,
|
||||||
isFullPage: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -39,7 +43,6 @@ export const exceptionRoutes: AppRouteRecord = {
|
|||||||
title: "menus.exception.serverError",
|
title: "menus.exception.serverError",
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isHideTab: true,
|
isHideTab: true,
|
||||||
isFullPage: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { AppRouteRecord } from "@/types/router";
|
import { AppRouteRecord } from "@/types/router";
|
||||||
import { widgetsRoutes } from "./widgets";
|
import { widgetsRoutes } from "./widgets";
|
||||||
import { systemRoutes } from "./system";
|
|
||||||
import { exceptionRoutes } from "./exception";
|
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",
|
title: "menus.dashboard.pricing",
|
||||||
icon: "ri:money-cny-box-line",
|
icon: "ri:money-cny-box-line",
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isFullPage: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -173,10 +172,10 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// 通配 404 必须置于静态路由最后
|
// 通配 404 必须置于静态路由最后(name 勿与上方 `/404` 重复,否则按名跳转不稳定)
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "404",
|
name: "CatchAll404",
|
||||||
component: () => import("@views/exception/404/index.vue"),
|
component: () => import("@views/exception/404/index.vue"),
|
||||||
meta: { hidden: true, title: "404" },
|
meta: { hidden: true, title: "404" },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import type { MenuTable } from "@/api/module_system/menu";
|
import type { MenuTable } from "@/api/module_system/menu";
|
||||||
import type { AppRouteRecord, RouteMeta } from "@/types/router";
|
import type { AppRouteRecord, RouteMeta } from "@/types/router";
|
||||||
import { RoutesAlias } from "@/router/routesAlias";
|
import { RoutesAlias } from "@/router/routesAlias";
|
||||||
|
import { MenuTypeEnum } from "@/enums/system/menu.enum";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后端常把「模块目录」与「其下功能菜单」写成同一绝对 route_path。
|
* 后端常把「模块目录」与「其下功能菜单」写成同一绝对 route_path。
|
||||||
@@ -98,7 +99,7 @@ function toComponentImportPath(componentPath: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapMenuNode(item: MenuTable, depth = 0): AppRouteRecord {
|
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
|
const children = childrenRaw.length
|
||||||
? childrenRaw.map((c) => mapMenuNode(c, depth + 1))
|
? childrenRaw.map((c) => mapMenuNode(c, depth + 1))
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -108,7 +109,7 @@ function mapMenuNode(item: MenuTable, depth = 0): AppRouteRecord {
|
|||||||
const redirect = item.redirect?.trim() || undefined;
|
const redirect = item.redirect?.trim() || undefined;
|
||||||
|
|
||||||
const hasKids = !!(children && children.length > 0);
|
const hasKids = !!(children && children.length > 0);
|
||||||
const isDirectory = item.type === 1;
|
const isDirectory = item.type === MenuTypeEnum.CATALOG;
|
||||||
|
|
||||||
let component: string | undefined;
|
let component: string | undefined;
|
||||||
if (isDirectory || (hasKids && !(item.component_path ?? "").trim())) {
|
if (isDirectory || (hasKids && !(item.component_path ?? "").trim())) {
|
||||||
@@ -144,7 +145,7 @@ function mapMenuNode(item: MenuTable, depth = 0): AppRouteRecord {
|
|||||||
* 过滤按钮(type=3),再递归映射为 AppRouteRecord
|
* 过滤按钮(type=3),再递归映射为 AppRouteRecord
|
||||||
*/
|
*/
|
||||||
export function backendMenusToAppRoutes(menus: MenuTable[]): 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 normalized = normalizeMenuNestedPaths(roots);
|
||||||
const mapped = normalized.map((m) => mapMenuNode(m, 0));
|
const mapped = normalized.map((m) => mapMenuNode(m, 0));
|
||||||
return normalizeAppRouteChildPaths(mapped);
|
return normalizeAppRouteChildPaths(mapped);
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ const dashboardMenuSource: AppRouteRecord = {
|
|||||||
title: "menus.dashboard.pricing",
|
title: "menus.dashboard.pricing",
|
||||||
icon: "ri:money-cny-box-line",
|
icon: "ri:money-cny-box-line",
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
isFullPage: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { useWorktabStore } from "./worktab.store";
|
|||||||
import { useMenuStore } from "./menu.store";
|
import { useMenuStore } from "./menu.store";
|
||||||
import { AppRouteRecord } from "@/types/router";
|
import { AppRouteRecord } from "@/types/router";
|
||||||
import { setPageTitle } from "@/utils/navigation/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 { StorageConfig } from "@/utils/storage/storage-config";
|
||||||
import AuthAPI from "@/api/module_system/auth";
|
import AuthAPI from "@/api/module_system/auth";
|
||||||
import UserAPI from "@/api/module_system/user";
|
import UserAPI from "@/api/module_system/user";
|
||||||
@@ -277,6 +277,8 @@ export const useUserStore = defineStore(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
rememberMe.value = loginForm.remember;
|
rememberMe.value = loginForm.remember;
|
||||||
|
// 清除上次会话里「动态路由初始化失败」标记,避免重新登录后侧栏/菜单不注册
|
||||||
|
resetRouteInitState();
|
||||||
Auth.setTokens(data.access_token, data.refresh_token, rememberMe.value);
|
Auth.setTokens(data.access_token, data.refresh_token, rememberMe.value);
|
||||||
setToken(data.access_token, data.refresh_token);
|
setToken(data.access_token, data.refresh_token);
|
||||||
setLoginStatus(true);
|
setLoginStatus(true);
|
||||||
|
|||||||
@@ -117,8 +117,6 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
|||||||
fixedTab?: boolean;
|
fixedTab?: boolean;
|
||||||
/** 激活菜单路径 */
|
/** 激活菜单路径 */
|
||||||
activePath?: string;
|
activePath?: string;
|
||||||
/** 是否为全屏页面 */
|
|
||||||
isFullPage?: boolean;
|
|
||||||
/** 是否为权限按钮行 */
|
/** 是否为权限按钮行 */
|
||||||
isAuthButton?: boolean;
|
isAuthButton?: boolean;
|
||||||
/** 权限标识 */
|
/** 权限标识 */
|
||||||
|
|||||||
@@ -30,19 +30,14 @@ export function isIframe(url: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断菜单项是否可作为默认导航落点
|
* 判断菜单项是否可作为默认导航落点(隐藏菜单不作为首选路径)
|
||||||
* 隐藏的全屏页面虽然不展示在菜单中,但仍然可能是合法首页。
|
|
||||||
*/
|
*/
|
||||||
export const isNavigableMenuItem = (menuItem: AppRouteRecord): boolean => {
|
export const isNavigableMenuItem = (menuItem: AppRouteRecord): boolean => {
|
||||||
if (!menuItem.path || !menuItem.path.trim()) {
|
if (!menuItem.path || !menuItem.path.trim()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!menuItem.meta?.isHide) {
|
return !menuItem.meta?.isHide;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return menuItem.meta?.isFullPage === true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const asciiArt = `
|
|||||||
\x1b[0m
|
\x1b[0m
|
||||||
\x1b[33mGitHub: https://github.com/fastapiadmin/FastapiAdmin
|
\x1b[33mGitHub: https://github.com/fastapiadmin/FastapiAdmin
|
||||||
\x1b[0m
|
\x1b[0m
|
||||||
\x1b[31m技术支持(微信): 948080782,和开发者一起交流~ 群里有小伙伴实时答疑,遇到问题不用慌!
|
\x1b[31m技术支持(社区群): https://service.fastapiadmin.com/about/,和开发者一起交流~ 群里有小伙伴实时答疑,遇到问题不用慌!
|
||||||
\x1b[0m
|
\x1b[0m
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ export * from "./tableCache";
|
|||||||
export * from "./tableConfig";
|
export * from "./tableConfig";
|
||||||
|
|
||||||
export * from "./tableUtils";
|
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"],
|
recordFields: ["list", "data", "records", "items", "result", "rows"],
|
||||||
// 总条数
|
// 总条数
|
||||||
totalFields: ["total", "count"],
|
totalFields: ["total", "count"],
|
||||||
// 当前页码
|
// 当前页码(含后端常用 page_no)
|
||||||
currentFields: ["current", "page", "pageNum"],
|
currentFields: ["current", "page", "pageNum", "page_no"],
|
||||||
// 每页大小
|
// 每页大小(含后端常用 page_size)
|
||||||
sizeFields: ["size", "pageSize", "limit"],
|
sizeFields: ["size", "pageSize", "limit", "page_size"],
|
||||||
|
|
||||||
// 请求参数映射配置,前端发送请求时使用的分页参数名
|
// 请求参数映射配置,前端发送请求时使用的分页参数名
|
||||||
// useTable 组合式函数传递分页参数的时候 用 current 跟 size
|
// useTable 组合式函数传递分页参数的时候 用 current 跟 size
|
||||||
|
|||||||
@@ -42,6 +42,25 @@
|
|||||||
import type { ApiResponse } from "./tableCache";
|
import type { ApiResponse } from "./tableCache";
|
||||||
import { tableConfig } from "./tableConfig";
|
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 {
|
export interface BaseRequestParams extends PageQuery {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
@@ -115,6 +134,8 @@ export const defaultResponseAdapter = <T>(response: unknown): ApiResponse<T> =>
|
|||||||
// 定义支持的字段
|
// 定义支持的字段
|
||||||
const recordFields = tableConfig.recordFields;
|
const recordFields = tableConfig.recordFields;
|
||||||
|
|
||||||
|
response = unwrapAxiosResponseBody(response);
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return { records: [], total: 0 };
|
return { records: [], total: 0 };
|
||||||
}
|
}
|
||||||
@@ -184,7 +205,10 @@ export const updatePaginationFromResponse = <T>(
|
|||||||
pagination: PageQuery,
|
pagination: PageQuery,
|
||||||
response: ApiResponse<T>
|
response: ApiResponse<T>
|
||||||
): void => {
|
): 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,
|
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 { Edit } from "@element-plus/icons-vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import AiChatAPI, { type ChatSession, type ChatSessionDetail } from "@/api/module_ai/chat";
|
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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 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 type { ColumnOption } from "@/types/component";
|
||||||
|
import { useAuth } from "@/hooks/core/useAuth";
|
||||||
|
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||||
|
|
||||||
type MemorySearchForm = {
|
type MemorySearchForm = {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -212,6 +212,8 @@ const showSearchBar = ref(true);
|
|||||||
const searchBarRef = ref<InstanceType<typeof ArtSearchBar> | null>(null);
|
const searchBarRef = ref<InstanceType<typeof ArtSearchBar> | null>(null);
|
||||||
const searchBarRules: Record<string, unknown> = {};
|
const searchBarRules: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
const memorySearchItems = computed<SearchFormItem[]>(() => [
|
const memorySearchItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: "标题",
|
label: "标题",
|
||||||
@@ -269,17 +271,6 @@ function onTableSelectionChange(rows: ChatSession[]) {
|
|||||||
selectedRows.value = rows;
|
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) {
|
function deleteSessionRow(id: string) {
|
||||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
@@ -407,30 +398,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 102,
|
width: 160,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
align: "center",
|
align: "right",
|
||||||
formatter: (row: ChatSession) =>
|
formatter: (row: ChatSession) => formatMemoryOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -522,6 +493,37 @@ async function handleOpenDialog(type: "create" | "detail", id?: string) {
|
|||||||
dialogVisible.visible = true;
|
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) {
|
function handleEditTitle(row: ChatSession) {
|
||||||
editingRowId.value = row.id;
|
editingRowId.value = row.id;
|
||||||
editingTitle.value = row.title || "";
|
editingTitle.value = row.title || "";
|
||||||
|
|||||||
@@ -269,13 +269,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, computed, ref } from "vue";
|
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 { useTable } from "@/hooks/core/useTable";
|
||||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||||
import ArtImportDialog from "@/components/Core/modal/art-import-dialog/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 ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||||
import type { IContentConfig, IObject } from "@/components/Core/modal/types";
|
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 ArtSearchBarWithAudit from "@/components/Core/forms/art-search-bar/ArtSearchBarWithAudit.vue";
|
||||||
import type { AuditSearchFormParams } from "@/components/Core/forms/art-search-bar/auditSearchFormItems";
|
import type { AuditSearchFormParams } from "@/components/Core/forms/art-search-bar/auditSearchFormItems";
|
||||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||||
@@ -294,6 +294,8 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
type DemoSearchFormParams = { name?: string; status?: string } & AuditSearchFormParams;
|
type DemoSearchFormParams = { name?: string; status?: string } & AuditSearchFormParams;
|
||||||
|
|
||||||
function normalizeDemoQuery(params: Record<string, unknown>): DemoPageQuery {
|
function normalizeDemoQuery(params: Record<string, unknown>): DemoPageQuery {
|
||||||
@@ -436,19 +438,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 102,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
formatter: (row: DemoTable) =>
|
align: "right",
|
||||||
h("div", [
|
formatter: (row: DemoTable) => formatDemoOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -591,19 +584,41 @@ const onResetSearch = async () => {
|
|||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonMoreClick = (item: ButtonMoreItem, row: DemoTable) => {
|
function buildDemoRowActions(row: DemoTable): TableOperationAction[] {
|
||||||
switch (item.key) {
|
const all: TableOperationAction[] = [
|
||||||
case "detail":
|
{
|
||||||
openDetailDialog(row);
|
key: "detail",
|
||||||
break;
|
label: "详情",
|
||||||
case "edit":
|
artType: "view",
|
||||||
openEditDialog("edit", row);
|
icon: "ri:file-list-3-line",
|
||||||
break;
|
perm: "module_example:demo:detail",
|
||||||
case "delete":
|
run: () => void openDetailDialog(row),
|
||||||
deleteDemoRow(row);
|
},
|
||||||
break;
|
{
|
||||||
}
|
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) {
|
async function openDetailDialog(row: DemoTable) {
|
||||||
if (!row.id) return;
|
if (!row.id) return;
|
||||||
|
|||||||
@@ -155,13 +155,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, computed, ref, reactive } from "vue";
|
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 { useTable } from "@/hooks/core/useTable";
|
||||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||||
import ArtImportDialog from "@/components/Core/modal/art-import-dialog/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 ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||||
import type { IContentConfig, IObject } from "@/components/Core/modal/types";
|
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 ArtSearchBarWithAudit from "@/components/Core/forms/art-search-bar/ArtSearchBarWithAudit.vue";
|
||||||
import type { AuditSearchFormParams } from "@/components/Core/forms/art-search-bar/auditSearchFormItems";
|
import type { AuditSearchFormParams } from "@/components/Core/forms/art-search-bar/auditSearchFormItems";
|
||||||
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||||
@@ -179,6 +179,8 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
type Demo01SearchFormParams = {
|
type Demo01SearchFormParams = {
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -310,19 +312,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 120,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
formatter: (row: Demo01Table) =>
|
align: "right",
|
||||||
h("div", [
|
formatter: (row: Demo01Table) => formatDemo01OperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -447,19 +440,41 @@ const onResetSearch = async () => {
|
|||||||
await resetSearchParams();
|
await resetSearchParams();
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonMoreClick = (item: ButtonMoreItem, row: Demo01Table) => {
|
function buildDemo01RowActions(row: Demo01Table): TableOperationAction[] {
|
||||||
switch (item.key) {
|
const all: TableOperationAction[] = [
|
||||||
case "detail":
|
{
|
||||||
openDetailDialog(row);
|
key: "detail",
|
||||||
break;
|
label: "详情",
|
||||||
case "edit":
|
artType: "view",
|
||||||
openEditDialog("edit", row);
|
icon: "ri:file-list-3-line",
|
||||||
break;
|
perm: "module_example:demo01:detail",
|
||||||
case "delete":
|
run: () => void openDetailDialog(row),
|
||||||
deleteDemo01Row(row);
|
},
|
||||||
break;
|
{
|
||||||
}
|
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) {
|
async function openDetailDialog(row: Demo01Table) {
|
||||||
if (!row.id) return;
|
if (!row.id) return;
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
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 { useClipboard } from "@vueuse/core";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import type { EditorConfiguration } from "codemirror";
|
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 ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/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 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 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 CreateTableDialog, { type CreateTableSubmitMeta } from "./components/CreateTableDialog.vue";
|
||||||
import GenCodeDrawer from "./components/GenCodeDrawer.vue";
|
import GenCodeDrawer from "./components/GenCodeDrawer.vue";
|
||||||
import ImportDbTableDialog from "./components/ImportDbTableDialog.vue";
|
import ImportDbTableDialog from "./components/ImportDbTableDialog.vue";
|
||||||
@@ -192,6 +192,7 @@ const listRefresh = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
provide(GENCODE_BASIC_FORM_KEY, basicInfo);
|
provide(GENCODE_BASIC_FORM_KEY, basicInfo);
|
||||||
provide(GENCODE_CM_KEY, cmRef);
|
provide(GENCODE_CM_KEY, cmRef);
|
||||||
@@ -763,18 +764,42 @@ type GencodeSearchForm = {
|
|||||||
table_comment?: string;
|
table_comment?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function buttonMoreClick(item: ButtonMoreItem, row: GenTableSchema) {
|
function buildGencodeRowActions(row: GenTableSchema): TableOperationAction[] {
|
||||||
switch (item.key) {
|
const all: TableOperationAction[] = [
|
||||||
case "preview":
|
{
|
||||||
void handlePreviewTable(row);
|
key: "preview",
|
||||||
break;
|
label: "代码生成",
|
||||||
case "delete":
|
artType: "edit",
|
||||||
void handleDelete(row);
|
icon: "ri:magic-line",
|
||||||
break;
|
iconColor: "var(--el-color-primary)",
|
||||||
case "sync":
|
perm: "module_generator:gencode:update",
|
||||||
void handleSynchDb(row);
|
run: () => void handlePreviewTable(row),
|
||||||
break;
|
},
|
||||||
}
|
{
|
||||||
|
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>({
|
const searchForm = ref<GencodeSearchForm>({
|
||||||
@@ -862,36 +887,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 100,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
align: "center",
|
align: "right",
|
||||||
formatter: (row: GenTableSchema) =>
|
formatter: (row: GenTableSchema) => formatGencodeOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ElCard
|
<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' }"
|
:style="{ 'margin-top': showSearchBar ? '12px' : '0' }"
|
||||||
>
|
>
|
||||||
<div
|
<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="资源文件管理系统: 点击路径可以快速返回上级目录">
|
<ElTooltip content="资源文件管理系统: 点击路径可以快速返回上级目录">
|
||||||
<QuestionFilled class="mx-1 h-4 w-4" />
|
<QuestionFilled class="mx-1 h-4 w-4" />
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ArtTableHeader
|
<ArtTableHeader
|
||||||
|
class="resource-toolbar shrink-0"
|
||||||
v-model:columns="columnChecks"
|
v-model:columns="columnChecks"
|
||||||
v-model:showSearchBar="showSearchBar"
|
v-model:showSearchBar="showSearchBar"
|
||||||
layout="search,refresh,size,fullscreen"
|
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@refresh="refreshData"
|
@refresh="refreshData"
|
||||||
>
|
>
|
||||||
@@ -91,16 +91,20 @@
|
|||||||
</template>
|
</template>
|
||||||
</ArtTableHeader>
|
</ArtTableHeader>
|
||||||
|
|
||||||
<ArtTable
|
<!-- 面包屑与表头已占用高度:表格高度须在独立 flex 子项内计算,否则分页会被挤出卡片 -->
|
||||||
row-key="file_url"
|
<div class="resource-table-region min-h-0 flex flex-1 flex-col overflow-hidden pb-3">
|
||||||
:loading="loading"
|
<ArtTable
|
||||||
:data="data"
|
row-key="file_url"
|
||||||
:columns="columns"
|
:show-table-header="false"
|
||||||
:pagination="paginationBind"
|
:loading="loading"
|
||||||
@selection-change="onTableSelectionChange"
|
:data="data"
|
||||||
@pagination:size-change="handleSizeChange"
|
:columns="columns"
|
||||||
@pagination:current-change="handleCurrentChange"
|
:pagination="paginationBind"
|
||||||
/>
|
@selection-change="onTableSelectionChange"
|
||||||
|
@pagination:size-change="handleSizeChange"
|
||||||
|
@pagination:current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<ArtDialog
|
<ArtDialog
|
||||||
@@ -644,6 +648,13 @@ async function handleBatchDelete() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<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) {
|
:deep(.el-breadcrumb__item) {
|
||||||
&.is-link {
|
&.is-link {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
|
|||||||
@@ -178,29 +178,20 @@ import DeptAPI, {
|
|||||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/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 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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||||
import {
|
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||||
ElMessage,
|
|
||||||
ElMessageBox,
|
|
||||||
ElTag,
|
|
||||||
ElTooltip,
|
|
||||||
ElDropdown,
|
|
||||||
ElDropdownMenu,
|
|
||||||
ElDropdownItem,
|
|
||||||
} from "element-plus";
|
|
||||||
import { useAuth } from "@/hooks/core/useAuth";
|
import { useAuth } from "@/hooks/core/useAuth";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import { formatTree } from "@/utils/common";
|
import { formatTree } from "@/utils/common";
|
||||||
|
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Dept",
|
name: "Dept",
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
|
||||||
const { hasAuth } = useAuth();
|
const { hasAuth } = useAuth();
|
||||||
const userStore = useUserStore();
|
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(
|
function buildDeptRowActions(
|
||||||
row: DeptTable,
|
row: DeptTable,
|
||||||
ctx: {
|
ctx: {
|
||||||
@@ -237,8 +218,8 @@ function buildDeptRowActions(
|
|||||||
onEdit: (id: number) => void;
|
onEdit: (id: number) => void;
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
}
|
}
|
||||||
): RowAction[] {
|
): TableOperationAction[] {
|
||||||
const all: RowAction[] = [
|
const all: TableOperationAction[] = [
|
||||||
{
|
{
|
||||||
key: "add",
|
key: "add",
|
||||||
label: "新增",
|
label: "新增",
|
||||||
@@ -268,74 +249,14 @@ function buildDeptRowActions(
|
|||||||
run: () => ctx.onDelete(row.id!),
|
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]) {
|
function formatDeptOperationCell(row: DeptTable, ctx: Parameters<typeof buildDeptRowActions>[1]) {
|
||||||
const actions = buildDeptRowActions(row, ctx);
|
const actions = buildDeptRowActions(row, ctx);
|
||||||
if (actions.length === 0) {
|
return renderTableOperationCell(actions, {
|
||||||
return h("span", { class: "text-g-400" }, "—");
|
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 dept-table-actions",
|
||||||
}
|
});
|
||||||
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]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchForm = ref<DeptSearchForm>({
|
const searchForm = ref<DeptSearchForm>({
|
||||||
|
|||||||
@@ -267,12 +267,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, computed, ref, reactive } from "vue";
|
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 { useTable } from "@/hooks/core/useTable";
|
||||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/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 ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||||
import type { IObject } from "@/components/Core/modal/types";
|
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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||||
@@ -284,6 +282,8 @@ import DictAPI, {
|
|||||||
type DictDataTable,
|
type DictDataTable,
|
||||||
} from "@/api/module_system/dict";
|
} from "@/api/module_system/dict";
|
||||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
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 { useDictStore } from "@/store";
|
||||||
import { useAppStore } from "@/store/modules/app.store";
|
import { useAppStore } from "@/store/modules/app.store";
|
||||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
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 appStore = useAppStore();
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "80%" : "60%"));
|
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "80%" : "60%"));
|
||||||
|
|
||||||
function getTagPreviewStyle(value?: string) {
|
function getTagPreviewStyle(value?: string) {
|
||||||
@@ -398,7 +399,7 @@ const statusOptions = ref([
|
|||||||
|
|
||||||
const dictDataSearchItems = computed<SearchFormItem[]>(() => [
|
const dictDataSearchItems = computed<SearchFormItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: "字典数据标签",
|
label: "字典标签",
|
||||||
key: "dict_label",
|
key: "dict_label",
|
||||||
type: "input",
|
type: "input",
|
||||||
placeholder: "请输入字典标签",
|
placeholder: "请输入字典标签",
|
||||||
@@ -509,33 +510,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 120,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
formatter: (row: DictDataTable) =>
|
align: "right",
|
||||||
h(ArtButtonMore, {
|
formatter: (row: DictDataTable) => formatDictDataOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -667,20 +645,6 @@ function onResetSearch() {
|
|||||||
void resetSearchParams();
|
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() {
|
async function resetForm() {
|
||||||
if (dataFormRef.value) {
|
if (dataFormRef.value) {
|
||||||
dataFormRef.value.resetFields();
|
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) {
|
function deleteDictDataRow(id: number) {
|
||||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||||
confirmButtonText: "确定",
|
confirmButtonText: "确定",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- 字典类型:与 demo 同一套 Art 布局;接口 DictAPI / fetchGetDictTypeList -->
|
<!-- 字典类型:Art 布局;操作列最多 3 个外露 +「更多」 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="art-full-height">
|
<div class="art-full-height">
|
||||||
<ArtSearchBar
|
<ArtSearchBar
|
||||||
@@ -152,12 +152,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, computed, ref, reactive } from "vue";
|
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 { useTable } from "@/hooks/core/useTable";
|
||||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/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 ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||||
import type { IObject } from "@/components/Core/modal/types";
|
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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||||
@@ -170,6 +168,8 @@ import DictAPI, {
|
|||||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||||
import { useDictStore } from "@/store";
|
import { useDictStore } from "@/store";
|
||||||
import DataDrawer from "@/views/module_system/dict/components/DataDrawer.vue";
|
import DataDrawer from "@/views/module_system/dict/components/DataDrawer.vue";
|
||||||
|
import { useAuth } from "@/hooks/core/useAuth";
|
||||||
|
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Dict",
|
name: "Dict",
|
||||||
@@ -191,6 +191,7 @@ function normalizeDictTypeQuery(params: Record<string, unknown>): DictPageQuery
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
const searchForm = ref<DictTypeSearchForm>({
|
const searchForm = ref<DictTypeSearchForm>({
|
||||||
dict_name: undefined,
|
dict_name: undefined,
|
||||||
@@ -314,39 +315,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 120,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
formatter: (row: DictTable) =>
|
align: "right",
|
||||||
h(ArtButtonMore, {
|
formatter: (row: DictTable) => formatDictOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -462,21 +434,50 @@ function onResetSearch() {
|
|||||||
void resetSearchParams();
|
void resetSearchParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
function buttonMoreClick(item: ButtonMoreItem, row: DictTable) {
|
function buildDictRowActions(row: DictTable): TableOperationAction[] {
|
||||||
switch (item.key) {
|
const all: TableOperationAction[] = [
|
||||||
case "dictData":
|
{
|
||||||
handleDictDataDrawer(row);
|
key: "dictData",
|
||||||
break;
|
label: "字典",
|
||||||
case "detail":
|
artType: "view",
|
||||||
void handleOpenDialog("detail", row.id);
|
icon: "ri:book-2-line",
|
||||||
break;
|
iconColor: "var(--el-color-warning)",
|
||||||
case "edit":
|
perm: "module_system:dict_data:query",
|
||||||
void handleOpenDialog("update", row.id);
|
run: () => handleDictDataDrawer(row),
|
||||||
break;
|
},
|
||||||
case "delete":
|
{
|
||||||
if (row.id != null) deleteDictTypeRow(row.id);
|
key: "detail",
|
||||||
break;
|
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) {
|
function handleDictDataDrawer(dictTypeRow: DictTable) {
|
||||||
|
|||||||
@@ -144,14 +144,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, computed, ref, nextTick } from "vue";
|
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 { useTable } from "@/hooks/core/useTable";
|
||||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/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 ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||||
import type { IObject } from "@/components/Core/modal/types";
|
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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 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 type { ColumnOption } from "@/types/component";
|
||||||
import LogAPI, { type LogPageQuery, type LogTable } from "@/api/module_system/log";
|
import LogAPI, { type LogPageQuery, type LogTable } from "@/api/module_system/log";
|
||||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
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";
|
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -167,6 +167,8 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
type LogSearchForm = {
|
type LogSearchForm = {
|
||||||
request_path?: string;
|
request_path?: string;
|
||||||
type?: number;
|
type?: number;
|
||||||
@@ -348,27 +350,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 100,
|
width: 160,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
formatter: (row: LogTable) =>
|
align: "right",
|
||||||
h(ArtButtonMore, {
|
formatter: (row: LogTable) => formatLogOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -473,17 +458,6 @@ function onResetSearch() {
|
|||||||
void resetSearchParams();
|
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() {
|
async function resetForm() {
|
||||||
formData.value = {};
|
formData.value = {};
|
||||||
}
|
}
|
||||||
@@ -515,6 +489,37 @@ function deleteLogRow(id: number) {
|
|||||||
.catch(() => {});
|
.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() {
|
function handleBatchDelete() {
|
||||||
const ids = selectedIds.value;
|
const ids = selectedIds.value;
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- 菜单管理:Art + 树形表格(对齐 system/menu 模版) -->
|
<!-- 菜单管理:Art + 树形表格;操作列最多 3 个外露,其余「更多」 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="art-full-height">
|
<div class="art-full-height">
|
||||||
<ArtSearchBar
|
<ArtSearchBar
|
||||||
@@ -526,22 +526,13 @@ import MenuRouteIcon from "@/components/MenuRouteIcon/index.vue";
|
|||||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/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 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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 ArtDrawer from "@/components/Core/modal/art-drawer/index.vue";
|
||||||
import {
|
import { ElMessage, ElMessageBox, ElTag, ElTooltip } from "element-plus";
|
||||||
ElMessage,
|
|
||||||
ElMessageBox,
|
|
||||||
ElTag,
|
|
||||||
ElTooltip,
|
|
||||||
ElDropdown,
|
|
||||||
ElDropdownMenu,
|
|
||||||
ElDropdownItem,
|
|
||||||
} from "element-plus";
|
|
||||||
import { useAuth } from "@/hooks/core/useAuth";
|
import { useAuth } from "@/hooks/core/useAuth";
|
||||||
|
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||||
|
|
||||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
|
||||||
const { hasAuth } = useAuth();
|
const { hasAuth } = useAuth();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -580,16 +571,6 @@ function ynTag(v: boolean | undefined, yes = "是", no = "否") {
|
|||||||
return h(ElTag, { type: v ? "success" : "danger" }, () => (v ? 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(
|
function buildMenuRowActions(
|
||||||
row: MenuTable,
|
row: MenuTable,
|
||||||
ctx: {
|
ctx: {
|
||||||
@@ -598,8 +579,8 @@ function buildMenuRowActions(
|
|||||||
onEdit: (id: number) => void;
|
onEdit: (id: number) => void;
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
}
|
}
|
||||||
): RowAction[] {
|
): TableOperationAction[] {
|
||||||
const actions: RowAction[] = [];
|
const actions: TableOperationAction[] = [];
|
||||||
if (ctx.onAdd && (row.type === MenuTypeEnum.CATALOG || row.type === MenuTypeEnum.MENU)) {
|
if (ctx.onAdd && (row.type === MenuTypeEnum.CATALOG || row.type === MenuTypeEnum.MENU)) {
|
||||||
actions.push({
|
actions.push({
|
||||||
key: "add",
|
key: "add",
|
||||||
@@ -632,74 +613,14 @@ function buildMenuRowActions(
|
|||||||
run: () => ctx.onDelete(row.id!),
|
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]) {
|
function formatMenuOperationCell(row: MenuTable, ctx: Parameters<typeof buildMenuRowActions>[1]) {
|
||||||
const actions = buildMenuRowActions(row, ctx);
|
const actions = buildMenuRowActions(row, ctx);
|
||||||
if (actions.length === 0) {
|
return renderTableOperationCell(actions, {
|
||||||
return h("span", { class: "text-g-400" }, "—");
|
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 menu-table-actions",
|
||||||
}
|
});
|
||||||
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]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuClientTab = ref<"pc" | "app">("pc");
|
const menuClientTab = ref<"pc" | "app">("pc");
|
||||||
@@ -997,7 +918,7 @@ const { columnChecks, columns } = useTableColumns<MenuTable>(() => [
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 260,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
align: "right",
|
align: "right",
|
||||||
formatter: (row: MenuTable) => formatMenuOperationCell(row, opCtx),
|
formatter: (row: MenuTable) => formatMenuOperationCell(row, opCtx),
|
||||||
|
|||||||
@@ -185,14 +185,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, computed, ref, reactive, nextTick, onMounted } from "vue";
|
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 { useTable } from "@/hooks/core/useTable";
|
||||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/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 ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||||
import type { IObject } from "@/components/Core/modal/types";
|
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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||||
@@ -203,6 +201,8 @@ import NoticeAPI, {
|
|||||||
type NoticeTable,
|
type NoticeTable,
|
||||||
} from "@/api/module_system/notice";
|
} from "@/api/module_system/notice";
|
||||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
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 { useDictStore, useNoticeStore } from "@/store/index";
|
||||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||||
|
|
||||||
@@ -213,6 +213,7 @@ defineOptions({
|
|||||||
|
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
const noticeStore = useNoticeStore();
|
const noticeStore = useNoticeStore();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
type NoticeSearchForm = {
|
type NoticeSearchForm = {
|
||||||
notice_title?: string;
|
notice_title?: string;
|
||||||
@@ -391,33 +392,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 120,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
formatter: (row: NoticeTable) =>
|
align: "right",
|
||||||
h(ArtButtonMore, {
|
formatter: (row: NoticeTable) => formatNoticeOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -555,20 +533,6 @@ function onResetSearch() {
|
|||||||
void resetSearchParams();
|
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() {
|
async function resetForm() {
|
||||||
if (dataFormRef.value) {
|
if (dataFormRef.value) {
|
||||||
dataFormRef.value.resetFields();
|
dataFormRef.value.resetFields();
|
||||||
@@ -641,6 +605,47 @@ function deleteNoticeRow(id: number) {
|
|||||||
.catch(() => {});
|
.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() {
|
function handleBatchDelete() {
|
||||||
const ids = selectedIds.value;
|
const ids = selectedIds.value;
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
|
|||||||
@@ -161,14 +161,12 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, computed, ref, reactive } from "vue";
|
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 { useTable } from "@/hooks/core/useTable";
|
||||||
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
import ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/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 ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||||
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
import ArtExportDialog from "@/components/Core/modal/art-export-dialog/index.vue";
|
||||||
import type { IObject } from "@/components/Core/modal/types";
|
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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
|
||||||
@@ -179,6 +177,8 @@ import ParamsAPI, {
|
|||||||
type ConfigTable,
|
type ConfigTable,
|
||||||
} from "@/api/module_system/params";
|
} from "@/api/module_system/params";
|
||||||
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||||
|
import { useAuth } from "@/hooks/core/useAuth";
|
||||||
|
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||||
import { useConfigStore } from "@/store";
|
import { useConfigStore } from "@/store";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -187,6 +187,7 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
type ParamSearchForm = {
|
type ParamSearchForm = {
|
||||||
config_name?: string;
|
config_name?: string;
|
||||||
@@ -329,33 +330,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 120,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
formatter: (row: ConfigTable) =>
|
align: "right",
|
||||||
h(ArtButtonMore, {
|
formatter: (row: ConfigTable) => formatParamOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -462,20 +440,6 @@ function onResetSearch() {
|
|||||||
void resetSearchParams();
|
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() {
|
async function resetForm() {
|
||||||
if (dataFormRef.value) {
|
if (dataFormRef.value) {
|
||||||
dataFormRef.value.resetFields();
|
dataFormRef.value.resetFields();
|
||||||
@@ -550,6 +514,47 @@ function deleteParamRow(id: number) {
|
|||||||
.catch(() => {});
|
.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() {
|
function handleBatchDelete() {
|
||||||
const ids = selectedIds.value;
|
const ids = selectedIds.value;
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- 角色管理:Art + useTable;操作列前 3 个为 ArtButtonTable,其余收入「更多」下拉(对齐 system/menu) -->
|
<!-- 角色管理:Art + useTable;操作列最多 3 个外露 +「更多」 -->
|
||||||
<template>
|
<template>
|
||||||
<div class="art-full-height">
|
<div class="art-full-height">
|
||||||
<ArtSearchBar
|
<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 ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
|
||||||
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/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 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 type { IObject } from "@/components/Core/modal/types";
|
||||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/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 type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
@@ -212,15 +212,7 @@ import RoleAPI, {
|
|||||||
type RoleTable,
|
type RoleTable,
|
||||||
type TablePageQuery,
|
type TablePageQuery,
|
||||||
} from "@/api/module_system/role";
|
} from "@/api/module_system/role";
|
||||||
import {
|
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
|
||||||
ElMessage,
|
|
||||||
ElMessageBox,
|
|
||||||
ElTag,
|
|
||||||
ElTooltip,
|
|
||||||
ElDropdown,
|
|
||||||
ElDropdownMenu,
|
|
||||||
ElDropdownItem,
|
|
||||||
} from "element-plus";
|
|
||||||
import { useAuth } from "@/hooks/core/useAuth";
|
import { useAuth } from "@/hooks/core/useAuth";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import PermissonDrawer from "./components/PermissonDrawer.vue";
|
import PermissonDrawer from "./components/PermissonDrawer.vue";
|
||||||
@@ -230,8 +222,6 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
|
||||||
|
|
||||||
const { hasAuth } = useAuth();
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
type RoleSearchForm = {
|
type RoleSearchForm = {
|
||||||
@@ -292,17 +282,6 @@ function deptsCell(row: RoleTable) {
|
|||||||
return h("span", { class: "inline-flex flex-wrap items-center" }, tags);
|
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(
|
function buildRoleRowActions(
|
||||||
row: RoleTable,
|
row: RoleTable,
|
||||||
ctx: {
|
ctx: {
|
||||||
@@ -311,22 +290,25 @@ function buildRoleRowActions(
|
|||||||
onEdit: (id: number) => void;
|
onEdit: (id: number) => void;
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
}
|
}
|
||||||
): RowAction[] {
|
): TableOperationAction[] {
|
||||||
const isSys = row.id === 1;
|
const isSys = row.id === 1;
|
||||||
const warn = () => {
|
const warnSys = () => ElMessage.warning("系统默认角色,不可操作");
|
||||||
ElMessage.warning("系统默认角色,不可操作");
|
|
||||||
};
|
const all: TableOperationAction[] = [
|
||||||
const all: RowAction[] = [
|
|
||||||
{
|
{
|
||||||
key: "perm",
|
key: "perm",
|
||||||
label: "分配权限",
|
label: "分配权限",
|
||||||
artType: "edit",
|
artType: "view",
|
||||||
icon: "ri:shield-keyhole-line",
|
icon: "ri:shield-keyhole-line",
|
||||||
|
iconColor: "var(--el-color-primary)",
|
||||||
perm: "module_system:role:permission",
|
perm: "module_system:role:permission",
|
||||||
disabled: isSys,
|
disabled: isSys,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (isSys) warn();
|
if (isSys) {
|
||||||
else ctx.onPerm(row.id, row.name);
|
warnSys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.onPerm(row.id!, row.name);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -334,99 +316,46 @@ function buildRoleRowActions(
|
|||||||
label: "详情",
|
label: "详情",
|
||||||
artType: "view",
|
artType: "view",
|
||||||
perm: "module_system:role:detail",
|
perm: "module_system:role:detail",
|
||||||
run: () => ctx.onDetail(row.id),
|
run: () => ctx.onDetail(row.id!),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "edit",
|
key: "edit",
|
||||||
label: "编辑",
|
label: "编辑",
|
||||||
artType: "edit",
|
artType: "edit",
|
||||||
|
icon: "ri:edit-2-line",
|
||||||
perm: "module_system:role:update",
|
perm: "module_system:role:update",
|
||||||
disabled: isSys,
|
disabled: isSys,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (isSys) warn();
|
if (isSys) {
|
||||||
else ctx.onEdit(row.id);
|
warnSys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.onEdit(row.id!);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "delete",
|
key: "delete",
|
||||||
label: "删除",
|
label: "删除",
|
||||||
artType: "delete",
|
artType: "delete",
|
||||||
|
icon: "ri:delete-bin-4-line",
|
||||||
perm: "module_system:role:delete",
|
perm: "module_system:role:delete",
|
||||||
disabled: isSys,
|
disabled: isSys,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (isSys) warn();
|
if (isSys) {
|
||||||
else ctx.onDelete(row.id);
|
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]) {
|
function formatRoleOperationCell(row: RoleTable, ctx: Parameters<typeof buildRoleRowActions>[1]) {
|
||||||
const actions = buildRoleRowActions(row, ctx);
|
return renderTableOperationCell(buildRoleRowActions(row, ctx), {
|
||||||
if (actions.length === 0) {
|
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 role-table-actions",
|
||||||
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]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchForm = ref<RoleSearchForm>({
|
const searchForm = ref<RoleSearchForm>({
|
||||||
@@ -579,7 +508,7 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 200,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
align: "right",
|
align: "right",
|
||||||
formatter: (row: RoleTable) => formatRoleOperationCell(row, opCtx),
|
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 ArtTable from "@/components/Core/tables/art-table/index.vue";
|
||||||
import ArtTableHeader from "@/components/Core/tables/art-table-header/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 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 ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
|
||||||
import type { SearchFormItem } 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 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 DeptTree from "./components/DeptTree.vue";
|
||||||
import UserTableSelect from "./components/UserTableSelect.vue";
|
import UserTableSelect from "./components/UserTableSelect.vue";
|
||||||
import { useUserStore } from "@/store";
|
import { useUserStore } from "@/store";
|
||||||
import {
|
import { ElMessage, ElMessageBox, ElTag, ElAvatar } from "element-plus";
|
||||||
ElMessage,
|
|
||||||
ElMessageBox,
|
|
||||||
ElTag,
|
|
||||||
ElTooltip,
|
|
||||||
ElDropdown,
|
|
||||||
ElDropdownMenu,
|
|
||||||
ElDropdownItem,
|
|
||||||
ElAvatar,
|
|
||||||
} from "element-plus";
|
|
||||||
import { useAuth } from "@/hooks/core/useAuth";
|
import { useAuth } from "@/hooks/core/useAuth";
|
||||||
import type { ColumnOption } from "@/types/component";
|
import type { ColumnOption } from "@/types/component";
|
||||||
|
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
|
||||||
|
|
||||||
const MAX_INLINE_ROW_ACTIONS = 3;
|
|
||||||
const { hasAuth } = useAuth();
|
const { hasAuth } = useAuth();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
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(
|
function buildUserRowActions(
|
||||||
row: UserInfo,
|
row: UserInfo,
|
||||||
ctx: {
|
ctx: {
|
||||||
@@ -421,9 +401,9 @@ function buildUserRowActions(
|
|||||||
onEdit: (id: number) => void;
|
onEdit: (id: number) => void;
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
}
|
}
|
||||||
): RowAction[] {
|
): TableOperationAction[] {
|
||||||
const sys = row.is_superuser === true;
|
const sys = row.is_superuser === true;
|
||||||
const all: RowAction[] = [
|
const all: TableOperationAction[] = [
|
||||||
{
|
{
|
||||||
key: "resetPwd",
|
key: "resetPwd",
|
||||||
label: "重置密码",
|
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]) {
|
function formatUserOperationCell(row: UserInfo, ctx: Parameters<typeof buildUserRowActions>[1]) {
|
||||||
const actions = buildUserRowActions(row, ctx);
|
const actions = buildUserRowActions(row, ctx);
|
||||||
if (actions.length === 0) {
|
return renderTableOperationCell(actions, {
|
||||||
return h("span", { class: "text-g-400" }, "—");
|
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1 user-table-actions",
|
||||||
}
|
});
|
||||||
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]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataFormRef = ref();
|
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 ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
|
||||||
import ArtSearchBar from "@/components/Core/forms/art-search-bar/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 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 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 { 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 { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { vue3CronPlus } from "vue3-cron-plus";
|
import { vue3CronPlus } from "vue3-cron-plus";
|
||||||
import "vue3-cron-plus/dist/index.css";
|
import "vue3-cron-plus/dist/index.css";
|
||||||
@@ -332,6 +332,7 @@ import "codemirror/mode/python/python.js";
|
|||||||
import "codemirror/theme/dracula.css";
|
import "codemirror/theme/dracula.css";
|
||||||
|
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
|
const { hasAuth } = useAuth();
|
||||||
|
|
||||||
const BATCH_DELETE_NODE_MSG =
|
const BATCH_DELETE_NODE_MSG =
|
||||||
"确认删除选中的节点吗?\n" +
|
"确认删除选中的节点吗?\n" +
|
||||||
@@ -389,20 +390,6 @@ function onTableSelectionChange(rows: NodeTable[]) {
|
|||||||
selectedRows.value = rows;
|
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) {
|
function deleteNodeRow(id: number | undefined) {
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
ElMessageBox.confirm("确认删除该节点吗?将从调度器移除相关任务。", "警告", {
|
ElMessageBox.confirm("确认删除该节点吗?将从调度器移除相关任务。", "警告", {
|
||||||
@@ -419,6 +406,47 @@ function deleteNodeRow(id: number | undefined) {
|
|||||||
.catch(() => {});
|
.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() {
|
function handleBatchDelete() {
|
||||||
const ids = selectedIds.value;
|
const ids = selectedIds.value;
|
||||||
if (ids.length === 0) return;
|
if (ids.length === 0) return;
|
||||||
@@ -498,36 +526,10 @@ const {
|
|||||||
{
|
{
|
||||||
prop: "operation",
|
prop: "operation",
|
||||||
label: "操作",
|
label: "操作",
|
||||||
width: 88,
|
width: 220,
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
align: "center",
|
align: "right",
|
||||||
formatter: (row: NodeTable) =>
|
formatter: (row: NodeTable) => formatNodeOperationCell(row),
|
||||||
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),
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<div class="flex flex-col flex-grow min-w-0">
|
<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">
|
<ElCard class="flex flex-col flex-1 min-h-0 art-table-card">
|
||||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||||
@@ -53,13 +65,52 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue";
|
||||||
import { useTable } from "@/hooks/core/useTable";
|
import { useTable } from "@/hooks/core/useTable";
|
||||||
import { fetchGetUserList } from "@/api/system-manage";
|
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" });
|
defineOptions({ name: "TreeTable" });
|
||||||
|
|
||||||
const showButtons = ref(false);
|
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([
|
const treeData = ref([
|
||||||
@@ -229,6 +280,15 @@ const {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onTreeSearch() {
|
||||||
|
void searchBarRef.value?.validate?.();
|
||||||
|
void refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTreeSearchReset() {
|
||||||
|
void refreshData();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user