feat(components): 增强 FaForm/FaDialog/FaDrawer 并重构业务页面

- FaForm 新增 scrollbar 和 maxHeight 属性,支持表单滚动条
- FaForm 暴露 resetFields、clearValidate、validateField 方法
- FaDialog 和 FaDrawer 新增 formMode、confirmLoading 等属性及 confirm/cancel 事件
- 使用 FaDescriptions 和 FaForm 重构 demo 和 memory 页面,减少模板冗余
- 修复布局组件路径引用,统一至 @/components/layouts
- 补充 Fa 前缀组件的 ESLint 全局变量声明
This commit is contained in:
zhangtao
2026-05-15 19:42:39 +08:00
parent 9176d477f9
commit 0344765e5a
35 changed files with 1864 additions and 1762 deletions
+53
View File
@@ -15,6 +15,58 @@ const __dirname = dirname(__filename)
const autoImportConfig = JSON.parse( const autoImportConfig = JSON.parse(
fs.readFileSync(path.resolve(__dirname, ".auto-import.json"), "utf-8") fs.readFileSync(path.resolve(__dirname, ".auto-import.json"), "utf-8")
) )
// Fa- 组件全局配置(unplugin-vue-components 自动注册)
const faComponents = {
FaTable: "readonly",
FaTableHeader: "readonly",
FaTableHeaderLeft: "readonly",
FaSearchBar: "readonly",
FaSearchBarWithAudit: "readonly",
FaDialog: "readonly",
FaDrawer: "readonly",
FaForm: "readonly",
FaDescriptions: "readonly",
FaImportDialog: "readonly",
FaExportDialog: "readonly",
FaIconSelect: "readonly",
FaWangEditor: "readonly",
FaSvgIcon: "readonly",
FaButtonMore: "readonly",
FaButtonTable: "readonly",
ArtButtonTable: "readonly",
FaExcelImport: "readonly",
FaExcelExport: "readonly",
FaBreadcrumb: "readonly",
FaCardBanner: "readonly",
FaJsonPretty: "readonly",
JsonPretty: "readonly",
FaCopyButton: "readonly",
CopyButton: "readonly",
FaMenuRouteIcon: "readonly",
MenuRouteIcon: "readonly",
FaIntervalTab: "readonly",
FaPagination: "readonly",
FaSizeSelect: "readonly",
FaMarkdownRenderer: "readonly",
FaScreenLock: "readonly",
FaSettingsPanel: "readonly",
FaGlobalSearch: "readonly",
FaHeaderBar: "readonly",
FaFastEnter: "readonly",
FaFireworksEffect: "readonly",
FaGlobalComponent: "readonly",
FaWatermark: "readonly",
FaCountTo: "readonly",
FaECharts: "readonly",
FaIcon: "readonly",
FaChatWindow: "readonly",
FaBackToTop: "readonly",
FaCalender: "readonly",
FaDragVerify: "readonly",
FaException: "readonly",
FaVideoPlayer: "readonly",
FaCutterImg: "readonly",
}
// Element Plus 组件全局配置 // Element Plus 组件全局配置
const elementPlusComponents = { const elementPlusComponents = {
@@ -115,6 +167,7 @@ export default [
languageOptions: { languageOptions: {
globals: { globals: {
...autoImportConfig.globals, ...autoImportConfig.globals,
...faComponents,
Api: "readonly", Api: "readonly",
...elementPlusComponents, ...elementPlusComponents,
// 全局类型定义 // 全局类型定义
@@ -2,7 +2,106 @@
<!-- 支持常用表单组件、自定义组件、插槽、校验、隐藏表单项 --> <!-- 支持常用表单组件、自定义组件、插槽、校验、隐藏表单项 -->
<!-- 写法同 ElementPlus 官方文档组件,把属性写在 props 里面就可以了 --> <!-- 写法同 ElementPlus 官方文档组件,把属性写在 props 里面就可以了 -->
<template> <template>
<section class="px-4 pb-0 pt-4 md:px-4 md:pt-4"> <ElScrollbar v-if="scrollbar" :max-height="maxHeight" :view-style="{ overflowX: 'hidden' }">
<section class="px-4 pb-0 pt-4 md:px-4 md:pt-4">
<ElForm
ref="formRef"
:model="modelValue"
:label-position="labelPosition"
v-bind="{ ...$attrs }"
>
<ElRow class="flex flex-wrap" :gutter="gutter">
<ElCol
v-for="item in visibleFormItems"
:key="item.key"
:xs="getColSpan(item.span, 'xs')"
:sm="getColSpan(item.span, 'sm')"
:md="getColSpan(item.span, 'md')"
:lg="getColSpan(item.span, 'lg')"
:xl="getColSpan(item.span, 'xl')"
>
<ElFormItem
:prop="item.key"
:label-width="item.label ? item.labelWidth || labelWidth : undefined"
>
<template #label v-if="item.label">
<component v-if="typeof item.label !== 'string'" :is="item.label" />
<span v-else>{{ item.label }}</span>
</template>
<slot :name="item.key" :item="item" :modelValue="modelValue">
<component
:is="getComponent(item)"
:model-value="getFieldValue(item.key)"
@update:model-value="setFieldValue(item.key, $event)"
v-bind="getProps(item)"
>
<!-- 下拉选择 -->
<template v-if="item.type === 'select' && getProps(item)?.options">
<ElOption
v-for="option in getProps(item).options"
v-bind="option"
:key="option.value"
/>
</template>
<!-- 复选框组 -->
<template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
<ElCheckbox
v-for="option in getProps(item).options"
v-bind="option"
:key="option.value"
/>
</template>
<!-- 单选框组 -->
<template v-if="item.type === 'radiogroup' && getProps(item)?.options">
<ElRadio
v-for="option in getProps(item).options"
v-bind="option"
:key="option.value"
/>
</template>
<!-- 动态插槽支持 -->
<template
v-for="(slotFn, slotName) in getSlots(item)"
:key="slotName"
#[slotName]
>
<component :is="slotFn" />
</template>
</component>
</slot>
</ElFormItem>
</ElCol>
<ElCol :xs="24" :sm="24" :md="span" :lg="span" :xl="span" class="max-w-full flex-1">
<div
class="mb-3 flex-c flex-wrap justify-end md:flex-row md:items-stretch md:gap-2"
:style="actionButtonsStyle"
>
<div class="flex gap-2 md:justify-center">
<ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
{{ t("table.form.reset") }}
</ElButton>
<ElButton
v-if="showSubmit"
type="primary"
class="submit-button"
@click="handleSubmit"
v-ripple
:disabled="disabledSubmit"
>
{{ t("table.form.submit") }}
</ElButton>
</div>
</div>
</ElCol>
</ElRow>
</ElForm>
</section>
</ElScrollbar>
<!-- 不使用滚动条时直接渲染 -->
<section v-else class="px-4 pb-0 pt-4 md:px-4 md:pt-4">
<ElForm <ElForm
ref="formRef" ref="formRef"
:model="modelValue" :model="modelValue"
@@ -34,7 +133,6 @@
@update:model-value="setFieldValue(item.key, $event)" @update:model-value="setFieldValue(item.key, $event)"
v-bind="getProps(item)" v-bind="getProps(item)"
> >
<!-- 下拉选择 -->
<template v-if="item.type === 'select' && getProps(item)?.options"> <template v-if="item.type === 'select' && getProps(item)?.options">
<ElOption <ElOption
v-for="option in getProps(item).options" v-for="option in getProps(item).options"
@@ -42,8 +140,6 @@
:key="option.value" :key="option.value"
/> />
</template> </template>
<!-- 复选框组 -->
<template v-if="item.type === 'checkboxgroup' && getProps(item)?.options"> <template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
<ElCheckbox <ElCheckbox
v-for="option in getProps(item).options" v-for="option in getProps(item).options"
@@ -51,8 +147,6 @@
:key="option.value" :key="option.value"
/> />
</template> </template>
<!-- 单选框组 -->
<template v-if="item.type === 'radiogroup' && getProps(item)?.options"> <template v-if="item.type === 'radiogroup' && getProps(item)?.options">
<ElRadio <ElRadio
v-for="option in getProps(item).options" v-for="option in getProps(item).options"
@@ -60,8 +154,6 @@
:key="option.value" :key="option.value"
/> />
</template> </template>
<!-- 动态插槽支持 -->
<template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]> <template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
<component :is="slotFn" /> <component :is="slotFn" />
</template> </template>
@@ -206,6 +298,10 @@ interface FormProps {
disabledSubmit?: boolean; disabledSubmit?: boolean;
/** 提交时是否清洗空值 */ /** 提交时是否清洗空值 */
sanitizeOutput?: Partial<SanitizeOutputOptions>; sanitizeOutput?: Partial<SanitizeOutputOptions>;
/** 是否需要内置 ElScrollbar 包裹 */
scrollbar?: boolean;
/** ElScrollbar 最大高度 */
maxHeight?: string;
} }
interface SanitizeOutputOptions { interface SanitizeOutputOptions {
@@ -234,6 +330,8 @@ const props = withDefaults(defineProps<FormProps>(), {
showSubmit: true, showSubmit: true,
disabledSubmit: false, disabledSubmit: false,
sanitizeOutput: () => ({}), sanitizeOutput: () => ({}),
scrollbar: false,
maxHeight: "75vh",
}); });
interface FormEmits { interface FormEmits {
@@ -505,6 +603,12 @@ const handleSubmit = () => {
defineExpose({ defineExpose({
ref: formInstance, ref: formInstance,
validate: (...args: any[]) => formInstance.value?.validate(...args), validate: (...args: any[]) => formInstance.value?.validate(...args),
/** 代理 ElForm.resetFields */
resetFields: (...args: any[]) => formInstance.value?.resetFields(...args),
/** 代理 ElForm.clearValidate */
clearValidate: (...args: any[]) => formInstance.value?.clearValidate(...args),
/** 代理 ElForm.validateField */
validateField: (...args: any[]) => formInstance.value?.validateField(...args),
reset: handleReset, reset: handleReset,
// 允许外部在不触发提交事件时主动获取清洗后的输出。 // 允许外部在不触发提交事件时主动获取清洗后的输出。
getOutput: getSanitizedOutput, getOutput: getSanitizedOutput,
@@ -42,6 +42,16 @@
<template v-if="$slots.footer" #footer> <template v-if="$slots.footer" #footer>
<slot name="footer" /> <slot name="footer" />
</template> </template>
<template v-else-if="formMode" #footer>
<div class="fa-dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton v-if="formMode !== 'detail'" @click="emit('cancel')">
{{ cancelText }}
</ElButton>
<ElButton type="primary" :loading="confirmLoading" @click="emit('confirm')">
{{ confirmText }}
</ElButton>
</div>
</template>
</ElDialog> </ElDialog>
</template> </template>
@@ -63,9 +73,19 @@ const props = withDefaults(
dialogClass?: string; dialogClass?: string;
/** 遮罩层自定义 class */ /** 遮罩层自定义 class */
modalClass?: string; modalClass?: string;
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
formMode?: "detail" | "create" | "update";
/** 确定按钮 loading 状态 */
confirmLoading?: boolean;
/** 确定按钮文本 */
confirmText?: string;
/** 取消按钮文本 */
cancelText?: string;
}>(), }>(),
{ {
draggable: true, draggable: true,
confirmText: "确定",
cancelText: "取消",
} }
); );
@@ -74,6 +94,10 @@ const emit = defineEmits<{
close: []; close: [];
opened: []; opened: [];
"fullscreen-change": [isFullscreen: boolean]; "fullscreen-change": [isFullscreen: boolean];
/** 点击取消按钮 */
cancel: [];
/** 点击确定按钮 */
confirm: [];
}>(); }>();
const attrs = useAttrs(); const attrs = useAttrs();
@@ -112,6 +136,13 @@ const dialogAttrs = computed(() => {
padding-right: 4px; padding-right: 4px;
} }
.fa-dialog-footer {
display: flex;
gap: 8px;
justify-content: flex-end;
padding-top: 4px;
}
.core-overlay-dialog__actions { .core-overlay-dialog__actions {
display: inline-flex; display: inline-flex;
flex-shrink: 0; flex-shrink: 0;
@@ -26,6 +26,16 @@
<template v-if="$slots.footer" #footer> <template v-if="$slots.footer" #footer>
<slot name="footer" /> <slot name="footer" />
</template> </template>
<template v-else-if="formMode" #footer>
<div class="fa-drawer-footer" :style="'padding-right: var(--el-drawer-padding-primary)'">
<ElButton v-if="formMode !== 'detail'" @click="emit('cancel')">
{{ cancelText }}
</ElButton>
<ElButton type="primary" :loading="confirmLoading" @click="emit('confirm')">
{{ confirmText }}
</ElButton>
</div>
</template>
</ElDrawer> </ElDrawer>
</template> </template>
@@ -44,9 +54,19 @@ const props = withDefaults(
direction?: "rtl" | "ltr" | "ttb" | "btt"; direction?: "rtl" | "ltr" | "ttb" | "btt";
/** 透传到 el-drawer 的 class */ /** 透传到 el-drawer 的 class */
drawerClass?: string; drawerClass?: string;
/** 表单模式:detail 仅显示确定;create/update 显示取消+确定 */
formMode?: "detail" | "create" | "update";
/** 确定按钮 loading 状态 */
confirmLoading?: boolean;
/** 确定按钮文本 */
confirmText?: string;
/** 取消按钮文本 */
cancelText?: string;
}>(), }>(),
{ {
direction: "rtl", direction: "rtl",
confirmText: "确定",
cancelText: "取消",
} }
); );
@@ -54,6 +74,10 @@ const emit = defineEmits<{
"update:modelValue": [v: boolean]; "update:modelValue": [v: boolean];
close: []; close: [];
opened: []; opened: [];
/** 点击取消按钮 */
cancel: [];
/** 点击确定按钮 */
confirm: [];
}>(); }>();
const attrs = useAttrs(); const attrs = useAttrs();
@@ -92,6 +116,13 @@ const drawerAttrs = computed(() => {
color: var(--el-text-color-primary); color: var(--el-text-color-primary);
} }
.fa-drawer-footer {
display: flex;
gap: 8px;
justify-content: flex-end;
padding-top: 4px;
}
.core-overlay-drawer__actions { .core-overlay-drawer__actions {
display: inline-flex; display: inline-flex;
flex-shrink: 0; flex-shrink: 0;
@@ -0,0 +1,245 @@
<template>
<ElScrollbar v-if="scrollbar" :max-height="maxHeight" :view-style="{ overflowX: 'hidden' }">
<ElDescriptions v-bind="bindings" :class="ns.b()">
<template v-if="$slots.title" #title>
<slot name="title" />
</template>
<template v-if="!$slots.default">
<ElDescriptionsItem
v-for="item in items"
:key="item.prop"
:label="item.label"
:span="item.span || span"
:label-class-name="item.labelClassName"
:class-name="item.className"
>
<!-- 带 slot 名称的项:通过具名插槽自定义 -->
<slot
v-if="item.slot"
:name="item.slot"
:item="item"
:value="data ? getNestedValue(data, item.prop) : undefined"
:row="data"
/>
<!-- Tag 模式 -->
<template v-else-if="item.tag">
<ElTag
v-if="item.tag === true && data && data[item.prop] !== undefined"
:type="resolveTagType(getNestedValue(data, item.prop))"
>
{{ getNestedValue(data, item.prop) }}
</ElTag>
<ElTag
v-else-if="typeof item.tag === 'object' && data"
:type="resolveTagType(getNestedValue(data, item.prop), item.tag.type)"
>
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
</ElTag>
</template>
<!-- 纯文本模式 -->
<template v-else>
<slot
:name="item.prop"
:item="item"
:value="data ? getNestedValue(data, item.prop) : undefined"
:row="data"
>
{{ data ? getNestedValue(data, item.prop) : "" }}
</slot>
</template>
</ElDescriptionsItem>
</template>
<!-- 完全自定义模式 -->
<template v-else>
<slot />
</template>
</ElDescriptions>
</ElScrollbar>
<!-- 不使用滚动条时直接渲染 -->
<ElDescriptions v-else v-bind="bindings" :class="ns.b()">
<template v-if="$slots.title" #title>
<slot name="title" />
</template>
<template v-if="!$slots.default">
<ElDescriptionsItem
v-for="item in items"
:key="item.prop"
:label="item.label"
:span="item.span || span"
:label-class-name="item.labelClassName"
:class-name="item.className"
>
<slot
v-if="item.slot"
:name="item.slot"
:item="item"
:value="data ? getNestedValue(data, item.prop) : undefined"
:row="data"
/>
<template v-else-if="item.tag">
<ElTag
v-if="item.tag === true && data && data[item.prop] !== undefined"
:type="resolveTagType(getNestedValue(data, item.prop))"
>
{{ getNestedValue(data, item.prop) }}
</ElTag>
<ElTag
v-else-if="typeof item.tag === 'object' && data"
:type="resolveTagType(getNestedValue(data, item.prop), item.tag.type)"
>
{{ resolveTagText(getNestedValue(data, item.prop), item.tag) }}
</ElTag>
</template>
<template v-else>
<slot
:name="item.prop"
:item="item"
:value="data ? getNestedValue(data, item.prop) : undefined"
:row="data"
>
{{ data ? getNestedValue(data, item.prop) : "" }}
</slot>
</template>
</ElDescriptionsItem>
</template>
<template v-else>
<slot />
</template>
</ElDescriptions>
</template>
<script setup lang="ts">
defineOptions({ name: "FaDescriptions" });
import { computed, useAttrs } from "vue";
import { useNamespace } from "element-plus";
// ── 组件类型定义(内联,避免跨文件引用导致 TS Server 延迟)──
/** ElTag 可接受的 type 值 */
export type TagType = "primary" | "success" | "warning" | "danger" | "info";
/** Tag 渲染配置 */
export interface TagConfig {
/**
* Tag 映射表:key 为数据值,value 为 { type: ElTag类型, text: 显示文本 }
* 例:{ '0': { type: 'success', text: '启用' }, '1': { type: 'danger', text: '停用' } }
*/
map?: Record<string, { type?: TagType; text?: string }>;
/** 默认 type,当 map 未匹配到时使用 */
type?: TagType;
}
/** 单个描述项配置 */
export interface DescriptionsItem {
/** 标签文本 */
label: string;
/** 对应 data 中的属性名,支持 'a.b.c' 嵌套路径 */
prop: string;
/** 列跨度,不传则使用组件级 span 默认值 */
span?: number;
/**
* 渲染模式:
* - 不设置:纯文本
* - true:作为 ElTag 渲染,文本为 data[prop]
* - TagConfig 对象:通过 map 映射类型和文本
*/
tag?: boolean | TagConfig;
/** 自定义插槽名称,设置后该 item 将通过具名插槽渲染 */
slot?: string;
/** 传递给 el-descriptions-item 的 label-class-name */
labelClassName?: string;
/** 传递给 el-descriptions-item 的 class-name */
className?: string;
}
const attrs = useAttrs();
const ns = useNamespace("descriptions");
const props = withDefaults(
defineProps<{
/** 每行显示的描述项数量,默认 4 */
column?: number;
/** 是否展示边框,默认 true */
border?: boolean;
/** 列表尺寸,默认 'default' */
size?: "default" | "small";
/** 标签宽度 */
labelWidth?: string;
/** 描述项配置列表 */
items?: DescriptionsItem[];
/** 数据对象,items 模式下自动取 data[item.prop] 渲染 */
data?: Record<string, unknown> | null;
/** 默认 span,当 item 未指定 span 时生效,默认 2 */
span?: number;
/** 是否需要内置 ElScrollbar 包裹,默认 true */
scrollbar?: boolean;
/** ElScrollbar 最大高度,默认 '70vh' */
maxHeight?: string;
}>(),
{
column: 4,
border: true,
size: "default",
labelWidth: undefined,
items: () => [],
data: null,
span: 2,
scrollbar: true,
maxHeight: "70vh",
}
);
// 传递给 ElDescriptions 的绑定属性
const bindings = computed(() => {
const bind: Record<string, unknown> = {
column: props.column,
border: props.border,
...attrs, // 透传 class、style 等属性
};
if (props.size !== "default") bind.size = props.size;
if (props.labelWidth !== undefined) bind.labelWidth = props.labelWidth;
return bind;
});
/** 获取嵌套属性值,支持 'a.b.c' 路径 */
function getNestedValue(obj: Record<string, unknown> | null, path: string): unknown {
if (!obj) return undefined;
return path.split(".").reduce((acc, key) => {
if (acc && typeof acc === "object" && key in acc) {
return (acc as Record<string, unknown>)[key];
}
return undefined;
}, obj as unknown);
}
/** 解析 ElTag 的 type 属性,确保返回 ElTag 可接受的有效类型值 */
function resolveTagType(_value: unknown, fallback?: string): TagType {
// 优先级 1: fallback 本身就是合法 TagType → 直接使用
if (fallback && isValidTagType(fallback)) return fallback;
// 优先级 2: 无有效 fallback 时,用 'info' 作为兜底
return "info";
}
const TAG_TYPES: Set<string> = new Set(["primary", "success", "warning", "danger", "info"]);
function isValidTagType(v: string): v is TagType {
return TAG_TYPES.has(v);
}
/** 解析 ElTag 显示的文本,支持 tagMap 映射 */
function resolveTagText(value: unknown, tag: TagConfig): string {
const raw = value == null ? "" : String(value);
if (tag.map && raw in tag.map) {
return tag.map[raw].text ?? raw;
}
return raw;
}
</script>
<style scoped>
/*
* fa-descriptions 在 ElScrollbar 内部包裹时,
* 需要占满宽度,避免 ElDescriptions 边框被截断。
*/
:deep(.fa-descriptions) {
width: 100%;
}
</style>
@@ -0,0 +1,21 @@
import { ref } from "vue";
/**
* 导入/导出弹窗状态管理
* 减少各页面重复的 importModalVisible / exportModalVisible 及 open 函数
*/
export function useImportExport() {
const importVisible = ref(false);
const exportVisible = ref(false);
return {
importVisible,
exportVisible,
openImport: () => {
importVisible.value = true;
},
openExport: () => {
exportVisible.value = true;
},
};
}
-1
View File
@@ -1 +0,0 @@
../components/layouts/index.vue
+5 -5
View File
@@ -312,11 +312,11 @@ export class MenuProcessor {
private isNavigableRoute(route: AppRouteRecord): boolean { private isNavigableRoute(route: AppRouteRecord): boolean {
return Boolean( return Boolean(
route.path && route.path &&
route.path !== "/" && route.path !== "/" &&
!route.meta?.link && !route.meta?.link &&
route.meta?.isIframe !== true && route.meta?.isIframe !== true &&
route.component && route.component &&
route.component !== "" route.component !== ""
); );
} }
+1 -1
View File
@@ -191,7 +191,7 @@ export class ComponentLoader {
} }
loadLayout(): () => Promise<any> { loadLayout(): () => Promise<any> {
return () => import("@/layouts/index.vue"); return () => import("@/components/layouts/index.vue");
} }
loadIframe(): () => Promise<any> { loadIframe(): () => Promise<any> {
+3 -3
View File
@@ -102,8 +102,8 @@ export const ROUTE_COMPONENT_NESTED_PARENT = "/nested/router-view-parent";
export const ROUTE_PATH_LOGIN_ALT = "/auth/login"; export const ROUTE_PATH_LOGIN_ALT = "/auth/login";
/** /**
* 主框架布局:新版 art 体系(`src/layouts/index.vue` + `src/layouts/art-*` 组件)。 * 主框架布局:新版 art 体系(`src/components/layouts/index.vue` + `src/components/layouts/fa-*` 组件)。
* 旧版 Left/Top/Mix 壳子仍在 `@/layouts/index.vue`,路由不再默认使用。 * 旧版 Left/Top/Mix 壳子已移除,统一使用 `@/components/layouts/index.vue`。
*/ */
export const Layout = () => import("@/components/layouts/index.vue"); export const Layout = () => import("@/components/layouts/index.vue");
@@ -442,7 +442,7 @@ export const staticRoutes: AppRouteRecordRaw[] = [
}, },
{ {
path: "/outside", path: "/outside",
component: () => import("@/layouts/index.vue"), component: () => import("@/components/layouts/index.vue"),
name: "Outside", name: "Outside",
meta: { title: "menus.outside.title" }, meta: { title: "menus.outside.title" },
children: [ children: [
+1 -3
View File
@@ -280,9 +280,7 @@ export interface MapChartProps extends BaseChartProps {
// 双向堆叠柱状图 Props 接口(人口金字塔样式) // 双向堆叠柱状图 Props 接口(人口金字塔样式)
export interface BidirectionalBarChartProps export interface BidirectionalBarChartProps
extends BaseChartProps, extends BaseChartProps, AxisDisplayProps, InteractionProps {
AxisDisplayProps,
InteractionProps {
/** 正向数据(向上显示) */ /** 正向数据(向上显示) */
positiveData: number[]; positiveData: number[];
/** 负向数据(向下显示) */ /** 负向数据(向下显示) */
+29
View File
@@ -0,0 +1,29 @@
/**
* 全局组件 / API 类型声明(手动维护,不会被 unplugin 覆盖)
* 仅用于 <script> 中通过 typeof / h() 等方式引用的组件和 API
* 模板中使用的组件由 unplugin-vue-components 自动注册,无需在此声明
*/
export {};
declare global {
// ─── Fa 组件 ───
const FaButtonTable: (typeof import("./../components/forms/fa-button-table/index.vue"))["default"];
const FaForm: (typeof import("./../components/forms/fa-form/index.vue"))["default"];
const FaSearchBar: (typeof import("./../components/forms/fa-search-bar/index.vue"))["default"];
const FaSearchBarWithAudit: (typeof import("./../components/forms/fa-search-bar/FaSearchBarWithAudit.vue"))["default"];
// ─── 自定义组件 ───
const JsonPretty: (typeof import("./../components/others/fa-json-pretty/index.vue"))["default"];
const CopyButton: (typeof import("./../components/others/fa-copy-button/index.vue"))["default"];
const MenuRouteIcon: (typeof import("./../components/others/fa-menu-routeIcon/index.vue"))["default"];
// ─── Element Plus 组件(在 <script> 中通过 h() 使用) ───
const ElTag: (typeof import("element-plus/es"))["ElTag"];
const ElTooltip: (typeof import("element-plus/es"))["ElTooltip"];
const ElDropdown: (typeof import("element-plus/es"))["ElDropdown"];
const ElDropdownMenu: (typeof import("element-plus/es"))["ElDropdownMenu"];
const ElDropdownItem: (typeof import("element-plus/es"))["ElDropdownItem"];
// ─── Element Plus API ───
const ElMessageBox: (typeof import("element-plus/es"))["ElMessageBox"];
}
@@ -74,48 +74,24 @@
width="920px" width="920px"
dialog-class="session-detail-dialog" dialog-class="session-detail-dialog"
modal-class="session-detail-dialog" modal-class="session-detail-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }">
<ElDescriptions :column="2" border> <FaDescriptions
<ElDescriptionsItem label="会话ID" :span="2"> :column="2"
{{ detailFormData.session_id }} :data="detailFormData"
</ElDescriptionsItem> :items="memoryDetailItems"
<ElDescriptionsItem label="标题" :span="2"> :scrollbar="false"
{{ detailFormData.title }} >
</ElDescriptionsItem> <template #metadata="{ row }">
<ElDescriptionsItem label="用户ID" :span="1"> <pre v-if="row?.metadata">{{ JSON.stringify(row?.metadata, null, 2) }}</pre>
{{ detailFormData.user_id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="团队ID" :span="1">
{{ detailFormData.team_id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="部门名称" :span="1">
{{ detailFormData.team_name || "未知部门" }}
</ElDescriptionsItem>
<ElDescriptionsItem label="Agent ID" :span="1">
{{ detailFormData.agent_id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="1">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="1">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="消息数量" :span="1">
{{ detailFormData.message_count }}
</ElDescriptionsItem>
<ElDescriptionsItem label="会话摘要" :span="2">
{{ detailFormData.summary || "无" }}
</ElDescriptionsItem>
<ElDescriptionsItem label="元数据" :span="2">
<pre v-if="detailFormData.metadata">{{
JSON.stringify(detailFormData.metadata, null, 2)
}}</pre>
<span v-else>无</span> <span v-else>无</span>
</ElDescriptionsItem> </template>
</ElDescriptions> </FaDescriptions>
<ElDivider content-position="left">消息记录</ElDivider> <ElDivider content-position="left">消息记录</ElDivider>
<ElTimeline v-if="detailFormData.messages && detailFormData.messages.length > 0"> <ElTimeline v-if="detailFormData.messages && detailFormData.messages.length > 0">
@@ -158,16 +134,6 @@
class="crud-dialog-art-form" class="crud-dialog-art-form"
/> />
</template> </template>
<template #footer>
<div class="dialog-footer">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template>
</FaDialog> </FaDialog>
</div> </div>
</template> </template>
@@ -184,13 +150,7 @@ 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";
import { formatToDateTime } from "@utils/common"; import { formatToDateTime } from "@utils/common";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
@@ -264,6 +224,7 @@ const memorySearchItems = computed<SearchFormItem[]>(() => [
]); ]);
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const memoryFormRenderKey = ref(0); const memoryFormRenderKey = ref(0);
const memoryDialogFormItems = computed<FormItem[]>(() => [ const memoryDialogFormItems = computed<FormItem[]>(() => [
@@ -439,6 +400,21 @@ const dialogVisible = reactive({
const detailFormData = ref<Partial<ChatSessionDetail>>({}); const detailFormData = ref<Partial<ChatSessionDetail>>({});
const memoryDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "会话ID", prop: "session_id" },
{ label: "标题", prop: "title" },
{ label: "用户ID", prop: "user_id", span: 1 },
{ label: "团队ID", prop: "team_id", span: 1 },
{ label: "部门名称", prop: "team_name", span: 1 },
{ label: "Agent ID", prop: "agent_id", span: 1 },
{ label: "创建时间", prop: "created_time", span: 1 },
{ label: "更新时间", prop: "updated_time", span: 1 },
{ label: "消息数量", prop: "message_count", span: 1 },
{ label: "会话摘要", prop: "summary" },
{ label: "元数据", prop: "metadata", slot: "metadata" },
];
const rules = reactive({ const rules = reactive({
title: [{ required: true, message: "请输入标题", trigger: "blur" }], title: [{ required: true, message: "请输入标题", trigger: "blur" }],
}); });
@@ -469,8 +445,8 @@ async function onResetSearch() {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
} }
@@ -457,8 +457,8 @@ function resetForm() {
status: "0", status: "0",
description: "", description: "",
}); });
formRef.value?.ref?.resetFields(); formRef.value?.resetFields();
formRef.value?.ref?.clearValidate(); formRef.value?.clearValidate();
} }
function handleCloseDialog() { function handleCloseDialog() {
@@ -38,8 +38,8 @@
:perm-patch="['module_example:demo:patch']" :perm-patch="['module_example:demo:patch']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="openEditDialog('add')" @add="openEditDialog('add')"
@import="openImportModal" @import="openImport"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="runBatchStatus" @more="runBatchStatus"
/> />
@@ -64,205 +64,86 @@
width="920px" width="920px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="4" border> :column="4"
<ElDescriptionsItem label="名称" :span="2"> :data="detailFormData"
{{ detailFormData.name }} :items="demoDetailItems"
</ElDescriptionsItem> max-height="70vh"
<ElDescriptionsItem label="UUID" :span="2"> >
{{ detailFormData.uuid }} <template #i="{ row }">
</ElDescriptionsItem> <JsonPretty v-if="row?.i != null" :value="row?.i" height="140px" />
<ElDescriptionsItem label="状态" :span="2"> </template>
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'"> </FaDescriptions>
{{ detailFormData.status === "0" ? "启用" : "停用" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="整数" :span="2">
{{ detailFormData.a }}
</ElDescriptionsItem>
<ElDescriptionsItem label="大整数" :span="2">
{{ detailFormData.b }}
</ElDescriptionsItem>
<ElDescriptionsItem label="浮点数" :span="2">
{{ detailFormData.c }}
</ElDescriptionsItem>
<ElDescriptionsItem label="布尔值" :span="2">
<ElTag :type="detailFormData.d ? 'success' : 'danger'">
{{ detailFormData.d ? "是" : "否" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="日期" :span="2">
{{ detailFormData.e }}
</ElDescriptionsItem>
<ElDescriptionsItem label="时间" :span="2">
{{ detailFormData.f }}
</ElDescriptionsItem>
<ElDescriptionsItem label="日期时间" :span="2">
{{ detailFormData.g }}
</ElDescriptionsItem>
<ElDescriptionsItem label="长文本" :span="2">
{{ detailFormData.h }}
</ElDescriptionsItem>
<ElDescriptionsItem label="元数据" :span="2">
<JsonPretty
v-if="detailFormData.i != null"
:value="detailFormData.i"
height="140px"
/>
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="2">
{{ detailFormData.description }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建人" :span="2">
{{ detailFormData.created_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新人" :span="2">
{{ detailFormData.updated_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<ElForm :key="demoFormRenderKey"
ref="dataFormRef" scrollbar
:model="formData" max-height="70vh"
:rules="rules" ref="dataFormRef"
label-suffix=":" v-model="formData"
label-width="100px" :items="demoDialogFormItems"
label-position="right" :rules="rules"
inline label-suffix=":"
> :label-width="'auto'"
<ElFormItem label="名称" prop="name"> label-position="right"
<ElInput v-model="formData.name" placeholder="请输入名称" :maxlength="50" /> :span="24"
</ElFormItem> :gutter="16"
<ElFormItem label="状态" prop="status"> :show-reset="false"
<ElRadioGroup v-model="formData.status"> :show-submit="false"
<ElRadio value="0">启用</ElRadio> class="crud-dialog-art-form"
<ElRadio value="1">停用</ElRadio> >
</ElRadioGroup> <template #i>
</ElFormItem> <div class="flex flex-col gap-2">
<ElFormItem label="整数" prop="a"> <div
<ElInputNumber v-model="formData.a" placeholder="请输入整数" /> v-for="(item, index) in metadataList"
</ElFormItem> :key="index"
<ElFormItem label="大整数" prop="b"> class="flex items-center gap-2"
<ElInputNumber v-model="formData.b" placeholder="请输入大整数" /> >
</ElFormItem> <ElInput v-model="item.key" placeholder="键" />
<ElFormItem label="浮点数" prop="c"> <ElInput v-model="item.value" placeholder="值" />
<ElInputNumber
v-model="formData.c"
placeholder="请输入浮点数"
:step="0.01"
:precision="2"
/>
</ElFormItem>
<ElFormItem label="布尔值" prop="d">
<ElSwitch v-model="formData.d" />
</ElFormItem>
<ElFormItem label="日期" prop="e">
<ElDatePicker
v-model="formData.e"
type="date"
placeholder="请选择日期"
:style="'width: 100%'"
value-format="YYYY-MM-DD"
/>
</ElFormItem>
<ElFormItem label="时间" prop="f">
<ElTimePicker
v-model="formData.f"
placeholder="请选择时间"
:style="'width: 100%'"
value-format="HH:mm:ss"
/>
</ElFormItem>
<ElFormItem label="日期时间" prop="g">
<ElDatePicker
v-model="formData.g"
type="datetime"
placeholder="请选择日期时间"
:style="'width: 100%'"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</ElFormItem>
<ElFormItem label="长文本" prop="h">
<ElInput v-model="formData.h" :rows="4" type="textarea" placeholder="请输入长文本" />
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput
v-model="formData.description"
:rows="4"
:maxlength="100"
show-word-limit
type="textarea"
placeholder="请输入描述"
/>
</ElFormItem>
<ElFormItem label="元数据" prop="i">
<div class="flex flex-col gap-2">
<div
v-for="(item, index) in metadataList"
:key="index"
class="flex items-center gap-2"
>
<ElInput v-model="item.key" placeholder="键" />
<ElInput v-model="item.value" placeholder="值" />
<ElButton
type="primary"
icon="Plus"
circle
@click="metadataList.push({ key: '', value: '' })"
/>
<ElButton
type="danger"
icon="Delete"
circle
@click="metadataList.splice(index, 1)"
/>
</div>
<ElButton <ElButton
v-if="metadataList.length === 0"
type="primary" type="primary"
icon="Plus" icon="Plus"
circle
@click="metadataList.push({ key: '', value: '' })" @click="metadataList.push({ key: '', value: '' })"
> />
添加元数据 <ElButton
</ElButton> type="danger"
icon="Delete"
circle
@click="metadataList.splice(index, 1)"
/>
</div> </div>
</ElFormItem> <ElButton
</ElForm> v-if="metadataList.length === 0"
</ElScrollbar> type="primary"
</template> icon="Plus"
@click="metadataList.push({ key: '', value: '' })"
<template #footer> >
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'"> 添加元数据
<ElButton @click="handleCloseDialog">取消</ElButton> </ElButton>
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit"> </div>
确定 </template>
</ElButton> </FaForm>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
<FaImportDialog <FaImportDialog
v-model="importModalVisible" v-model="importVisible"
:content-config="demoImportContentConfig" :content-config="demoImportContentConfig"
default-template-file-name="demo_import_template.xlsx" default-template-file-name="demo_import_template.xlsx"
@upload="handleCrudImportUpload" @upload="handleCrudImportUpload"
/> />
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="demoExportContentConfig" :content-config="demoExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -272,25 +153,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref } from "vue";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaImportDialog from "@/components/modal/fa-import-dialog/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IContentConfig, IObject } from "@/components/modal/types"; import type { IContentConfig, IObject } from "@/components/modal/types";
import FaSearchBarWithAudit from "@/components/forms/fa-search-bar/FaSearchBarWithAudit.vue";
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems"; import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
import FaDialog from "@/components/modal/fa-dialog/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import JsonPretty from "@/components/others/fa-json-pretty/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import DemoAPI, { import DemoAPI, {
type DemoForm, type DemoForm,
type DemoPageQuery, type DemoPageQuery,
type DemoTable, type DemoTable,
} from "@/api/module_example/demo"; } from "@/api/module_example/demo";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { ResultEnum } from "@/enums/api/result.enum"; import { ResultEnum } from "@/enums/api/result.enum";
defineOptions({ defineOptions({
@@ -502,6 +377,110 @@ const dialogVisible = reactive({
const detailFormData = ref<DemoTable>({}); const detailFormData = ref<DemoTable>({});
const demoDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "名称", prop: "name" },
{ label: "UUID", prop: "uuid" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "整数", prop: "a" },
{ label: "大整数", prop: "b" },
{ label: "浮点数", prop: "c" },
{
label: "布尔值",
prop: "d",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{ label: "日期", prop: "e" },
{ label: "时间", prop: "f" },
{ label: "日期时间", prop: "g" },
{ label: "长文本", prop: "h" },
{ label: "元数据", prop: "i", slot: "i" },
{ label: "描述", prop: "description" },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
// 示例页新增/编辑表单 —— 元数据字段用具名插槽渲染
const demoDialogFormItems: FormItem[] = [
{
key: "name",
label: "名称",
type: "input",
props: { placeholder: "请输入名称", maxlength: 50 },
},
{
key: "status",
label: "状态",
type: "radiogroup",
props: {
options: [
{ label: "启用", value: "0" },
{ label: "停用", value: "1" },
],
},
},
{ key: "a", label: "整数", type: "number", props: { placeholder: "请输入整数" } },
{ key: "b", label: "大整数", type: "number", props: { placeholder: "请输入大整数" } },
{
key: "c",
label: "浮点数",
type: "number",
props: { placeholder: "请输入浮点数", step: 0.01, precision: 2 },
},
{ key: "d", label: "布尔值", type: "switch" },
{
key: "e",
label: "日期",
type: "date",
props: { placeholder: "请选择日期", valueFormat: "YYYY-MM-DD", style: "width: 100%" },
},
{
key: "f",
label: "时间",
type: "timepicker",
props: { placeholder: "请选择时间", valueFormat: "HH:mm:ss", style: "width: 100%" },
},
{
key: "g",
label: "日期时间",
type: "datetime",
props: {
placeholder: "请选择日期时间",
valueFormat: "YYYY-MM-DD HH:mm:ss",
style: "width: 100%",
},
},
{
key: "h",
label: "长文本",
type: "input",
props: { type: "textarea", rows: 4, placeholder: "请输入长文本" },
},
{
key: "description",
label: "描述",
type: "input",
props: {
type: "textarea",
rows: 4,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
{ key: "i", label: "元数据", type: "input" /* 由 #i 插槽接管 */ },
];
const formData = ref<DemoForm>({ const formData = ref<DemoForm>({
id: undefined, id: undefined,
name: "", name: "",
@@ -523,11 +502,12 @@ const rules = reactive({
status: [{ required: true, message: "请选择状态", trigger: "blur" }], status: [{ required: true, message: "请选择状态", trigger: "blur" }],
}); });
const dataFormRef = ref(); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const demoFormRenderKey = ref(0);
const metadataList = ref<{ key: string; value: string }[]>([]); const metadataList = ref<{ key: string; value: string }[]>([]);
const importModalVisible = ref(false); const { importVisible, exportVisible, openImport, openExport } = useImportExport();
const exportModalVisible = ref(false);
const initialFormData: DemoForm = { const initialFormData: DemoForm = {
id: undefined, id: undefined,
@@ -628,6 +608,7 @@ async function openEditDialog(type: "add" | "edit", row?: DemoTable) {
Object.assign(formData.value, initialFormData); Object.assign(formData.value, initialFormData);
formData.value.id = undefined; formData.value.id = undefined;
metadataList.value = []; metadataList.value = [];
demoFormRenderKey.value += 1;
} else if (row?.id) { } else if (row?.id) {
dialogVisible.title = "修改"; dialogVisible.title = "修改";
const response = await DemoAPI.getDemoDetail(row.id); const response = await DemoAPI.getDemoDetail(row.id);
@@ -659,7 +640,7 @@ async function handleCloseDialog() {
} }
async function handleSubmit() { async function handleSubmit() {
dataFormRef.value.validate(async (valid: boolean) => { dataFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return; if (!valid) return;
const submitData = { ...formData.value }; const submitData = { ...formData.value };
if (metadataList.value.length > 0) { if (metadataList.value.length > 0) {
@@ -757,10 +738,6 @@ async function runBatchStatus(status: string) {
} }
} }
function openImportModal() {
importModalVisible.value = true;
}
async function handleCrudImportUpload(formData: FormData) { async function handleCrudImportUpload(formData: FormData) {
try { try {
const res = await DemoAPI.importDemo(formData); const res = await DemoAPI.importDemo(formData);
@@ -769,15 +746,11 @@ async function handleCrudImportUpload(formData: FormData) {
return; return;
} }
ElMessage.success(res.data.msg || "导入成功"); ElMessage.success(res.data.msg || "导入成功");
importModalVisible.value = false; importVisible.value = false;
await refreshData(); await refreshData();
} catch (error) { } catch (error) {
console.error("[Import]", error); console.error("[Import]", error);
ElMessage.error("导入失败"); ElMessage.error("导入失败");
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
</script> </script>
@@ -38,8 +38,8 @@
:perm-patch="['module_example:demo01:patch']" :perm-patch="['module_example:demo01:patch']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="openEditDialog('add')" @add="openEditDialog('add')"
@import="openImportModal" @import="openImport"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="runBatchStatus" @more="runBatchStatus"
/> />
@@ -64,87 +64,56 @@
width="768px" width="768px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="2" border> :column="2"
<ElDescriptionsItem label="名称" :span="2"> :data="detailFormData"
{{ detailFormData.name }} :items="demo01DetailItems"
</ElDescriptionsItem> max-height="70vh"
<ElDescriptionsItem label="UUID" :span="2"> />
{{ detailFormData.uuid }}
</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2">
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'">
{{ detailFormData.status === "0" ? "正常" : "停用" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="2">
{{ detailFormData.description }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建人" :span="2">
{{ detailFormData.created_by?.name ?? "—" }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新人" :span="2">
{{ detailFormData.updated_by?.name ?? "—" }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm :key="demo01FormRenderKey"
:key="demo01FormRenderKey" scrollbar
ref="dataFormRef" max-height="70vh"
v-model="formData" ref="dataFormRef"
:items="demo01DialogFormItems" v-model="formData"
:rules="rules" :items="demo01DialogFormItems"
label-suffix=":" :rules="rules"
:label-width="'auto'" label-suffix=":"
label-position="right" :label-width="'auto'"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #status> >
<ElRadioGroup v-model="formData.status"> <template #status>
<ElRadio value="0">正常</ElRadio> <ElRadioGroup v-model="formData.status">
<ElRadio value="1">停用</ElRadio> <ElRadio value="0">正常</ElRadio>
</ElRadioGroup> <ElRadio value="1">停用</ElRadio>
</template> </ElRadioGroup>
</FaForm> </template>
</ElScrollbar> </FaForm>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
<FaImportDialog <FaImportDialog
v-model="importModalVisible" v-model="importVisible"
:content-config="demo01ImportContentConfig" :content-config="demo01ImportContentConfig"
default-template-file-name="demo01_import_template.xlsx" default-template-file-name="demo01_import_template.xlsx"
@upload="handleCrudImportUpload" @upload="handleCrudImportUpload"
/> />
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="demo01ExportContentConfig" :content-config="demo01ExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -154,18 +123,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive } from "vue";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaImportDialog from "@/components/modal/fa-import-dialog/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IContentConfig, IObject } from "@/components/modal/types"; import type { IContentConfig, IObject } from "@/components/modal/types";
import FaSearchBarWithAudit from "@/components/forms/fa-search-bar/FaSearchBarWithAudit.vue";
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems"; import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import Demo01API, { import Demo01API, {
@@ -373,6 +336,24 @@ const dialogVisible = reactive({
const detailFormData = ref<Demo01Table>({}); const detailFormData = ref<Demo01Table>({});
const demo01DetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "名称", prop: "name" },
{ label: "UUID", prop: "uuid" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "正常" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "描述", prop: "description" },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<Demo01Form>({ const formData = ref<Demo01Form>({
id: undefined, id: undefined,
name: "", name: "",
@@ -386,6 +367,7 @@ const rules = reactive({
}); });
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const demo01FormRenderKey = ref(0); const demo01FormRenderKey = ref(0);
const demo01DialogFormItems = computed<FormItem[]>(() => [ const demo01DialogFormItems = computed<FormItem[]>(() => [
@@ -412,8 +394,7 @@ const demo01DialogFormItems = computed<FormItem[]>(() => [
}, },
]); ]);
const importModalVisible = ref(false); const { importVisible, exportVisible, openImport, openExport } = useImportExport();
const exportModalVisible = ref(false);
const initialFormData: Demo01Form = { const initialFormData: Demo01Form = {
id: undefined, id: undefined,
@@ -516,8 +497,8 @@ async function openEditDialog(type: "add" | "edit", row?: Demo01Table) {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData.value, initialFormData); Object.assign(formData.value, initialFormData);
} }
@@ -605,10 +586,6 @@ async function runBatchStatus(status: string) {
} }
} }
function openImportModal() {
importModalVisible.value = true;
}
async function handleCrudImportUpload(formDataUpload: FormData) { async function handleCrudImportUpload(formDataUpload: FormData) {
try { try {
const res = await Demo01API.importDemo01(formDataUpload); const res = await Demo01API.importDemo01(formDataUpload);
@@ -617,17 +594,13 @@ async function handleCrudImportUpload(formDataUpload: FormData) {
return; return;
} }
ElMessage.success(res.data.msg || "导入成功"); ElMessage.success(res.data.msg || "导入成功");
importModalVisible.value = false; importVisible.value = false;
await refreshData(); await refreshData();
} catch (error) { } catch (error) {
console.error("[Import]", error); console.error("[Import]", error);
ElMessage.error("导入失败"); ElMessage.error("导入失败");
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -65,7 +65,7 @@
<div v-show="editMode === 'visual'" class="visual-pane"> <div v-show="editMode === 'visual'" class="visual-pane">
<ElScrollbar max-height="min(58vh, 520px)" class="visual-pane-scroll"> <ElScrollbar max-height="min(58vh, 520px)" class="visual-pane-scroll">
<div class="visual-structure max-w-3xl"> <div class="visual-structure max-w-3xl">
<ElDescriptions :column="1" border size="small" class="visual-desc"> <FaDescriptions :column="1" size="small" class="visual-desc" :scrollbar="false">
<template #title> <template #title>
<span class="text-sm font-medium text-[var(--el-text-color-primary)]">选项</span> <span class="text-sm font-medium text-[var(--el-text-color-primary)]">选项</span>
</template> </template>
@@ -81,9 +81,9 @@
<ElRadioButton value="masterSub">主表 + 子表明细</ElRadioButton> <ElRadioButton value="masterSub">主表 + 子表明细</ElRadioButton>
</ElRadioGroup> </ElRadioGroup>
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
<ElDescriptions :column="1" border size="small" class="visual-desc mt-3"> <FaDescriptions :column="1" size="small" class="visual-desc mt-3" :scrollbar="false">
<template #title> <template #title>
<span class="text-sm font-medium text-[var(--el-text-color-primary)]">主表</span> <span class="text-sm font-medium text-[var(--el-text-color-primary)]">主表</span>
</template> </template>
@@ -101,14 +101,14 @@
<ElDescriptionsItem label="说明" label-class-name="visual-desc-label"> <ElDescriptionsItem label="说明" label-class-name="visual-desc-label">
<ElInput v-model="visual.mainComment" placeholder="中文说明,可选" clearable /> <ElInput v-model="visual.mainComment" placeholder="中文说明,可选" clearable />
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
<ElDescriptions <FaDescriptions
v-if="visual.subEnabled" v-if="visual.subEnabled"
:column="1" :column="1"
border
size="small" size="small"
class="visual-desc mt-3" class="visual-desc mt-3"
:scrollbar="false"
> >
<template #title> <template #title>
<span class="text-sm font-medium text-[var(--el-text-color-primary)]">子表</span> <span class="text-sm font-medium text-[var(--el-text-color-primary)]">子表</span>
@@ -137,7 +137,7 @@
<ElDescriptionsItem label="对应主表列" label-class-name="visual-desc-label"> <ElDescriptionsItem label="对应主表列" label-class-name="visual-desc-label">
<ElInput v-model="visual.fkRefColumn" placeholder="一般是 id" clearable /> <ElInput v-model="visual.fkRefColumn" placeholder="一般是 id" clearable />
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
<div class="mt-3"> <div class="mt-3">
<ElLink type="primary" underline="never" @click="syncVisualToSql"> <ElLink type="primary" underline="never" @click="syncVisualToSql">
@@ -179,6 +179,7 @@ import { ElMessage } from "element-plus";
import { ArrowDown, CopyDocument } from "@element-plus/icons-vue"; import { ArrowDown, CopyDocument } from "@element-plus/icons-vue";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import FaDialog from "@/components/modal/fa-dialog/index.vue"; import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
import { useSettingsStore } from "@stores"; import { useSettingsStore } from "@stores";
import { ThemeMode } from "@/enums/settings/theme.enum"; import { ThemeMode } from "@/enums/settings/theme.enum";
import { buildSqlFromVisual } from "../utils/buildCreateTableSql"; import { buildSqlFromVisual } from "../utils/buildCreateTableSql";
+3 -2
View File
@@ -13,7 +13,7 @@
<span class="font-medium">Redis监控信息</span> <span class="font-medium">Redis监控信息</span>
</div> </div>
</template> </template>
<ElDescriptions :column="6" border> <FaDescriptions :column="6" :scrollbar="false">
<ElDescriptionsItem label="Redis版本"> <ElDescriptionsItem label="Redis版本">
{{ cache.info?.redis_version || "-" }} {{ cache.info?.redis_version || "-" }}
</ElDescriptionsItem> </ElDescriptionsItem>
@@ -55,7 +55,7 @@
{{ cache.info?.instantaneous_input_kbps || 0 }}kps/ {{ cache.info?.instantaneous_input_kbps || 0 }}kps/
{{ cache.info?.instantaneous_output_kbps || 0 }}kps {{ cache.info?.instantaneous_output_kbps || 0 }}kps
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
</ElCard> </ElCard>
</ElCol> </ElCol>
</ElRow> </ElRow>
@@ -262,6 +262,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
import { ElMessageBox } from "element-plus"; import { ElMessageBox } from "element-plus";
import CacheAPI, { import CacheAPI, {
type CacheInfo, type CacheInfo,
@@ -66,7 +66,6 @@ import FaTable from "@/components/tables/fa-table/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue"; import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue"; import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import ArtButtonTable from "@/components/forms/fa-button-table/index.vue";
import CopyButton from "@/components/others/fa-copy-button/index.vue"; import CopyButton from "@/components/others/fa-copy-button/index.vue";
import OnlineAPI, { type OnlineUserTable } from "@/api/module_monitor/online"; import OnlineAPI, { type OnlineUserTable } from "@/api/module_monitor/online";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
@@ -235,7 +234,7 @@ const {
} }
return h(ElTooltip, { content: "强退", placement: "top" }, () => return h(ElTooltip, { content: "强退", placement: "top" }, () =>
h("span", { class: "inline-flex" }, [ h("span", { class: "inline-flex" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: "delete", type: "delete",
onClick: () => kickSession(row.session_id), onClick: () => kickSession(row.session_id),
}), }),
@@ -222,7 +222,6 @@ import FaTable from "@/components/tables/fa-table/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue"; import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue"; import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import ArtButtonTable from "@/components/forms/fa-button-table/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue"; import FaDialog from "@/components/modal/fa-dialog/index.vue";
import { ResourceAPI, type ResourceItem } from "@/api/module_monitor/resource"; import { ResourceAPI, type ResourceItem } from "@/api/module_monitor/resource";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
@@ -406,7 +405,7 @@ const {
!row.is_dir && hasAuth("module_monitor:resource:download") !row.is_dir && hasAuth("module_monitor:resource:download")
? h(ElTooltip, { content: "下载", placement: "top" }, () => ? h(ElTooltip, { content: "下载", placement: "top" }, () =>
h("span", { class: "inline-flex" }, [ h("span", { class: "inline-flex" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: "view", type: "view",
icon: "ri:download-line", icon: "ri:download-line",
onClick: () => void handleDownload(row), onClick: () => void handleDownload(row),
@@ -417,7 +416,7 @@ const {
hasAuth("module_monitor:resource:rename") hasAuth("module_monitor:resource:rename")
? h(ElTooltip, { content: "重命名", placement: "top" }, () => ? h(ElTooltip, { content: "重命名", placement: "top" }, () =>
h("span", { class: "inline-flex" }, [ h("span", { class: "inline-flex" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: "edit", type: "edit",
onClick: () => void handleRenameOpen(row), onClick: () => void handleRenameOpen(row),
}), }),
@@ -427,7 +426,7 @@ const {
hasAuth("module_monitor:resource:delete") hasAuth("module_monitor:resource:delete")
? h(ElTooltip, { content: "删除", placement: "top" }, () => ? h(ElTooltip, { content: "删除", placement: "top" }, () =>
h("span", { class: "inline-flex" }, [ h("span", { class: "inline-flex" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: "delete", type: "delete",
onClick: () => void handleDelete(row), onClick: () => void handleDelete(row),
}), }),
@@ -25,7 +25,7 @@
> >
<span class="text-sm">{{ (server.cpu?.used || 0).toFixed(1) }}%</span> <span class="text-sm">{{ (server.cpu?.used || 0).toFixed(1) }}%</span>
</ElProgress> </ElProgress>
<ElDescriptions :column="2" border size="small"> <FaDescriptions :column="2" size="small" :scrollbar="false">
<ElDescriptionsItem label="核心数"> <ElDescriptionsItem label="核心数">
{{ server.cpu?.cpu_num || 0 }} {{ server.cpu?.cpu_num || 0 }}
</ElDescriptionsItem> </ElDescriptionsItem>
@@ -38,7 +38,7 @@
<ElDescriptionsItem label="系统使用率"> <ElDescriptionsItem label="系统使用率">
{{ (server.cpu?.sys || 0).toFixed(1) }}% {{ (server.cpu?.sys || 0).toFixed(1) }}%
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
</div> </div>
</div> </div>
</ElCard> </ElCard>
@@ -68,7 +68,7 @@
> >
<span class="text-sm">{{ (server.mem?.usage || 0).toFixed(1) }}%</span> <span class="text-sm">{{ (server.mem?.usage || 0).toFixed(1) }}%</span>
</ElProgress> </ElProgress>
<ElDescriptions :column="2" border size="small"> <FaDescriptions :column="2" size="small" :scrollbar="false">
<ElDescriptionsItem label="总内存"> <ElDescriptionsItem label="总内存">
{{ server.mem?.total }} {{ server.mem?.total }}
</ElDescriptionsItem> </ElDescriptionsItem>
@@ -81,7 +81,7 @@
<ElDescriptionsItem label="Python内存"> <ElDescriptionsItem label="Python内存">
{{ server.py?.memory_usage ? server.py.memory_usage.toFixed(1) + "%" : "-" }} {{ server.py?.memory_usage ? server.py.memory_usage.toFixed(1) + "%" : "-" }}
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
</div> </div>
</div> </div>
</ElCard> </ElCard>
@@ -98,7 +98,7 @@
<span class="font-medium">服务器基本信息</span> <span class="font-medium">服务器基本信息</span>
</div> </div>
</template> </template>
<ElDescriptions :column="2" border size="small"> <FaDescriptions :column="2" size="small" :scrollbar="false">
<ElDescriptionsItem label="服务器名称"> <ElDescriptionsItem label="服务器名称">
{{ server.sys?.computer_name || "-" }} {{ server.sys?.computer_name || "-" }}
</ElDescriptionsItem> </ElDescriptionsItem>
@@ -111,7 +111,7 @@
<ElDescriptionsItem label="系统架构"> <ElDescriptionsItem label="系统架构">
{{ server.sys?.os_arch || "-" }} {{ server.sys?.os_arch || "-" }}
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
</ElCard> </ElCard>
</ElCol> </ElCol>
@@ -124,7 +124,7 @@
<span class="font-medium">Python运行环境</span> <span class="font-medium">Python运行环境</span>
</div> </div>
</template> </template>
<ElDescriptions :column="2" border size="small"> <FaDescriptions :column="2" size="small" :scrollbar="false">
<ElDescriptionsItem label="Python名称"> <ElDescriptionsItem label="Python名称">
{{ server.py?.name || "-" }} {{ server.py?.name || "-" }}
</ElDescriptionsItem> </ElDescriptionsItem>
@@ -143,7 +143,7 @@
<ElDescriptionsItem label="项目路径" :span="2"> <ElDescriptionsItem label="项目路径" :span="2">
{{ server.sys?.user_dir || "-" }} {{ server.sys?.user_dir || "-" }}
</ElDescriptionsItem> </ElDescriptionsItem>
</ElDescriptions> </FaDescriptions>
</ElCard> </ElCard>
</ElCol> </ElCol>
</ElRow> </ElRow>
@@ -186,6 +186,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
import ServerAPI, { type ServerInfo } from "@/api/module_monitor/server"; import ServerAPI, { type ServerInfo } from "@/api/module_monitor/server";
defineOptions({ name: "ServerMonitor" }); defineOptions({ name: "ServerMonitor" });
@@ -1,4 +1,4 @@
<!-- 部门配置:Art + 树形表格(对齐 system/menu 模版) --> <!-- 部门配置:FA + 树形表格(对齐 system/menu 模版) -->
<template> <template>
<div class="fa-full-height"> <div class="fa-full-height">
<FaSearchBar <FaSearchBar
@@ -63,97 +63,58 @@
width="640px" width="640px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="4" border> :column="4"
<ElDescriptionsItem label="部门名称" :span="2"> :data="detailFormData"
{{ detailFormData.name }} :items="deptDetailItems"
</ElDescriptionsItem> max-height="75vh"
<ElDescriptionsItem label="部门编码" :span="2"> />
{{ detailFormData.code }}
</ElDescriptionsItem>
<ElDescriptionsItem label="上级部门" :span="2">
{{ detailFormData.parent_name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2">
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'">
{{ detailFormData.status === "0" ? "启用" : "停用" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="排序" :span="2">
{{ detailFormData.order }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="4">
{{ detailFormData.description }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm :key="deptFormRenderKey"
:key="deptFormRenderKey" scrollbar
ref="dataFormRef" max-height="75vh"
v-model="formData" ref="dataFormRef"
:items="deptDialogFormItems" v-model="formData"
:rules="rules" :items="deptDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="'auto'" label-suffix=":"
label-position="right" :label-width="'auto'"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #status> >
<ElRadioGroup v-model="formData.status"> <template #status>
<ElRadio value="0">启用</ElRadio> <ElRadioGroup v-model="formData.status">
<ElRadio value="1">停用</ElRadio> <ElRadio value="0">启用</ElRadio>
</ElRadioGroup> <ElRadio value="1">停用</ElRadio>
</template> </ElRadioGroup>
</FaForm> </template>
</ElScrollbar> </FaForm>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, ref, reactive, computed, nextTick, onMounted } from "vue";
import { useTableColumns } from "@/hooks/core/useTableColumns"; import { useTableColumns } from "@/hooks/core/useTableColumns";
import DeptAPI, { import DeptAPI, {
type DeptForm, type DeptForm,
type DeptPageQuery, type DeptPageQuery,
type DeptTable, type DeptTable,
} from "@/api/module_system/dept"; } from "@/api/module_system/dept";
import FaTable from "@/components/tables/fa-table/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { useUserStore } from "@stores"; import { useUserStore } from "@stores";
import { formatTree } from "@utils/common"; import { formatTree } from "@utils/common";
@@ -368,6 +329,24 @@ const { columnChecks, columns } = useTableColumns<DeptTable>(() => [
const detailFormData = ref<DeptTable>({ code: "" }); const detailFormData = ref<DeptTable>({ code: "" });
const deptDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "部门名称", prop: "name" },
{ label: "部门编码", prop: "code" },
{ label: "上级部门", prop: "parent_name" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "排序", prop: "order" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
const formData = ref<DeptForm>({ const formData = ref<DeptForm>({
id: undefined, id: undefined,
name: undefined, name: undefined,
@@ -411,6 +390,7 @@ const initialFormData: DeptForm = {
}; };
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const deptFormRenderKey = ref(0); const deptFormRenderKey = ref(0);
const deptDialogFormItems = computed<FormItem[]>(() => [ const deptDialogFormItems = computed<FormItem[]>(() => [
@@ -494,8 +474,8 @@ function onResetSearch() {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
} }
@@ -42,7 +42,7 @@
:perm-patch="['module_system:dict_data:patch']" :perm-patch="['module_system:dict_data:patch']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="handleOpenDialog('create')" @add="handleOpenDialog('create')"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="handleMoreClick" @more="handleMoreClick"
/> />
@@ -67,167 +67,125 @@
width="720px" width="720px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="2" border> :column="2"
<ElDescriptionsItem label="数据标签" :span="2"> :data="detailFormData"
{{ detailFormData.dict_label }} :items="dictDataDetailItems"
</ElDescriptionsItem> max-height="70vh"
<ElDescriptionsItem label="数据类型" :span="2"> />
{{ detailFormData.dict_type }}
</ElDescriptionsItem>
<ElDescriptionsItem label="数据值" :span="2">
{{ detailFormData.dict_value }}
</ElDescriptionsItem>
<ElDescriptionsItem label="样式属性" :span="2">
{{ detailFormData.css_class }}
</ElDescriptionsItem>
<ElDescriptionsItem label="列表样式属性" :span="2">
{{ detailFormData.list_class }}
</ElDescriptionsItem>
<ElDescriptionsItem label="是否默认" :span="2">
<ElTag v-if="detailFormData.is_default" type="success">是</ElTag>
<ElTag v-else type="danger">否</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2">
<ElTag v-if="detailFormData.status === '0'" type="success">启用</ElTag>
<ElTag v-else type="danger">停用</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="排序" :span="2">
{{ detailFormData.dict_sort }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="2">
{{ detailFormData.description }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm :key="dictDataFormRenderKey"
:key="dictDataFormRenderKey" scrollbar
ref="dataFormRef" max-height="70vh"
v-model="formData" ref="dataFormRef"
:items="dictDataDialogFormItems" v-model="formData"
:rules="rules" :items="dictDataDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="'auto'" label-suffix=":"
label-position="right" :label-width="'auto'"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #css_class> >
<ElSelect <template #css_class>
v-model="formData.css_class" <ElSelect
placeholder="请选择常用颜色或输入自定义" v-model="formData.css_class"
clearable placeholder="请选择常用颜色或输入自定义"
filterable clearable
allow-create filterable
default-first-option allow-create
> default-first-option
<ElOption value="primary" label="主要(primary)"> >
<span class="tag-option-preview" :style="getTagPreviewStyle('primary')"> <ElOption value="primary" label="主要(primary)">
主要(primary) <span class="tag-option-preview" :style="getTagPreviewStyle('primary')">
</span> 主要(primary)
</ElOption> </span>
<ElOption value="success" label="成功(success)"> </ElOption>
<span class="tag-option-preview" :style="getTagPreviewStyle('success')"> <ElOption value="success" label="成功(success)">
成功(success) <span class="tag-option-preview" :style="getTagPreviewStyle('success')">
</span> 成功(success)
</ElOption> </span>
<ElOption value="warning" label="警告(warning)"> </ElOption>
<span class="tag-option-preview" :style="getTagPreviewStyle('warning')"> <ElOption value="warning" label="警告(warning)">
警告(warning) <span class="tag-option-preview" :style="getTagPreviewStyle('warning')">
</span> 警告(warning)
</ElOption> </span>
<ElOption value="danger" label="危险(danger)"> </ElOption>
<span class="tag-option-preview" :style="getTagPreviewStyle('danger')"> <ElOption value="danger" label="危险(danger)">
危险(danger) <span class="tag-option-preview" :style="getTagPreviewStyle('danger')">
</span> 危险(danger)
</ElOption> </span>
<ElOption value="info" label="信息(info)"> </ElOption>
<span class="tag-option-preview" :style="getTagPreviewStyle('info')"> <ElOption value="info" label="信息(info)">
信息(info) <span class="tag-option-preview" :style="getTagPreviewStyle('info')">
</span> 信息(info)
</ElOption> </span>
</ElSelect> </ElOption>
</template> </ElSelect>
<template #list_class> </template>
<ElSelect v-model="formData.list_class" placeholder="请选择列表类样式" clearable> <template #list_class>
<ElOption value="default" label="默认(default)"> <ElSelect v-model="formData.list_class" placeholder="请选择列表类样式" clearable>
<span class="tag-option-preview tag-option-preview--default"> <ElOption value="default" label="默认(default)">
默认(default) <span class="tag-option-preview tag-option-preview--default">默认(default)</span>
</span> </ElOption>
</ElOption> <ElOption value="primary" label="主要(primary)">
<ElOption value="primary" label="主要(primary)"> <span class="tag-option-preview" :style="getTagPreviewStyle('primary')">
<span class="tag-option-preview" :style="getTagPreviewStyle('primary')"> 主要(primary)
主要(primary) </span>
</span> </ElOption>
</ElOption> <ElOption value="success" label="成功(success)">
<ElOption value="success" label="成功(success)"> <span class="tag-option-preview" :style="getTagPreviewStyle('success')">
<span class="tag-option-preview" :style="getTagPreviewStyle('success')"> 成功(success)
成功(success) </span>
</span> </ElOption>
</ElOption> <ElOption value="warning" label="警告(warning)">
<ElOption value="warning" label="警告(warning)"> <span class="tag-option-preview" :style="getTagPreviewStyle('warning')">
<span class="tag-option-preview" :style="getTagPreviewStyle('warning')"> 警告(warning)
警告(warning) </span>
</span> </ElOption>
</ElOption> <ElOption value="danger" label="危险(danger)">
<ElOption value="danger" label="危险(danger)"> <span class="tag-option-preview" :style="getTagPreviewStyle('danger')">
<span class="tag-option-preview" :style="getTagPreviewStyle('danger')"> 危险(danger)
危险(danger) </span>
</span> </ElOption>
</ElOption> <ElOption value="info" label="信息(info)">
<ElOption value="info" label="信息(info)"> <span class="tag-option-preview" :style="getTagPreviewStyle('info')">
<span class="tag-option-preview" :style="getTagPreviewStyle('info')"> 信息(info)
信息(info) </span>
</span> </ElOption>
</ElOption> </ElSelect>
</ElSelect> </template>
</template> <template #is_default>
<template #is_default> <ElRadioGroup v-model="formData.is_default">
<ElRadioGroup v-model="formData.is_default"> <ElRadio :value="true">是</ElRadio>
<ElRadio :value="true">是</ElRadio> <ElRadio :value="false">否</ElRadio>
<ElRadio :value="false">否</ElRadio> </ElRadioGroup>
</ElRadioGroup> </template>
</template> <template #status>
<template #status> <ElSwitch
<ElSwitch v-model="formData.status"
v-model="formData.status" inline-prompt
inline-prompt :active-value="'0'"
:active-value="'0'" :inactive-value="'1'"
:inactive-value="'1'" />
/> </template>
</template> </FaForm>
</FaForm>
</ElScrollbar>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="dictDataExportContentConfig" :content-config="dictDataExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -238,16 +196,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IObject } from "@/components/modal/types"; import type { IObject } from "@/components/modal/types";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaDrawer from "@/components/modal/fa-drawer/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import DictAPI, { import DictAPI, {
@@ -255,7 +207,6 @@ import DictAPI, {
type DictDataPageQuery, type DictDataPageQuery,
type DictDataTable, type DictDataTable,
} from "@/api/module_system/dict"; } from "@/api/module_system/dict";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
import { useDictStore } from "@stores"; import { useDictStore } from "@stores";
@@ -542,6 +493,33 @@ const dialogVisible = reactive({
const detailFormData = ref<DictDataTable>({}); const detailFormData = ref<DictDataTable>({});
const dictDataDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "数据标签", prop: "dict_label" },
{ label: "数据类型", prop: "dict_type" },
{ label: "数据值", prop: "dict_value" },
{ label: "样式属性", prop: "css_class" },
{ label: "列表样式属性", prop: "list_class" },
{
label: "是否默认",
prop: "is_default",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "排序", prop: "dict_sort" },
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<DictDataForm>({ const formData = ref<DictDataForm>({
id: undefined, id: undefined,
dict_sort: 1, dict_sort: 1,
@@ -566,6 +544,7 @@ const rules = reactive({
}); });
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const dictDataFormRenderKey = ref(0); const dictDataFormRenderKey = ref(0);
const dictDataDialogFormItems = computed<FormItem[]>(() => [ const dictDataDialogFormItems = computed<FormItem[]>(() => [
@@ -662,7 +641,7 @@ const initialFormData: DictDataForm = {
dict_type_id: props.dictTypeId, dict_type_id: props.dictTypeId,
}; };
const exportModalVisible = ref(false); const { exportVisible, openExport } = useImportExport();
async function handleSearchBarSearch(params: DictDataSearchForm) { async function handleSearchBarSearch(params: DictDataSearchForm) {
await searchBarRef.value?.validate?.(); await searchBarRef.value?.validate?.();
@@ -689,8 +668,8 @@ function onResetSearch() {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, { Object.assign(formData, {
...initialFormData, ...initialFormData,
dict_type_id: props.dictTypeId, dict_type_id: props.dictTypeId,
@@ -848,10 +827,6 @@ async function handleMoreClick(status: string) {
// 用户取消 // 用户取消
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -37,7 +37,7 @@
:perm-patch="['module_system:dict_type:patch']" :perm-patch="['module_system:dict_type:patch']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="handleOpenDialog('create')" @add="handleOpenDialog('create')"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="handleMoreClick" @more="handleMoreClick"
/> />
@@ -62,73 +62,53 @@
width="640px" width="640px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="2" border> :column="2"
<ElDescriptionsItem label="字典名称" :span="2"> :data="detailFormData"
{{ detailFormData.dict_name }} :items="dictDetailItems"
</ElDescriptionsItem> max-height="70vh"
<ElDescriptionsItem label="字典类型" :span="2"> >
<ElTag type="primary">{{ detailFormData.dict_type }}</ElTag> <template #dict_type="{ row }">
</ElDescriptionsItem> <ElTag type="primary">{{ row?.dict_type }}</ElTag>
<ElDescriptionsItem label="状态" :span="2"> </template>
<ElTag v-if="detailFormData.status === '0'" type="success">启用</ElTag> </FaDescriptions>
<ElTag v-else type="danger">停用</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="2">
{{ detailFormData.description }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm :key="dictFormRenderKey"
:key="dictFormRenderKey" scrollbar
ref="dataFormRef" max-height="70vh"
v-model="formData" ref="dataFormRef"
:items="dictDialogFormItems" v-model="formData"
:rules="rules" :items="dictDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="'auto'" label-suffix=":"
label-position="right" :label-width="'auto'"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #status> >
<ElRadioGroup v-model="formData.status"> <template #status>
<ElRadio value="0">启用</ElRadio> <ElRadioGroup v-model="formData.status">
<ElRadio value="1">停用</ElRadio> <ElRadio value="0">启用</ElRadio>
</ElRadioGroup> <ElRadio value="1">停用</ElRadio>
</template> </ElRadioGroup>
</FaForm> </template>
</ElScrollbar> </FaForm>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="dictTypeExportContentConfig" :content-config="dictTypeExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -146,15 +126,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IObject } from "@/components/modal/types"; import type { IObject } from "@/components/modal/types";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import DictAPI, { import DictAPI, {
@@ -162,7 +137,6 @@ import DictAPI, {
type DictPageQuery, type DictPageQuery,
type DictTable, type DictTable,
} from "@/api/module_system/dict"; } from "@/api/module_system/dict";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { useDictStore } from "@stores"; import { useDictStore } from "@stores";
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 { useAuth } from "@/hooks/core/useAuth";
@@ -364,6 +338,22 @@ const dialogVisible = reactive({
const detailFormData = ref<DictTable>({}); const detailFormData = ref<DictTable>({});
const dictDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "字典名称", prop: "dict_name" },
{ label: "字典类型", prop: "dict_type", slot: "dict_type" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<DictForm>({ const formData = ref<DictForm>({
id: undefined, id: undefined,
dict_name: "", dict_name: "",
@@ -379,6 +369,7 @@ const rules = reactive({
}); });
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const submitLoading = ref(false);
const dictFormRenderKey = ref(0); const dictFormRenderKey = ref(0);
const dictDialogFormItems = computed<FormItem[]>(() => [ const dictDialogFormItems = computed<FormItem[]>(() => [
@@ -426,7 +417,7 @@ const initialFormData: DictForm = {
description: undefined, description: undefined,
}; };
const exportModalVisible = ref(false); const { exportVisible, openExport } = useImportExport();
const drawerVisible = ref(false); const drawerVisible = ref(false);
const currentDictType = ref(""); const currentDictType = ref("");
@@ -511,8 +502,8 @@ function handleDictDataDrawer(dictTypeRow: DictTable) {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
} }
@@ -629,10 +620,6 @@ async function handleMoreClick(status: string) {
// 用户取消 // 用户取消
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -43,7 +43,7 @@
:perm-export="['module_system:log:export']" :perm-export="['module_system:log:export']"
:perm-delete="['module_system:log:delete']" :perm-delete="['module_system:log:delete']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
/> />
</template> </template>
@@ -67,77 +67,42 @@
width="960px" width="960px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" form-mode="detail"
@confirm="handleCloseDialog"
> >
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="8" border label-width="200px"> :column="8"
<ElDescriptionsItem label="日志类型" :span="2"> :data="formData"
<ElTag :type="formData.type === 1 ? 'success' : 'primary'"> :items="logDetailItems"
{{ formData.type === 1 ? "登录日志" : "操作日志" }} label-width="200px"
</ElTag> max-height="75vh"
</ElDescriptionsItem> >
<ElDescriptionsItem label="请求路径" :span="2"> <template #type="{ row }">
{{ formData.request_path }} <ElTag :type="row?.type === 1 ? 'success' : 'primary'">
</ElDescriptionsItem> {{ row?.type === 1 ? "登录日志" : "操作日志" }}
<ElDescriptionsItem label="请求方法" :span="2"> </ElTag>
<ElTag :type="getMethodType(formData.request_method)"> </template>
{{ formData.request_method }} <template #request_method="{ row }">
</ElTag> <ElTag :type="getMethodType(row?.request_method as string)">
</ElDescriptionsItem> {{ row?.request_method }}
<ElDescriptionsItem label="响应状态码" :span="2"> </ElTag>
<ElTag :type="getStatusCodeType(formData.response_code)"> </template>
{{ formData.response_code }} <template #response_code="{ row }">
</ElTag> <ElTag :type="getStatusCodeType(row?.response_code as number)">
</ElDescriptionsItem> {{ row?.response_code }}
<ElDescriptionsItem label="请求IP" :span="2"> </ElTag>
{{ formData.request_ip }} </template>
</ElDescriptionsItem> <template #request_payload="{ row }">
<ElDescriptionsItem label="处理时间" :span="2"> <JsonPretty :value="row?.request_payload as any" height="80px" />
{{ formData.process_time }} </template>
</ElDescriptionsItem> <template #response_json="{ row }">
<ElDescriptionsItem label="浏览器" :span="2"> <JsonPretty :value="row?.response_json as any" height="140px" />
{{ formData.request_browser }} </template>
</ElDescriptionsItem> </FaDescriptions>
<ElDescriptionsItem label="操作系统" :span="2">
{{ formData.request_os }}
</ElDescriptionsItem>
<ElDescriptionsItem label="请求参数" :span="4">
<JsonPretty :value="formData.request_payload" height="80px" />
</ElDescriptionsItem>
<ElDescriptionsItem label="响应数据" :span="4">
<JsonPretty :value="formData.response_json" height="140px" />
</ElDescriptionsItem>
<ElDescriptionsItem label="登录地点" :span="2">
{{ formData.login_location }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="4">
{{ formData.description }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建人" :span="2">
{{ formData.created_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新人" :span="2">
{{ formData.updated_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ formData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ formData.updated_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template>
</FaDialog> </FaDialog>
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="logExportContentConfig" :content-config="logExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -147,21 +112,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, nextTick } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IObject } from "@/components/modal/types"; import type { IObject } from "@/components/modal/types";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import JsonPretty from "@/components/others/fa-json-pretty/index.vue";
import CopyButton from "@/components/others/fa-copy-button/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 { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; 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";
@@ -400,6 +356,25 @@ const logExportContentConfig = computed(() => ({
const formData = ref<LogTable>({}); const formData = ref<LogTable>({});
const logDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] = [
{ label: "日志类型", prop: "type", slot: "type" },
{ label: "请求路径", prop: "request_path" },
{ label: "请求方法", prop: "request_method", slot: "request_method" },
{ label: "响应状态码", prop: "response_code", slot: "response_code" },
{ label: "请求IP", prop: "request_ip" },
{ label: "处理时间", prop: "process_time" },
{ label: "浏览器", prop: "request_browser" },
{ label: "操作系统", prop: "request_os" },
{ label: "请求参数", prop: "request_payload", slot: "request_payload", span: 4 },
{ label: "响应数据", prop: "response_json", slot: "response_json", span: 4 },
{ label: "登录地点", prop: "login_location" },
{ label: "描述", prop: "description", span: 4 },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const dialogVisible = ref({ const dialogVisible = ref({
title: "", title: "",
visible: false, visible: false,
@@ -421,7 +396,7 @@ function getMethodType(method?: string) {
return "info"; return "info";
} }
const exportModalVisible = ref(false); const { exportVisible, openExport } = useImportExport();
async function handleSearchBarSearch(params: LogSearchForm) { async function handleSearchBarSearch(params: LogSearchForm) {
await searchBarRef.value?.validate?.(); await searchBarRef.value?.validate?.();
@@ -533,8 +508,4 @@ async function handleBatchDelete() {
batchDeleting.value = false; batchDeleting.value = false;
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
</script> </script>
@@ -71,122 +71,57 @@
v-model="dialogVisible.visible" v-model="dialogVisible.visible"
:title="dialogVisible.title" :title="dialogVisible.title"
:size="drawerSize" :size="drawerSize"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<!-- 详情 --> <!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElDescriptions :column="4" border> <FaDescriptions
<ElDescriptionsItem label="编号" :span="2"> :column="4"
{{ detailFormData.id }} :data="detailFormData"
</ElDescriptionsItem> :items="menuDetailItems"
<ElDescriptionsItem label="菜单名称" :span="2"> :scrollbar="false"
{{ detailFormData.name }} >
</ElDescriptionsItem> <template #type="{ row }">
<ElDescriptionsItem label="菜单类型" :span="2"> <ElTag v-if="row?.type === MenuTypeEnum.CATALOG" type="warning">目录</ElTag>
<ElTag v-if="detailFormData.type === MenuTypeEnum.CATALOG" type="warning">目录</ElTag> <ElTag v-if="row?.type === MenuTypeEnum.MENU" type="success">菜单</ElTag>
<ElTag v-if="detailFormData.type === MenuTypeEnum.MENU" type="success">菜单</ElTag> <ElTag v-if="row?.type === MenuTypeEnum.BUTTON" type="danger">按钮</ElTag>
<ElTag v-if="detailFormData.type === MenuTypeEnum.BUTTON" type="danger">按钮</ElTag> <ElTag v-if="row?.type === MenuTypeEnum.EXTLINK" type="info">外链</ElTag>
<ElTag v-if="detailFormData.type === MenuTypeEnum.EXTLINK" type="info">外链</ElTag> </template>
</ElDescriptionsItem> <template #client="{ row }">
<ElDescriptionsItem label="终端" :span="2"> <ElTag v-if="row?.client === MenuClientEnum.PC" type="primary">PC</ElTag>
<ElTag v-if="detailFormData.client === MenuClientEnum.PC" type="primary">PC</ElTag> <ElTag v-else-if="row?.client === MenuClientEnum.APP" type="success">APP</ElTag>
<ElTag v-else-if="detailFormData.client === MenuClientEnum.APP" type="success"> <ElTag v-else type="info">{{ row?.client || "—" }}</ElTag>
APP </template>
</ElTag> <template #icon="{ row }">
<ElTag v-else type="info">{{ detailFormData.client || "—" }}</ElTag> <template v-if="row?.icon">
</ElDescriptionsItem> <MenuRouteIcon :icon="row?.icon as string" :style="'vertical-align: -0.15em'" />
<ElDescriptionsItem label="图标" :span="2">
<template #default>
<template v-if="detailFormData.icon">
<MenuRouteIcon :icon="detailFormData.icon" :style="'vertical-align: -0.15em'" />
</template>
</template> </template>
</ElDescriptionsItem> </template>
<ElDescriptionsItem label="排序" :span="2"> </FaDescriptions>
{{ detailFormData.order }}
</ElDescriptionsItem>
<ElDescriptionsItem label="权限标识" :span="2">
{{ detailFormData.permission }}
</ElDescriptionsItem>
<ElDescriptionsItem label="路由名称" :span="2">
{{ detailFormData.route_name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="路由路径" :span="2">
{{ detailFormData.route_path }}
</ElDescriptionsItem>
<ElDescriptionsItem label="组件路径" :span="2">
{{ detailFormData.component_path }}
</ElDescriptionsItem>
<ElDescriptionsItem label="重定向" :span="2">
{{ detailFormData.redirect }}
</ElDescriptionsItem>
<ElDescriptionsItem label="父级编号" :span="2">
{{ detailFormData.parent_id }}
</ElDescriptionsItem>
<ElDescriptionsItem label="父级菜单" :span="2">
{{ detailFormData.parent_name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="是否缓存" :span="2">
<ElTag :type="detailFormData.keep_alive ? 'success' : 'danger'">
{{ detailFormData.keep_alive ? "是" : "否" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="是否显示" :span="2">
<ElTag :type="detailFormData.hidden ? 'success' : 'danger'">
{{ detailFormData.hidden ? "是" : "否" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="是否显示根路由" :span="2">
<ElTag :type="detailFormData.always_show ? 'success' : 'danger'">
{{ detailFormData.always_show ? "是" : "否" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="菜单标题" :span="2">
{{ detailFormData.title }}
</ElDescriptionsItem>
<ElDescriptionsItem label="路由参数" :span="2">
{{ detailFormData.params }}
</ElDescriptionsItem>
<ElDescriptionsItem label="是否固定路由" :span="2">
<ElTag :type="detailFormData.affix ? 'success' : 'danger'">
{{ detailFormData.affix ? "是" : "否" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2">
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'">
{{ detailFormData.status === "0" ? "启用" : "停用" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="排序" :span="2">
{{ detailFormData.order }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="4">
{{ detailFormData.description }}
</ElDescriptionsItem>
</ElDescriptions>
</template> </template>
<!-- 新增、编辑表单 --> <!-- 新增、编辑表单 -->
<template v-else> <template v-else>
<ElForm <FaForm
:key="menuFormRenderKey"
ref="dataFormRef" ref="dataFormRef"
:model="formData" v-model="formData"
:items="menuDialogFormItems"
:rules="rules" :rules="rules"
label-suffix=":" label-suffix=":"
label-width="100px" :label-width="'auto'"
label-position="right" label-position="right"
:span="24"
:gutter="16"
:show-reset="false"
:show-submit="false"
class="crud-dialog-art-form"
> >
<ElFormItem <!-- 父级菜单(条件显示) -->
v-if="formData.type !== MenuTypeEnum.CATALOG" <template v-if="formData.type !== MenuTypeEnum.CATALOG" #parent_id>
label="父级菜单"
prop="parent_id"
>
<ElTreeSelect <ElTreeSelect
v-model="formData.parent_id" v-model="formData.parent_id"
placeholder="选择上级菜单" placeholder="选择上级菜单"
@@ -199,17 +134,10 @@
<ElText v-if="createParentLocked" type="info" size="small" class="block mt-1"> <ElText v-if="createParentLocked" type="info" size="small" class="block mt-1">
在菜单下仅可新增按钮,父级已固定 在菜单下仅可新增按钮,父级已固定
</ElText> </ElText>
</ElFormItem> </template>
<ElFormItem label="菜单名称" prop="name"> <!-- 菜单类型(动态枚举按钮组) -->
<ElInput v-model="formData.name" placeholder="请输入菜单名称" /> <template #type>
</ElFormItem>
<ElFormItem label="菜单标题" prop="title">
<ElInput v-model="formData.title" placeholder="请输入菜单标题" />
</ElFormItem>
<ElFormItem label="菜单类型" prop="type">
<ElRadioGroup v-model="formData.type" @change="handleMenuTypeChange"> <ElRadioGroup v-model="formData.type" @change="handleMenuTypeChange">
<ElRadio <ElRadio
v-if="allowedMenuTypeValues.includes(MenuTypeEnum.CATALOG)" v-if="allowedMenuTypeValues.includes(MenuTypeEnum.CATALOG)"
@@ -236,9 +164,10 @@
外链 外链
</ElRadio> </ElRadio>
</ElRadioGroup> </ElRadioGroup>
</ElFormItem> </template>
<ElFormItem label="终端" prop="client"> <!-- 终端(禁用态+提示) -->
<template #client>
<ElRadioGroup v-model="formData.client" :disabled="createParentLocked"> <ElRadioGroup v-model="formData.client" :disabled="createParentLocked">
<ElRadio :value="MenuClientEnum.PC">PC 桌面</ElRadio> <ElRadio :value="MenuClientEnum.PC">PC 桌面</ElRadio>
<ElRadio :value="MenuClientEnum.APP">APP 移动</ElRadio> <ElRadio :value="MenuClientEnum.APP">APP 移动</ElRadio>
@@ -246,91 +175,41 @@
<ElText v-if="createParentLocked" type="info" size="small" class="block mt-1"> <ElText v-if="createParentLocked" type="info" size="small" class="block mt-1">
子级终端与父菜单一致 子级终端与父菜单一致
</ElText> </ElText>
</ElFormItem> </template>
<ElFormItem v-if="formData.type == MenuTypeEnum.EXTLINK" label="外链地址" prop="path"> <!-- 外链地址(条件显示) -->
<template v-if="formData.type == MenuTypeEnum.EXTLINK" #extlink_path>
<ElInput v-model="formData.route_path" placeholder="请输入外链完整路径" /> <ElInput v-model="formData.route_path" placeholder="请输入外链完整路径" />
</ElFormItem> </template>
<ElFormItem v-if="formData.type !== MenuTypeEnum.BUTTON" prop="route_name"> <!-- 路由名称(Tooltip标签) -->
<template #label> <template v-if="formData.type !== MenuTypeEnum.BUTTON" #route_name>
<div class="flex-y-center">
路由名称
<ElTooltip placement="bottom" effect="light">
<template #content>
如果需要开启缓存,需保证页面 defineOptions 中的 name 与此处一致,建议使用驼峰。
</template>
<ElIcon class="ml-1 cursor-pointer">
<QuestionFilled />
</ElIcon>
</ElTooltip>
</div>
</template>
<ElInput v-model="formData.route_name" placeholder="请输入路由名称" /> <ElInput v-model="formData.route_name" placeholder="请输入路由名称" />
</ElFormItem> </template>
<ElFormItem <!-- 路由路径(Tooltip标签) -->
<template
v-if="formData.type == MenuTypeEnum.CATALOG || formData.type == MenuTypeEnum.MENU" v-if="formData.type == MenuTypeEnum.CATALOG || formData.type == MenuTypeEnum.MENU"
prop="route_path" #route_path
> >
<template #label>
<div class="flex-y-center">
路由路径
<ElTooltip placement="bottom" effect="light">
<template #content>
定义应用中不同页面对应的 URL 路径,目录需以 /
开头,菜单项不用。例如:系统管理目录 /system,系统管理下的用户管理菜单 user。
</template>
<ElIcon class="ml-1 cursor-pointer">
<QuestionFilled />
</ElIcon>
</ElTooltip>
</div>
</template>
<ElInput v-model="formData.route_path" placeholder="请输入路由路径,如:/system" /> <ElInput v-model="formData.route_path" placeholder="请输入路由路径,如:/system" />
</ElFormItem> </template>
<ElFormItem v-if="formData.type == MenuTypeEnum.MENU" prop="component">
<template #label>
<div class="flex-y-center">
组件路径
<ElTooltip placement="bottom" effect="light">
<template #content>
组件页面完整路径,相对于 src/views/,如 system/user/index,缺省后缀 .vue
</template>
<ElIcon class="ml-1 cursor-pointer">
<QuestionFilled />
</ElIcon>
</ElTooltip>
</div>
</template>
<!-- 组件路径(Tooltip+prepend/append) -->
<template v-if="formData.type == MenuTypeEnum.MENU" #component_path>
<ElInput <ElInput
v-model="formData.component_path" v-model="formData.component_path"
placeholder="请输入组件路径,如system/user/index" placeholder="请输入组件路径,如system/user/index"
:style="'width: 95%'" :style="'width: 95%'"
> >
<template v-if="formData.type == MenuTypeEnum.MENU" #prepend>src/views/</template> <template #prepend>src/views/</template>
<template v-if="formData.type == MenuTypeEnum.MENU" #append>.vue</template> <template #append>.vue</template>
</ElInput> </ElInput>
</ElFormItem> </template>
<ElFormItem v-if="formData.type == MenuTypeEnum.MENU"> <!-- 路由参数(动态键值编辑器) -->
<template #label> <template v-if="formData.type == MenuTypeEnum.MENU" #params>
<div class="flex-y-center"> <template
路由参数
<ElTooltip placement="bottom" effect="light">
<template #content>
组件页面使用 `useRoute().query.参数名` 获取路由参数值。
</template>
<ElIcon class="ml-1 cursor-pointer">
<QuestionFilled />
</ElIcon>
</ElTooltip>
</div>
</template>
<div
v-if=" v-if="
!formData.params || (Array.isArray(formData.params) && formData.params.length === 0) !formData.params || (Array.isArray(formData.params) && formData.params.length === 0)
" "
@@ -338,16 +217,12 @@
<ElButton type="success" plain @click="formData.params = [{ key: '', value: '' }]"> <ElButton type="success" plain @click="formData.params = [{ key: '', value: '' }]">
添加路由参数 添加路由参数
</ElButton> </ElButton>
</div> </template>
<template v-else>
<div v-else>
<div v-for="(item, index) in formData.params" :key="index"> <div v-for="(item, index) in formData.params" :key="index">
<ElInput v-model="item.key" placeholder="参数名" :style="'width: 100px'" /> <ElInput v-model="item.key" placeholder="参数名" :style="'width: 100px'" />
<span class="mx-1">=</span> <span class="mx-1">=</span>
<ElInput v-model="item.value" placeholder="参数值" :style="'width: 100px'" /> <ElInput v-model="item.value" placeholder="参数值" :style="'width: 100px'" />
<ElIcon <ElIcon
v-if="formData.params.indexOf(item) === formData.params.length - 1" v-if="formData.params.indexOf(item) === formData.params.length - 1"
class="ml-2 cursor-pointer color-[var(--el-color-success)]" class="ml-2 cursor-pointer color-[var(--el-color-success)]"
@@ -364,90 +239,52 @@
<DeleteFilled /> <DeleteFilled />
</ElIcon> </ElIcon>
</div> </div>
</div>
</ElFormItem>
<ElFormItem v-if="formData.type !== MenuTypeEnum.BUTTON">
<template #label>
<div class="flex-y-center">
是否隐藏
<ElTooltip placement="bottom" effect="light">
<template #content>
选择"是", 菜单中隐藏
<br />
选择"否",菜单中显示。
<br />
</template>
<ElIcon class="ml-1 cursor-pointer">
<QuestionFilled />
</ElIcon>
</ElTooltip>
</div>
</template> </template>
</template>
<!-- 是否隐藏(Tooltip标签) -->
<template v-if="formData.type !== MenuTypeEnum.BUTTON" #hidden>
<ElRadioGroup v-model="formData.hidden"> <ElRadioGroup v-model="formData.hidden">
<ElRadio :value="true">是</ElRadio> <ElRadio :value="true">是</ElRadio>
<ElRadio :value="false">否</ElRadio> <ElRadio :value="false">否</ElRadio>
</ElRadioGroup> </ElRadioGroup>
</ElFormItem> </template>
<ElFormItem <!-- 始终显示(Tooltip标签) -->
<template
v-if="formData.type === MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU" v-if="formData.type === MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU"
#always_show
> >
<template #label>
<div class="flex-y-center">
始终显示
<ElTooltip placement="bottom" effect="light">
<template #content>
选择"是",即使目录或菜单下只有一个子节点,也会显示父节点。
<br />
选择"否",如果目录或菜单下只有一个子节点,则只显示该子节点,隐藏父节点。
<br />
如果是叶子节点,请选择"否"。
</template>
<ElIcon class="ml-1 cursor-pointer">
<QuestionFilled />
</ElIcon>
</ElTooltip>
</div>
</template>
<ElRadioGroup v-model="formData.always_show"> <ElRadioGroup v-model="formData.always_show">
<ElRadio :value="true">是</ElRadio> <ElRadio :value="true">是</ElRadio>
<ElRadio :value="false">否</ElRadio> <ElRadio :value="false">否</ElRadio>
</ElRadioGroup> </ElRadioGroup>
</ElFormItem> </template>
<ElFormItem v-if="formData.type === MenuTypeEnum.MENU" label="缓存页面"> <!-- 缓存页面(v-if) -->
<template v-if="formData.type === MenuTypeEnum.MENU" #keep_alive>
<ElRadioGroup v-model="formData.keep_alive"> <ElRadioGroup v-model="formData.keep_alive">
<ElRadio :value="true">开启</ElRadio> <ElRadio :value="true">开启</ElRadio>
<ElRadio :value="false">关闭</ElRadio> <ElRadio :value="false">关闭</ElRadio>
</ElRadioGroup> </ElRadioGroup>
</ElFormItem> </template>
<ElFormItem label="排序" prop="order"> <!-- 权限标识(v-if) -->
<ElInputNumber v-model="formData.order" controls-position="right" :min="1" /> <template
</ElFormItem>
<!-- 权限标识 -->
<ElFormItem
v-if="formData.type == MenuTypeEnum.BUTTON || formData.type === MenuTypeEnum.MENU" v-if="formData.type == MenuTypeEnum.BUTTON || formData.type === MenuTypeEnum.MENU"
label="权限标识" #permission
prop="perm"
> >
<ElInput v-model="formData.permission" placeholder="请输入权限标识,如sys:user:add" /> <ElInput v-model="formData.permission" placeholder="请输入权限标识,如sys:user:add" />
</ElFormItem> </template>
<ElFormItem v-if="formData.type !== MenuTypeEnum.BUTTON" label="图标" prop="icon"> <!-- 图标(v-if) -->
<!-- 图标选择器 --> <template v-if="formData.type !== MenuTypeEnum.BUTTON" #icon>
<FaIconSelect v-model="formData.icon" /> <FaIconSelect v-model="formData.icon" />
</ElFormItem> </template>
<ElFormItem <!-- 重定向(v-if, 动态placeholder) -->
<template
v-if="formData.type == MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU" v-if="formData.type == MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU"
label="重定向" #redirect
prop="redirect"
:required="formData.type === MenuTypeEnum.CATALOG"
> >
<ElInput <ElInput
v-model="formData.redirect" v-model="formData.redirect"
@@ -457,49 +294,16 @@
: '可选,请输入重定向路由' : '可选,请输入重定向路由'
" "
/> />
</ElFormItem> </template>
<ElFormItem v-if="formData.type != MenuTypeEnum.BUTTON" label="常驻标签栏" prop="affix"> <!-- 常驻标签栏(v-if) -->
<template v-if="formData.type != MenuTypeEnum.BUTTON" #affix>
<ElRadioGroup v-model="formData.affix"> <ElRadioGroup v-model="formData.affix">
<ElRadio :value="true">是</ElRadio> <ElRadio :value="true">是</ElRadio>
<ElRadio :value="false">否</ElRadio> <ElRadio :value="false">否</ElRadio>
</ElRadioGroup> </ElRadioGroup>
</ElFormItem> </template>
</FaForm>
<ElFormItem label="状态" prop="status">
<ElRadioGroup v-model="formData.status">
<ElRadio value="0">启用</ElRadio>
<ElRadio value="1">禁用</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput
v-model="formData.description"
:rows="4"
:maxlength="100"
show-word-limit
type="textarea"
placeholder="请输入描述"
/>
</ElFormItem>
</ElForm>
</template>
<template #footer>
<div class="dialog-footer">
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
<ElButton
v-if="dialogVisible.type !== 'detail'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
<ElButton @click="handleCloseDialog">取消</ElButton>
</div>
</template> </template>
</FaDrawer> </FaDrawer>
</div> </div>
@@ -511,8 +315,7 @@ defineOptions({
inheritAttrs: false, inheritAttrs: false,
}); });
import { h, ref, reactive, computed, nextTick, onMounted } from "vue"; import { CirclePlusFilled, DeleteFilled } from "@element-plus/icons-vue";
import { CirclePlusFilled, DeleteFilled, QuestionFilled } from "@element-plus/icons-vue";
import { useAppStore } from "@stores/modules/app.store"; import { useAppStore } from "@stores/modules/app.store";
import { useUserStore } from "@stores/modules/user.store"; import { useUserStore } from "@stores/modules/user.store";
import { DeviceEnum } from "@/enums/settings/device.enum"; import { DeviceEnum } from "@/enums/settings/device.enum";
@@ -524,15 +327,8 @@ import MenuAPI, {
} from "@/api/module_system/menu"; } from "@/api/module_system/menu";
import { MenuClientEnum, MenuTypeEnum } from "@/enums/system/menu.enum"; import { MenuClientEnum, MenuTypeEnum } from "@/enums/system/menu.enum";
import { formatTree } from "@utils/common"; import { formatTree } from "@utils/common";
import MenuRouteIcon from "@/components/others/fa-menu-routeIcon/index.vue";
import FaTable from "@/components/tables/fa-table/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDrawer from "@/components/modal/fa-drawer/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import FaIconSelect from "@/components/others/fa-icon-select/index.vue";
import { ElMessage, ElMessageBox, ElTag, ElTooltip } from "element-plus";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
@@ -697,6 +493,108 @@ const createParentLocked = ref(false);
const detailFormData = ref<MenuTable>({}); const detailFormData = ref<MenuTable>({});
const menuDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "编号", prop: "id" },
{ label: "菜单名称", prop: "name" },
{ label: "菜单类型", prop: "type", slot: "type" },
{ label: "终端", prop: "client", slot: "client" },
{ label: "图标", prop: "icon", slot: "icon" },
{ label: "权限标识", prop: "permission" },
{ label: "路由名称", prop: "route_name" },
{ label: "路由路径", prop: "route_path" },
{ label: "组件路径", prop: "component_path" },
{ label: "重定向", prop: "redirect" },
{ label: "父级编号", prop: "parent_id" },
{ label: "父级菜单", prop: "parent_name" },
{
label: "是否缓存",
prop: "keep_alive",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{
label: "是否显示",
prop: "hidden",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{
label: "是否显示根路由",
prop: "always_show",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{ label: "菜单标题", prop: "title" },
{ label: "路由参数", prop: "params" },
{
label: "是否固定路由",
prop: "affix",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "排序", prop: "order" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
// 菜单新增/编辑表单 —— 简单字段走 items,条件渲染/Tooltip标签/复杂控件走插槽
const menuDialogFormItems: FormItem[] = [
{ key: "parent_id", label: "父级菜单", type: "input" /* #parent_id 插槽 */ },
{ key: "name", label: "菜单名称", type: "input", props: { placeholder: "请输入菜单名称" } },
{ key: "title", label: "菜单标题", type: "input", props: { placeholder: "请输入菜单标题" } },
{ key: "type", label: "菜单类型", type: "input" /* #type 插槽 */ },
{ key: "client", label: "终端", type: "input" /* #client 插槽 */ },
{ key: "extlink_path", label: "外链地址", type: "input" /* #extlink_path 插槽 */ },
{ key: "route_name", label: "路由名称", type: "input" /* #route_name 插槽 */ },
{ key: "route_path", label: "路由路径", type: "input" /* #route_path 插槽 */ },
{ key: "component_path", label: "组件路径", type: "input" /* #component_path 插槽 */ },
{ key: "params", label: "路由参数", type: "input" /* #params 插槽 */ },
{ key: "hidden", label: "是否隐藏", type: "input" /* #hidden 插槽 */ },
{ key: "always_show", label: "始终显示", type: "input" /* #always_show 插槽 */ },
{ key: "keep_alive", label: "缓存页面", type: "input" /* #keep_alive 插槽 */ },
{ key: "order", label: "排序", type: "number", props: { controlsPosition: "right", min: 1 } },
{ key: "permission", label: "权限标识", type: "input" /* #permission 插槽 */ },
{ key: "icon", label: "图标", type: "input" /* #icon 插槽 */ },
{ key: "redirect", label: "重定向", type: "input" /* #redirect 插槽 */ },
{ key: "affix", label: "常驻标签栏", type: "input" /* #affix 插槽 */ },
{
key: "status",
label: "状态",
type: "radiogroup",
props: {
options: [
{ label: "启用", value: "0" },
{ label: "禁用", value: "1" },
],
},
},
{
key: "description",
label: "描述",
type: "input",
props: {
type: "textarea",
rows: 4,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
];
const formData = ref<MenuForm>({ const formData = ref<MenuForm>({
id: undefined, id: undefined,
name: undefined, name: undefined,
@@ -1004,7 +902,8 @@ const initialFormData: MenuForm = {
client: MenuClientEnum.PC, client: MenuClientEnum.PC,
}; };
const dataFormRef = ref(); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const menuFormRenderKey = ref(0);
async function resetForm() { async function resetForm() {
if (dataFormRef.value) { if (dataFormRef.value) {
@@ -1052,6 +951,7 @@ async function handleOpenDialog(
} else { } else {
dialogVisible.title = "新增菜单"; dialogVisible.title = "新增菜单";
formData.value = { ...initialFormData }; formData.value = { ...initialFormData };
menuFormRenderKey.value += 1;
if (parentRow?.id != null) { if (parentRow?.id != null) {
formData.value.parent_id = parentRow.id; formData.value.parent_id = parentRow.id;
formData.value.client = (parentRow.client as MenuClientEnum) || menuClientTab.value; formData.value.client = (parentRow.client as MenuClientEnum) || menuClientTab.value;
@@ -1075,7 +975,7 @@ function handleMenuTypeChange() {
nextTick(() => { nextTick(() => {
dataFormRef.value?.clearValidate("redirect"); dataFormRef.value?.clearValidate("redirect");
if (formData.value.type === MenuTypeEnum.CATALOG) { if (formData.value.type === MenuTypeEnum.CATALOG) {
dataFormRef.value?.validateField("redirect").catch(() => {}); dataFormRef.value?.validateField("redirect")?.catch(() => {});
} }
}); });
} }
@@ -46,7 +46,7 @@
:perm-patch="['module_system:notice:patch']" :perm-patch="['module_system:notice:patch']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="handleOpenDialog('create')" @add="handleOpenDialog('create')"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="handleMoreClick" @more="handleMoreClick"
/> />
@@ -71,105 +71,74 @@
width="920px" width="920px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="4" border label-width="120px"> :column="4"
<ElDescriptionsItem label="标题" :span="2"> :data="detailFormData"
{{ detailFormData.notice_title }} :items="noticeDetailItems"
</ElDescriptionsItem> label-width="120px"
<ElDescriptionsItem label="类型" :span="2"> max-height="75vh"
<ElTag :type="detailFormData.notice_type === '1' ? 'primary' : 'warning'"> >
{{ noticeTypeLabel(detailFormData.notice_type) }} <template #notice_type="{ row }">
</ElTag> <ElTag :type="row?.notice_type === '1' ? 'primary' : 'warning'">
</ElDescriptionsItem> {{ noticeTypeLabel(row?.notice_type as string) }}
<ElDescriptionsItem label="状态" :span="2"> </ElTag>
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'"> </template>
{{ detailFormData.status === "0" ? "启用" : "停用" }} <template #notice_content>
</ElTag> <div class="notice-html-preview">
</ElDescriptionsItem> <template v-if="detailHasRenderableContent">
<ElDescriptionsItem label="描述" :span="2"> <div v-html="detailContentHtml" />
{{ detailFormData.description }} </template>
</ElDescriptionsItem> <p v-else class="notice-html-empty">暂无内容</p>
<ElDescriptionsItem label="内容" :span="4"> </div>
<div class="notice-html-preview"> </template>
<template v-if="detailHasRenderableContent"> </FaDescriptions>
<div v-html="detailContentHtml" />
</template>
<p v-else class="notice-html-empty">暂无内容</p>
</div>
</ElDescriptionsItem>
<ElDescriptionsItem label="创建人" :span="2">
{{ detailFormData.created_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新人" :span="2">
{{ detailFormData.updated_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <!-- FaForm + items + 栅格;弹窗内关闭内置提交/重置,仍用底部按钮 -->
<!-- FaForm + items + 栅格;弹窗内关闭内置提交/重置,仍用底部按钮 --> <FaForm
<FaForm :key="noticeFormRenderKey"
:key="noticeFormRenderKey" scrollbar
ref="dataFormRef" max-height="75vh"
v-model="formData" ref="dataFormRef"
:items="noticeDialogFormItems" v-model="formData"
:rules="rules" :items="noticeDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="100" label-suffix=":"
label-position="right" :label-width="100"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #status> >
<ElRadioGroup v-model="formData.status"> <template #status>
<ElRadio value="0">启用</ElRadio> <ElRadioGroup v-model="formData.status">
<ElRadio value="1">停用</ElRadio> <ElRadio value="0">启用</ElRadio>
</ElRadioGroup> <ElRadio value="1">停用</ElRadio>
</template> </ElRadioGroup>
<template #notice_content> </template>
<FaWangEditor <template #notice_content>
:model-value="formData.notice_content ?? ''" <FaWangEditor
height="min(38vh, 280px)" :model-value="formData.notice_content ?? ''"
placeholder="请输入公告内容,支持完整排版与插入..." height="min(38vh, 280px)"
:exclude-keys="[]" placeholder="请输入公告内容,支持完整排版与插入..."
@update:model-value="(v: string) => (formData.notice_content = v)" :exclude-keys="[]"
/> @update:model-value="(v: string) => (formData.notice_content = v)"
</template> />
</FaForm> </template>
</ElScrollbar> </FaForm>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton
v-if="dialogVisible.type !== 'detail'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="noticeExportContentConfig" :content-config="noticeExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -179,30 +148,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive, nextTick, onMounted } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IObject } from "@/components/modal/types"; import type { IObject } from "@/components/modal/types";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import NoticeAPI, { import NoticeAPI, {
type NoticeForm, type NoticeForm,
type NoticePageQuery, type NoticePageQuery,
type NoticeTable, type NoticeTable,
} from "@/api/module_system/notice"; } from "@/api/module_system/notice";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
import { useDictStore, useNoticeStore } from "@stores/index"; import { useDictStore, useNoticeStore } from "@stores/index";
import UserTableSelect from "@views/module_system/user/components/UserTableSelect.vue"; import UserTableSelect from "@views/module_system/user/components/UserTableSelect.vue";
import FaWangEditor from "@/components/forms/fa-wang-editor/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
defineOptions({ defineOptions({
name: "Notice", name: "Notice",
@@ -436,6 +396,25 @@ const noticeExportContentConfig = computed(() => ({
const detailFormData = ref<NoticeTable>({}); const detailFormData = ref<NoticeTable>({});
const noticeDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "标题", prop: "notice_title" },
{ label: "类型", prop: "notice_type", slot: "notice_type" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "描述", prop: "description" },
{ label: "内容", prop: "notice_content", slot: "notice_content", span: 4 },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
/** 详情富文本 HTML(用于预览) */ /** 详情富文本 HTML(用于预览) */
const detailContentHtml = computed({ const detailContentHtml = computed({
get: () => detailFormData.value.notice_content ?? "", get: () => detailFormData.value.notice_content ?? "",
@@ -545,7 +524,7 @@ const initialFormData: NoticeForm = {
description: undefined, description: undefined,
}; };
const exportModalVisible = ref(false); const { exportVisible, openExport } = useImportExport();
function buildNoticeReplaceParams(p: NoticeSearchForm): Record<string, unknown> { function buildNoticeReplaceParams(p: NoticeSearchForm): Record<string, unknown> {
return { return {
@@ -743,10 +722,6 @@ async function handleMoreClick(status: string) {
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
onMounted(async () => { onMounted(async () => {
await dictStore.getDict(["sys_notice_type"]); await dictStore.getDict(["sys_notice_type"]);
}); });
@@ -36,7 +36,7 @@
:perm-delete="['module_system:param:delete']" :perm-delete="['module_system:param:delete']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="handleOpenDialog('create')" @add="handleOpenDialog('create')"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
/> />
</template> </template>
@@ -60,81 +60,49 @@
width="640px" width="640px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="4" border> :column="4"
<ElDescriptionsItem label="配置名称" :span="2"> :data="detailFormData"
{{ detailFormData.config_name }} :items="paramDetailItems"
</ElDescriptionsItem> max-height="75vh"
<ElDescriptionsItem label="系统内置" :span="2"> />
<ElTag v-if="detailFormData.config_type" type="success">是</ElTag>
<ElTag v-else type="danger">否</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="配置键" :span="2">
{{ detailFormData.config_key }}
</ElDescriptionsItem>
<ElDescriptionsItem label="配置值" :span="2">
{{ detailFormData.config_value }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="2">
{{ detailFormData.description }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm :key="paramFormRenderKey"
:key="paramFormRenderKey" scrollbar
ref="dataFormRef" max-height="75vh"
v-model="formData" ref="dataFormRef"
:items="paramDialogFormItems" v-model="formData"
:rules="rules" :items="paramDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="100" label-suffix=":"
label-position="right" :label-width="100"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #config_type> >
<ElRadioGroup v-model="formData.config_type"> <template #config_type>
<ElRadio :value="true">是</ElRadio> <ElRadioGroup v-model="formData.config_type">
<ElRadio :value="false">否</ElRadio> <ElRadio :value="true">是</ElRadio>
</ElRadioGroup> <ElRadio :value="false">否</ElRadio>
</template> </ElRadioGroup>
</FaForm> </template>
</ElScrollbar> </FaForm>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton
v-if="dialogVisible.type !== 'detail'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="paramExportContentConfig" :content-config="paramExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -144,17 +112,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IObject } from "@/components/modal/types"; import type { IObject } from "@/components/modal/types";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import ParamsAPI, { import ParamsAPI, {
@@ -162,7 +123,6 @@ import ParamsAPI, {
type ConfigPageQuery, type ConfigPageQuery,
type ConfigTable, type ConfigTable,
} from "@/api/module_system/params"; } from "@/api/module_system/params";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
import { useConfigStore } from "@stores"; import { useConfigStore } from "@stores";
@@ -362,6 +322,23 @@ const paramExportContentConfig = computed(() => ({
const detailFormData = ref<ConfigTable>({} as ConfigTable); const detailFormData = ref<ConfigTable>({} as ConfigTable);
const paramDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "配置名称", prop: "config_name" },
{
label: "系统内置",
prop: "config_type",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{ label: "配置键", prop: "config_key" },
{ label: "配置值", prop: "config_value" },
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
const formData = ref<ConfigForm>({ const formData = ref<ConfigForm>({
id: undefined, id: undefined,
config_name: "", config_name: "",
@@ -441,7 +418,7 @@ const initialFormData: ConfigForm = {
description: "", description: "",
}; };
const exportModalVisible = ref(false); const { exportVisible, openExport } = useImportExport();
async function handleSearchBarSearch(params: ParamSearchForm) { async function handleSearchBarSearch(params: ParamSearchForm) {
await searchBarRef.value?.validate?.(); await searchBarRef.value?.validate?.();
@@ -460,8 +437,8 @@ function onResetSearch() {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
} }
@@ -597,10 +574,6 @@ async function handleBatchDelete() {
batchDeleting.value = false; batchDeleting.value = false;
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -1,4 +1,4 @@
<!-- 岗位管理:Art + useTable;操作列前 3 个为 ArtButtonTable,其余收入「更多」下拉 --> <!-- 岗位管理:FA + useTable;操作列前 3 个为 ArtButtonTable,其余收入「更多」下拉 -->
<template> <template>
<div class="fa-full-height"> <div class="fa-full-height">
<FaSearchBar <FaSearchBar
@@ -46,7 +46,7 @@
:perm-patch="['module_system:position:patch']" :perm-patch="['module_system:position:patch']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="handleOpenDialog('create')" @add="handleOpenDialog('create')"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="handleMoreClick" @more="handleMoreClick"
/> />
@@ -71,84 +71,49 @@
width="720px" width="720px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="4" border> :column="4"
<ElDescriptionsItem label="岗位名称" :span="2"> :data="detailFormData"
{{ detailFormData.name }} :items="positionDetailItems"
</ElDescriptionsItem> max-height="75vh"
<ElDescriptionsItem label="排序" :span="2"> />
{{ detailFormData.order }}
</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2">
<ElTag v-if="detailFormData.status === '0'" type="success">启用</ElTag>
<ElTag v-else type="danger">停用</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="创建人" :span="2">
{{ detailFormData.created_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新人" :span="2">
{{ detailFormData.updated_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="4">
{{ detailFormData.description }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm scrollbar
:key="positionFormRenderKey" max-height="75vh"
ref="dataFormRef" :key="positionFormRenderKey"
v-model="formData" ref="dataFormRef"
:items="positionDialogFormItems" v-model="formData"
:rules="rules" :items="positionDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="'auto'" label-suffix=":"
label-position="right" :label-width="'auto'"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #status> >
<ElRadioGroup v-model="formData.status"> <template #status>
<ElRadio value="0">启用</ElRadio> <ElRadioGroup v-model="formData.status">
<ElRadio value="1">停用</ElRadio> <ElRadio value="0">启用</ElRadio>
</ElRadioGroup> <ElRadio value="1">停用</ElRadio>
</template> </ElRadioGroup>
</FaForm> </template>
</ElScrollbar> </FaForm>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton
v-if="dialogVisible.type !== 'detail'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="positionExportContentConfig" :content-config="positionExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -158,18 +123,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive, nextTick } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import ArtButtonTable from "@/components/forms/fa-button-table/index.vue";
import type { IObject } from "@/components/modal/types"; import type { IObject } from "@/components/modal/types";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import PositionAPI, { import PositionAPI, {
@@ -177,15 +134,6 @@ import PositionAPI, {
type PositionPageQuery, type PositionPageQuery,
type PositionTable, type PositionTable,
} from "@/api/module_system/position"; } from "@/api/module_system/position";
import {
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 "@stores"; import { useUserStore } from "@stores";
import UserTableSelect from "@views/module_system/user/components/UserTableSelect.vue"; import UserTableSelect from "@views/module_system/user/components/UserTableSelect.vue";
@@ -281,7 +229,7 @@ function formatPositionOperationCell(
const inlineNodes = inline.map((a) => const inlineNodes = inline.map((a) =>
h(ElTooltip, { content: a.label, placement: "top" }, () => h(ElTooltip, { content: a.label, placement: "top" }, () =>
h("span", { class: "inline-flex" }, [ h("span", { class: "inline-flex" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: a.artType, type: a.artType,
icon: a.icon, icon: a.icon,
onClick: a.run, onClick: a.run,
@@ -305,7 +253,7 @@ function formatPositionOperationCell(
default: () => default: () =>
h(ElTooltip, { content: "更多", placement: "top" }, () => h(ElTooltip, { content: "更多", placement: "top" }, () =>
h("span", { class: "inline-flex align-middle" }, [ h("span", { class: "inline-flex align-middle" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: "more", type: "more",
onClick: () => {}, onClick: () => {},
}), }),
@@ -531,6 +479,24 @@ const positionExportContentConfig = computed(() => ({
const detailFormData = ref<PositionTable>({}); const detailFormData = ref<PositionTable>({});
const positionDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "岗位名称", prop: "name" },
{ label: "排序", prop: "order" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
const formData = ref<PositionForm>({ const formData = ref<PositionForm>({
id: undefined, id: undefined,
name: undefined, name: undefined,
@@ -599,11 +565,7 @@ const positionDialogFormItems = computed<FormItem[]>(() => [
}, },
]); ]);
const submitLoading = ref(false); const submitLoading = ref(false);
const exportModalVisible = ref(false); const { exportVisible, openExport } = useImportExport();
function openExportModal() {
exportModalVisible.value = true;
}
async function handleSearchBarSearch(params: PositionSearchForm) { async function handleSearchBarSearch(params: PositionSearchForm) {
await searchBarRef.value?.validate?.(); await searchBarRef.value?.validate?.();
@@ -633,8 +595,8 @@ function onResetSearch() {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
} }
@@ -37,7 +37,7 @@
:perm-patch="['module_system:role:patch']" :perm-patch="['module_system:role:patch']"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="handleOpenDialog('create')" @add="handleOpenDialog('create')"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="handleMoreClick" @more="handleMoreClick"
/> />
@@ -62,99 +62,65 @@
width="720px" width="720px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="4" border> :column="4"
<ElDescriptionsItem label="角色名称" :span="2"> :data="detailFormData"
{{ detailFormData.name }} :items="roleDetailItems"
</ElDescriptionsItem> max-height="75vh"
<ElDescriptionsItem label="排序" :span="2"> >
{{ detailFormData.order }} <template #data_scope="{ row }">
</ElDescriptionsItem> <ElTag v-if="row?.data_scope === 1" type="primary">仅本人数据权限</ElTag>
<ElDescriptionsItem label="角色编码" :span="2"> <ElTag v-else-if="row?.data_scope === 2" type="info">本部门数据权限</ElTag>
{{ detailFormData.code }} <ElTag v-else-if="row?.data_scope === 3" type="warning">本部门及以下数据权限</ElTag>
</ElDescriptionsItem> <ElTag v-else-if="row?.data_scope === 4" type="success">全部数据权限</ElTag>
<ElDescriptionsItem label="数据权限" :span="2"> <ElTag v-else type="danger">自定义数据权限</ElTag>
<ElTag v-if="detailFormData.data_scope === 1" type="primary">仅本人数据权限</ElTag> </template>
<ElTag v-else-if="detailFormData.data_scope === 2" type="info">本部门数据权限</ElTag> <template #depts="{ row }">
<ElTag v-else-if="detailFormData.data_scope === 3" type="warning"> <template v-if="row?.depts && (row.depts as any[]).length > 0">
本部门及以下数据权限 <ElTag
v-for="dept in row.depts as any[]"
:key="dept.id"
type="info"
:style="'margin-right: 4px; margin-bottom: 4px'"
>
{{ dept.name }}
</ElTag> </ElTag>
<ElTag v-else-if="detailFormData.data_scope === 4" type="success">全部数据权限</ElTag> </template>
<ElTag v-else type="danger">自定义数据权限</ElTag> <span v-else :style="'color: var(--el-text-color-placeholder)'">-</span>
</ElDescriptionsItem> </template>
<ElDescriptionsItem label="所属部门" :span="2"> </FaDescriptions>
<template v-if="detailFormData.depts && detailFormData.depts.length > 0">
<ElTag
v-for="dept in detailFormData.depts"
:key="dept.id"
type="info"
:style="'margin-right: 4px; margin-bottom: 4px'"
>
{{ dept.name }}
</ElTag>
</template>
<span v-else :style="'color: var(--el-text-color-placeholder)'">-</span>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2">
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'">
{{ detailFormData.status === "0" ? "启用" : "停用" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="4">
{{ detailFormData.description }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm :key="roleFormRenderKey"
:key="roleFormRenderKey" scrollbar
ref="dataFormRef" max-height="75vh"
v-model="formData" ref="dataFormRef"
:items="roleDialogFormItems" v-model="formData"
:rules="rules" :items="roleDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="'auto'" label-suffix=":"
label-position="right" :label-width="'auto'"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
> class="crud-dialog-art-form"
<template #status> >
<ElRadioGroup v-model="formData.status"> <template #status>
<ElRadio value="0">启用</ElRadio> <ElRadioGroup v-model="formData.status">
<ElRadio value="1">停用</ElRadio> <ElRadio value="0">启用</ElRadio>
</ElRadioGroup> <ElRadio value="1">停用</ElRadio>
</template> </ElRadioGroup>
</FaForm> </template>
</ElScrollbar> </FaForm>
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton
v-if="dialogVisible.type !== 'detail'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
@@ -167,7 +133,7 @@
/> />
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="roleExportContentConfig" :content-config="roleExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -177,18 +143,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
import type { IObject } from "@/components/modal/types"; import type { IObject } from "@/components/modal/types";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import RoleAPI, { import RoleAPI, {
@@ -196,7 +155,6 @@ import RoleAPI, {
type RoleTable, type RoleTable,
type TablePageQuery, type TablePageQuery,
} from "@/api/module_system/role"; } from "@/api/module_system/role";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
import { useUserStore } from "@stores"; import { useUserStore } from "@stores";
import PermissonDrawer from "./components/PermissonDrawer.vue"; import PermissonDrawer from "./components/PermissonDrawer.vue";
@@ -553,6 +511,25 @@ const roleExportContentConfig = computed(() => ({
const detailFormData = ref<RoleTable>({} as RoleTable); const detailFormData = ref<RoleTable>({} as RoleTable);
const roleDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "角色名称", prop: "name" },
{ label: "排序", prop: "order" },
{ label: "角色编码", prop: "code" },
{ label: "数据权限", prop: "data_scope", slot: "data_scope" },
{ label: "所属部门", prop: "depts", slot: "depts" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
const formData = ref<RoleForm>({ const formData = ref<RoleForm>({
id: undefined, id: undefined,
name: undefined, name: undefined,
@@ -649,7 +626,7 @@ const initialFormData: RoleForm = {
description: undefined, description: undefined,
}; };
const exportModalVisible = ref(false); const { exportVisible, openExport } = useImportExport();
async function handleSearchBarSearch(params: RoleSearchForm) { async function handleSearchBarSearch(params: RoleSearchForm) {
await searchBarRef.value?.validate?.(); await searchBarRef.value?.validate?.();
@@ -667,8 +644,8 @@ function onResetSearch() {
} }
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
} }
@@ -765,10 +742,6 @@ async function handleMoreClick(status: string) {
// 用户取消 // 用户取消
} }
} }
function openExportModal() {
exportModalVisible.value = true;
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -58,86 +58,45 @@
width="640px" width="640px"
dialog-class="crud-embed-dialog" dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog" modal-class="crud-embed-dialog"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaDescriptions
<ElDescriptions :column="2" border> :column="2"
<ElDescriptionsItem label="租户名称" :span="2"> :data="detailFormData"
{{ detailFormData.name }} :items="tenantDetailItems"
</ElDescriptionsItem> max-height="75vh"
<ElDescriptionsItem label="租户编码" :span="2"> />
{{ detailFormData.code }}
</ElDescriptionsItem>
<ElDescriptionsItem label="状态">
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'">
{{ detailFormData.status === "0" ? "正常" : "禁用" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">
{{ detailFormData.start_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="结束时间">
{{ detailFormData.end_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="2">
{{ detailFormData.description }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
</ElDescriptions>
</ElScrollbar>
</template> </template>
<template v-else> <template v-else>
<ElScrollbar max-height="75vh" :view-style="{ overflowX: 'hidden' }"> <FaForm
<FaForm :key="tenantFormRenderKey"
:key="tenantFormRenderKey" scrollbar
ref="dataFormRef" max-height="75vh"
v-model="formData" ref="dataFormRef"
:items="tenantDialogFormItems" v-model="formData"
:rules="rules" :items="tenantDialogFormItems"
label-suffix=":" :rules="rules"
:label-width="'auto'" label-suffix=":"
label-position="right" :label-width="'auto'"
:span="24" label-position="right"
:gutter="16" :span="24"
:show-reset="false" :gutter="16"
:show-submit="false" :show-reset="false"
class="crud-dialog-art-form" :show-submit="false"
/> class="crud-dialog-art-form"
</ElScrollbar> />
</template>
<template #footer>
<div class="dialog-footer" :style="'padding-right: var(--el-dialog-padding-primary)'">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton
v-if="dialogVisible.type !== 'detail'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDialog> </FaDialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h, computed, ref, reactive } from "vue";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import ArtButtonTable from "@/components/forms/fa-button-table/index.vue";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import type { FormItem } from "@/components/forms/fa-form/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import type { ColumnOption } from "@/types/component"; import type { ColumnOption } from "@/types/component";
import TenantAPI, { import TenantAPI, {
@@ -146,15 +105,6 @@ import TenantAPI, {
type TenantTable, type TenantTable,
type TenantUpdateForm, type TenantUpdateForm,
} from "@/api/module_system/tenant"; } from "@/api/module_system/tenant";
import {
ElMessage,
ElMessageBox,
ElTag,
ElTooltip,
ElDropdown,
ElDropdownMenu,
ElDropdownItem,
} from "element-plus";
import { useAuth } from "@/hooks/core/useAuth"; import { useAuth } from "@/hooks/core/useAuth";
defineOptions({ defineOptions({
@@ -240,7 +190,7 @@ function formatTenantOperationCell(
const inlineNodes = inline.map((a) => const inlineNodes = inline.map((a) =>
h(ElTooltip, { content: a.label, placement: "top" }, () => h(ElTooltip, { content: a.label, placement: "top" }, () =>
h("span", { class: "inline-flex" }, [ h("span", { class: "inline-flex" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: a.artType, type: a.artType,
icon: a.icon, icon: a.icon,
onClick: a.run, onClick: a.run,
@@ -264,7 +214,7 @@ function formatTenantOperationCell(
default: () => default: () =>
h(ElTooltip, { content: "更多", placement: "top" }, () => h(ElTooltip, { content: "更多", placement: "top" }, () =>
h("span", { class: "inline-flex align-middle" }, [ h("span", { class: "inline-flex align-middle" }, [
h(ArtButtonTable, { h(FaButtonTable, {
type: "more", type: "more",
onClick: () => {}, onClick: () => {},
}), }),
@@ -445,6 +395,24 @@ const {
const detailFormData = ref<TenantTable>({ code: "", name: "", status: "0" }); const detailFormData = ref<TenantTable>({ code: "", name: "", status: "0" });
const tenantDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "租户名称", prop: "name" },
{ label: "租户编码", prop: "code" },
{
label: "状态",
prop: "status",
span: 1,
tag: {
map: { "0": { type: "success", text: "正常" }, "1": { type: "danger", text: "禁用" } },
},
},
{ label: "开始时间", prop: "start_time", span: 1 },
{ label: "结束时间", prop: "end_time", span: 1 },
{ label: "描述", prop: "description" },
{ label: "创建时间", prop: "created_time" },
];
const currentEditId = ref<number | null>(null); const currentEditId = ref<number | null>(null);
const formData = ref<TenantForm>({ const formData = ref<TenantForm>({
@@ -574,8 +542,8 @@ const tenantDialogFormItems = computed<FormItem[]>(() => [
]); ]);
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
currentEditId.value = null; currentEditId.value = null;
} }
@@ -67,8 +67,8 @@
:import-loading="uploadLoading" :import-loading="uploadLoading"
:delete-loading="batchDeleting" :delete-loading="batchDeleting"
@add="handleOpenDialog('create')" @add="handleOpenDialog('create')"
@import="openImportModal" @import="openImport"
@export="openExportModal" @export="openExport"
@delete="handleBatchDelete" @delete="handleBatchDelete"
@more="handleMoreClick" @more="handleMoreClick"
/> />
@@ -95,121 +95,60 @@
:title="dialogVisible.title" :title="dialogVisible.title"
append-to-body append-to-body
:size="drawerSize" :size="drawerSize"
@close="handleCloseDialog" :form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@confirm="dialogVisible.type === 'detail' ? handleCloseDialog() : handleSubmit()"
> >
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<ElDescriptions :column="2" border> <FaDescriptions
<ElDescriptionsItem label="编号" :span="2"> :column="2"
{{ detailFormData.id }} :data="detailFormData"
</ElDescriptionsItem> :items="userDetailItems"
<ElDescriptionsItem label="头像" :span="2"> :scrollbar="false"
<template v-if="detailFormData.avatar"> >
<ElAvatar :src="detailFormData.avatar" size="small"></ElAvatar> <!-- 头像 → 自定义渲染 -->
</template> <template #avatar="{ row }">
<template v-else> <ElAvatar v-if="row?.avatar" :src="row?.avatar as string" size="small" />
<ElAvatar icon="UserFilled" size="small"></ElAvatar> <ElAvatar v-else icon="UserFilled" size="small" />
</template> </template>
</ElDescriptionsItem> <!-- 性别 → 三种状态 Tag -->
<ElDescriptionsItem label="账号" :span="2"> <template #gender="{ row }">
{{ detailFormData.username }} <ElTag v-if="row?.gender === '0'" type="success">男</ElTag>
</ElDescriptionsItem> <ElTag v-else-if="row?.gender === '1'" type="warning">女</ElTag>
<ElDescriptionsItem label="用户名" :span="2">
{{ detailFormData.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="性别" :span="2">
<ElTag v-if="detailFormData.gender === '0'" type="success">男</ElTag>
<ElTag v-else-if="detailFormData.gender === '1'" type="warning">女</ElTag>
<ElTag v-else type="info">未知</ElTag> <ElTag v-else type="info">未知</ElTag>
</ElDescriptionsItem> </template>
<ElDescriptionsItem label="部门" :span="2"> <!-- 角色 → 数组 join 渲染 -->
{{ detailFormData.dept ? detailFormData.dept.name : "" }} <template #roles="{ row }">
</ElDescriptionsItem> {{ row?.roles ? (row.roles as any[]).map((item: any) => item.name).join("、") : "" }}
<ElDescriptionsItem label="角色" :span="2"> </template>
<!-- 岗位 → 数组 join 渲染 -->
<template #positions="{ row }">
{{ {{
detailFormData.roles ? detailFormData.roles.map((item) => item.name).join("、") : "" row?.positions
}} ? (row.positions as any[]).map((item: any) => item.name).join("、")
</ElDescriptionsItem>
<ElDescriptionsItem label="岗位" :span="2">
{{
detailFormData.positions
? detailFormData.positions.map((item) => item.name).join("、")
: "" : ""
}} }}
</ElDescriptionsItem> </template>
<ElDescriptionsItem label="邮箱" :span="2"> </FaDescriptions>
{{ detailFormData.email }}
</ElDescriptionsItem>
<ElDescriptionsItem label="手机号" :span="2">
{{ detailFormData.mobile }}
</ElDescriptionsItem>
<ElDescriptionsItem label="是否超管" :span="2">
<ElTag :type="detailFormData.is_superuser ? 'success' : 'info'">
{{ detailFormData.is_superuser ? "是" : "否" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="状态" :span="2">
<ElTag :type="detailFormData.status === '0' ? 'success' : 'danger'">
{{ detailFormData.status === "0" ? "启用" : "停用" }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="上次登录时间" :span="2">
{{ detailFormData.last_login }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建人" :span="2">
{{ detailFormData.created_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新人" :span="2">
{{ detailFormData.updated_by?.name }}
</ElDescriptionsItem>
<ElDescriptionsItem label="创建时间" :span="2">
{{ detailFormData.created_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</ElDescriptionsItem>
<ElDescriptionsItem label="描述" :span="4">
{{ detailFormData.description }}
</ElDescriptionsItem>
</ElDescriptions>
</template> </template>
<template v-else> <template v-else>
<ElForm <FaForm
:key="userFormRenderKey"
ref="dataFormRef" ref="dataFormRef"
:model="formData" v-model="formData"
:items="userDialogFormItems"
:rules="rules" :rules="rules"
label-suffix=":" label-suffix=":"
label-width="100px" :label-width="'auto'"
label-position="right" label-position="right"
:span="24"
:gutter="16"
:show-reset="false"
:show-submit="false"
class="crud-dialog-art-form"
> >
<ElFormItem label="账号" prop="username"> <template #dept_id>
<ElInput
v-model="formData.username"
:disabled="!!formData.id"
placeholder="请输入账号"
/>
</ElFormItem>
<ElFormItem label="用户名" prop="name">
<ElInput v-model="formData.name" placeholder="请输入用户名" />
</ElFormItem>
<ElFormItem label="性别" prop="gender">
<ElSelect v-model="formData.gender" placeholder="请选择性别">
<ElOption label="男" value="0" />
<ElOption label="女" value="1" />
<ElOption label="未知" value="2" />
</ElSelect>
</ElFormItem>
<ElFormItem label="手机号" prop="mobile">
<ElInput v-model="formData.mobile" placeholder="请输入手机号码" maxlength="11" />
</ElFormItem>
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="formData.email" placeholder="请输入邮箱" maxlength="50" />
</ElFormItem>
<ElFormItem label="部门" prop="dept_id">
<ElTreeSelect <ElTreeSelect
v-model="formData.dept_id" v-model="formData.dept_id"
placeholder="请选择上级部门" placeholder="请选择上级部门"
@@ -219,9 +158,8 @@
check-strictly check-strictly
:render-after-expand="false" :render-after-expand="false"
/> />
</ElFormItem> </template>
<template #role_ids>
<ElFormItem label="角色" prop="role_ids">
<ElSelect v-model="formData.role_ids" multiple placeholder="请选择角色"> <ElSelect v-model="formData.role_ids" multiple placeholder="请选择角色">
<ElOption <ElOption
v-for="item in roleOptions" v-for="item in roleOptions"
@@ -231,9 +169,8 @@
:disabled="item.disabled" :disabled="item.disabled"
/> />
</ElSelect> </ElSelect>
</ElFormItem> </template>
<template #position_ids>
<ElFormItem label="岗位" prop="position_ids">
<ElSelect v-model="formData.position_ids" multiple placeholder="请选择岗位"> <ElSelect v-model="formData.position_ids" multiple placeholder="请选择岗位">
<ElOption <ElOption
v-for="item in positionOptions" v-for="item in positionOptions"
@@ -243,60 +180,13 @@
:disabled="item.disabled" :disabled="item.disabled"
/> />
</ElSelect> </ElSelect>
</ElFormItem> </template>
</FaForm>
<ElFormItem v-if="!formData.id" label="密码" prop="password">
<ElInput
v-model="formData.password"
placeholder="请输入密码"
type="password"
show-password
clearable
/>
</ElFormItem>
<ElFormItem label="是否超管" prop="is_superuser">
<ElSwitch v-model="formData.is_superuser" />
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElRadioGroup v-model="formData.status">
<ElRadio value="0">启用</ElRadio>
<ElRadio value="1">停用</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="描述" prop="description">
<ElInput
v-model="formData.description"
:rows="4"
:maxlength="100"
show-word-limit
type="textarea"
placeholder="请输入描述"
/>
</ElFormItem>
</ElForm>
</template>
<template #footer>
<div class="dialog-footer">
<ElButton @click="handleCloseDialog">取消</ElButton>
<ElButton
v-if="dialogVisible.type === 'create' || dialogVisible.type === 'update'"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
确定
</ElButton>
<ElButton v-else type="primary" @click="handleCloseDialog">确定</ElButton>
</div>
</template> </template>
</FaDrawer> </FaDrawer>
<FaImportDialog <FaImportDialog
v-model="importModalVisible" v-model="importVisible"
:content-config="userImportContentConfig" :content-config="userImportContentConfig"
default-template-file-name="user_import_template.xlsx" default-template-file-name="user_import_template.xlsx"
:loading="uploadLoading" :loading="uploadLoading"
@@ -304,7 +194,7 @@
/> />
<FaExportDialog <FaExportDialog
v-model="exportModalVisible" v-model="exportVisible"
:content-config="userExportContentConfig" :content-config="userExportContentConfig"
:query-params="exportQueryParams" :query-params="exportQueryParams"
:page-data="data" :page-data="data"
@@ -319,20 +209,14 @@ defineOptions({
inheritAttrs: false, inheritAttrs: false,
}); });
import { h, ref, reactive, computed, nextTick } from "vue";
import { UserFilled } from "@element-plus/icons-vue"; import { UserFilled } from "@element-plus/icons-vue";
import { useAppStore } from "@stores/modules/app.store"; import { useAppStore } from "@stores/modules/app.store";
import { DeviceEnum } from "@/enums/settings/device.enum"; import { DeviceEnum } from "@/enums/settings/device.enum";
import { ResultEnum } from "@/enums/api/result.enum"; import { ResultEnum } from "@/enums/api/result.enum";
import { useTable } from "@/hooks/core/useTable"; import { useTable } from "@/hooks/core/useTable";
import FaTable from "@/components/tables/fa-table/index.vue"; import { useImportExport } from "@/hooks/core/useImportExport";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vue";
import FaSearchBar from "@/components/forms/fa-search-bar/index.vue";
import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue"; import type { SearchFormItem } from "@/components/forms/fa-search-bar/index.vue";
import FaDrawer from "@/components/modal/fa-drawer/index.vue"; import type { FormItem } from "@/components/forms/fa-form/index.vue";
import FaImportDialog from "@/components/modal/fa-import-dialog/index.vue";
import FaExportDialog from "@/components/modal/fa-export-dialog/index.vue";
import type { IContentConfig, IObject } from "@/components/modal/types"; import type { IContentConfig, IObject } from "@/components/modal/types";
import UserAPI, { import UserAPI, {
type UserForm, type UserForm,
@@ -347,6 +231,7 @@ import DeptTree from "./components/DeptTree.vue";
import UserTableSelect from "./components/UserTableSelect.vue"; import UserTableSelect from "./components/UserTableSelect.vue";
import { useUserStore } from "@stores"; import { useUserStore } from "@stores";
import { ElMessage, ElMessageBox, ElTag, ElAvatar } from "element-plus"; import { ElMessage, ElMessageBox, ElTag, ElAvatar } from "element-plus";
import type { DescriptionsItem } from "@/components/others/fa-descriptions/index.vue";
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"; import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
@@ -455,7 +340,8 @@ function formatUserOperationCell(row: UserInfo, ctx: Parameters<typeof buildUser
}); });
} }
const dataFormRef = ref(); const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const userFormRenderKey = ref(0);
const submitLoading = ref(false); const submitLoading = ref(false);
const uploadLoading = ref(false); const uploadLoading = ref(false);
const batchDeleting = ref(false); const batchDeleting = ref(false);
@@ -465,10 +351,113 @@ const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "450
const deptOptions = ref<OptionType[]>(); const deptOptions = ref<OptionType[]>();
const roleOptions = ref<Array<{ value: number; label: string; disabled?: boolean }>>(); const roleOptions = ref<Array<{ value: number; label: string; disabled?: boolean }>>();
const positionOptions = ref<Array<{ value: number; label: string; disabled?: boolean }>>(); const positionOptions = ref<Array<{ value: number; label: string; disabled?: boolean }>>();
const importModalVisible = ref(false); const { importVisible, exportVisible, openImport, openExport } = useImportExport();
const exportModalVisible = ref(false);
const detailFormData = ref<UserInfo>({}); const detailFormData = ref<UserInfo>({});
// 用户详情描述项配置 —— 数据驱动 + 关键字段用具名插槽覆盖
const userDetailItems: DescriptionsItem[] = [
{ label: "编号", prop: "id" },
{ label: "头像", prop: "avatar", slot: "avatar" }, // 自定义插槽渲染
{ label: "账号", prop: "username" },
{ label: "用户名", prop: "name" },
{ label: "性别", prop: "gender", slot: "gender" }, // 三种状态 Tag
{ label: "部门", prop: "dept.name" }, // 嵌套属性 a.b.c
{ label: "角色", prop: "roles", slot: "roles" }, // 数组 join 渲染
{ label: "岗位", prop: "positions", slot: "positions" }, // 数组 join 渲染
{ label: "邮箱", prop: "email" },
{ label: "手机号", prop: "mobile" },
{
label: "是否超管",
prop: "is_superuser",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "info", text: "否" } },
},
},
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "上次登录时间", prop: "last_login" },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
{ label: "描述", prop: "description", span: 4 },
];
// 用户新增/编辑表单配置 —— 三个复杂字段(部门树、角色多选、岗位多选)用插槽渲染
const userDialogFormItems = computed<FormItem[]>(() => [
{
key: "username",
label: "账号",
type: "input",
props: { placeholder: "请输入账号", disabled: !!formData.value.id },
},
{ key: "name", label: "用户名", type: "input", props: { placeholder: "请输入用户名" } },
{
key: "gender",
label: "性别",
type: "select",
props: {
placeholder: "请选择性别",
options: [
{ label: "男", value: "0" },
{ label: "女", value: "1" },
{ label: "未知", value: "2" },
],
},
},
{
key: "mobile",
label: "手机号",
type: "input",
props: { placeholder: "请输入手机号码", maxlength: 11 },
},
{
key: "email",
label: "邮箱",
type: "input",
props: { placeholder: "请输入邮箱", maxlength: 50 },
},
{ key: "dept_id", label: "部门", type: "input" /* 实际渲染由 #dept_id 插槽接管 */ },
{ key: "role_ids", label: "角色", type: "input" /* 实际渲染由 #role_ids 插槽接管 */ },
{ key: "position_ids", label: "岗位", type: "input" /* 实际渲染由 #position_ids 插槽接管 */ },
{
key: "password",
label: "密码",
type: "input",
hidden: !!formData.value.id,
props: { placeholder: "请输入密码", type: "password", showPassword: true, clearable: true },
},
{ key: "is_superuser", label: "是否超管", type: "switch" },
{
key: "status",
label: "状态",
type: "radiogroup",
props: {
options: [
{ label: "启用", value: "0" },
{ label: "停用", value: "1" },
],
},
},
{
key: "description",
label: "描述",
type: "input",
props: {
type: "textarea",
rows: 4,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
]);
const searchForm = ref<UserSearchForm>({ const searchForm = ref<UserSearchForm>({
username: undefined, username: undefined,
name: undefined, name: undefined,
@@ -836,21 +825,13 @@ async function handleDeptNodeClick() {
await getData(); await getData();
} }
function openImportModal() {
importModalVisible.value = true;
}
function openExportModal() {
exportModalVisible.value = true;
}
async function handleImportUpload(formDataUpload: FormData) { async function handleImportUpload(formDataUpload: FormData) {
uploadLoading.value = true; uploadLoading.value = true;
try { try {
const response = await UserAPI.importUser(formDataUpload); const response = await UserAPI.importUser(formDataUpload);
if (response.data.code === ResultEnum.SUCCESS) { if (response.data.code === ResultEnum.SUCCESS) {
ElMessage.success(`${response.data.msg},${response.data.data}`); ElMessage.success(`${response.data.msg},${response.data.data}`);
importModalVisible.value = false; importVisible.value = false;
await refreshData(); await refreshData();
} else { } else {
ElMessage.error(response.data.msg || "导入失败"); ElMessage.error(response.data.msg || "导入失败");
@@ -893,7 +874,9 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
} }
} else { } else {
dialogVisible.title = "新增用户"; dialogVisible.title = "新增用户";
Object.assign(formData.value, initialFormData);
formData.value.id = undefined; formData.value.id = undefined;
userFormRenderKey.value += 1;
} }
dialogVisible.visible = true; dialogVisible.visible = true;
await nextTick(); await nextTick();
@@ -928,7 +911,7 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
} }
async function handleSubmit() { async function handleSubmit() {
dataFormRef.value.validate(async (valid: boolean) => { dataFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return; if (!valid) return;
submitLoading.value = true; submitLoading.value = true;
const id = formData.value.id; const id = formData.value.id;
@@ -778,8 +778,8 @@ const initialFormData: Partial<NodeForm> = {
}; };
async function resetForm() { async function resetForm() {
dataFormRef.value?.ref?.resetFields(); dataFormRef.value?.resetFields();
dataFormRef.value?.ref?.clearValidate(); dataFormRef.value?.clearValidate();
Object.assign(formData, initialFormData); Object.assign(formData, initialFormData);
argsList.value = []; argsList.value = [];
kwargsList.value = []; kwargsList.value = [];
@@ -886,7 +886,7 @@ function handleOpenExecuteDialog(row: NodeTable) {
function handleCloseExecuteDialog() { function handleCloseExecuteDialog() {
executeDialogVisible.value = false; executeDialogVisible.value = false;
currentExecuteNode.value = null; currentExecuteNode.value = null;
executeFormRef.value?.ref?.resetFields(); executeFormRef.value?.resetFields();
} }
async function handleExecuteNode() { async function handleExecuteNode() {
@@ -454,8 +454,8 @@ const rules: FormRules = {
function resetForm() { function resetForm() {
Object.assign(formData.value, defaultForm()); Object.assign(formData.value, defaultForm());
editingId.value = null; editingId.value = null;
formRef.value?.ref?.resetFields(); formRef.value?.resetFields();
formRef.value?.ref?.clearValidate(); formRef.value?.clearValidate();
} }
function handleCloseDialog() { function handleCloseDialog() {
+23 -2
View File
@@ -241,7 +241,29 @@ export default ({ mode }: { mode: string }) => {
vue(), vue(),
tailwindcss(), tailwindcss(),
AutoImport({ AutoImport({
imports: ["vue", "vue-router", "pinia", "@vueuse/core", "vue-i18n"], imports: [
"vue",
"vue-router",
"pinia",
"@vueuse/core",
"vue-i18n",
// Element Plus 组件/API:在 <script> 中通过 h() 或直接调用时需要自动导入
{
"element-plus": [
"ElMessageBox",
"ElTag",
"ElTooltip",
"ElDropdown",
"ElDropdownMenu",
"ElDropdownItem",
],
},
// 自定义组件:在 <script> 中通过 h() 使用时需要自动导入
{ "@/components/forms/fa-button-table/index.vue": [["default", "FaButtonTable"]] },
{ "@/components/others/fa-json-pretty/index.vue": [["default", "JsonPretty"]] },
{ "@/components/others/fa-copy-button/index.vue": [["default", "CopyButton"]] },
{ "@/components/others/fa-menu-routeIcon/index.vue": [["default", "MenuRouteIcon"]] },
],
dts: "src/types/import/auto-imports.d.ts", dts: "src/types/import/auto-imports.d.ts",
resolvers: [ElementPlusResolver({ importStyle: "sass" })], resolvers: [ElementPlusResolver({ importStyle: "sass" })],
eslintrc: { eslintrc: {
@@ -254,7 +276,6 @@ export default ({ mode }: { mode: string }) => {
Components({ Components({
dts: "src/types/import/components.d.ts", dts: "src/types/import/components.d.ts",
resolvers: [ElementPlusResolver({ importStyle: "sass" })], resolvers: [ElementPlusResolver({ importStyle: "sass" })],
/** art-* 布局组件以 src/layouts 为唯一源码目录(与 src/layouts/index.vue 一致),避免与旧路径重复 */
dirs: ["src/components", "src/**/components"], dirs: ["src/components", "src/**/components"],
}), }),
ElementPlus({ ElementPlus({