chore: 完成多批次代码优化与重构

- 重构工作流模块目录结构,迁移代码文件
- 修复类型断言空值安全问题,添加 ! 操作符
- 优化样式类名,替换 flex-cc 为标准 flex 工具类
- 更新路由标签简化文案,移除冗余注释
- 调整 ruff 配置,放宽行长度限制
- 更新 README 与多语言文案,优化项目描述
- 修复表单、图表组件的类型与样式问题
- 简化搜索表单、数据卡片的布局代码
This commit is contained in:
zhangtao
2026-06-20 05:31:46 +08:00
parent 82b742620b
commit d34d4a4c50
378 changed files with 4149 additions and 6375 deletions
@@ -11,7 +11,7 @@
:disabled="item.disabled"
@click="handleClick(item)"
>
<div class="flex-c gap-2" :style="{ color: item.color }">
<div class="flex items-center gap-2" :style="{ color: item.color }">
<FaSvgIcon v-if="item.icon" :icon="item.icon" />
<span>{{ item.label }}</span>
</div>
@@ -2,7 +2,7 @@
<template>
<div
:class="[
'inline-flex items-center justify-center min-w-8 h-8 px-2.5 mr-2.5 text-sm c-p rounded-md align-middle transition-all duration-200 hover-btn',
'inline-flex items-center justify-center min-w-8 h-8 px-2.5 mr-2.5 text-sm cursor-pointer rounded-md align-middle transition-all duration-200 hover-btn',
buttonClass,
]"
:style="{ backgroundColor: buttonBgColor, color: iconColor }"
@@ -216,7 +216,7 @@ const calculateColumnWidths = (data: Record<string, string>[]): XLSX.ColInfo[] =
if (data.length === 0) return [];
const sampleSize = Math.min(data.length, 100); // 只取前100行计算列宽
const columns = Object.keys(data[0]);
const columns = Object.keys(data[0]!);
return columns.map((column) => {
// 使用配置的列宽度
@@ -514,7 +514,6 @@ const handleImportError = (error: Error) => {
* 处理 Excel 导出成功
*/
const handleExportSuccess = () => {
console.log("导出成功");
ElMessage.success("Excel 导出成功");
};
@@ -531,7 +530,7 @@ const handleExportError = (error: Error) => {
* @param progress 导出进度百分比
*/
const handleProgress = (progress: number) => {
console.log("导出进度:", progress);
// 进度由进度条 UI 实时显示
};
/**
@@ -48,8 +48,8 @@ async function importExcel(file: File): Promise<Array<Record<string, unknown>>>
try {
const data = e.target?.result;
const workbook = XLSX.read(data, { type: "array" });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const firstSheetName = workbook.SheetNames[0]!;
const worksheet = workbook.Sheets[firstSheetName]!;
const results = XLSX.utils.sheet_to_json(worksheet);
resolve(results as Array<Record<string, unknown>>);
} catch (error) {
@@ -206,7 +206,6 @@ const handleImportError = (error: Error) => {
* 处理 Excel 导出成功
*/
const handleExportSuccess = () => {
console.log("导出成功");
ElMessage.success("Excel 导出成功");
};
@@ -223,7 +222,7 @@ const handleExportError = (error: Error) => {
* @param progress 导出进度百分比
*/
const handleProgress = (progress: number) => {
console.log("导出进度:", progress);
// 进度由进度条 UI 实时显示
};
/**
@@ -76,7 +76,7 @@
</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"
class="mb-3 flex items-center flex-wrap justify-end md:flex-row md:items-stretch md:gap-2"
:style="actionButtonsStyle"
>
<div class="flex gap-2 md:justify-center">
@@ -163,7 +163,7 @@
</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"
class="mb-3 flex items-center flex-wrap justify-end md:flex-row md:items-stretch md:gap-2"
:style="actionButtonsStyle"
>
<div class="flex gap-2 md:justify-center">
@@ -18,7 +18,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { ElDatePicker } from "element-plus";
type DatePickerType =
| "year"
@@ -1,94 +0,0 @@
<!-- 在 FaSearchBar 上追加「创建人 / 更新人 / 创建时间 / 更新时间」,并内置 UserTableSelect 插槽;业务页只传自己的 items 即可 -->
<template>
<FaSearchBar
ref="innerRef"
v-model="modelValue"
v-bind="forwardedAttrs"
:items="mergedItems"
@search="(p) => emit('search', p)"
@reset="emit('reset')"
>
<template v-for="(_, name) in $slots" :key="name" #[name]="scope">
<slot :name="name" v-bind="scope || {}" />
</template>
<template v-if="!$slots.created_id" #created_id>
<div class="w-full min-w-0">
<FaUserTableSelect
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
@update:model-value="(v: number | undefined) => patchField('created_id', v)"
@confirm-click="emitImmediateSearch"
@clear-click="emitImmediateSearch"
/>
</div>
</template>
<template v-if="!$slots.updated_id" #updated_id>
<div class="w-full min-w-0">
<FaUserTableSelect
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
@update:model-value="(v: number | undefined) => patchField('updated_id', v)"
@confirm-click="emitImmediateSearch"
@clear-click="emitImmediateSearch"
/>
</div>
</template>
</FaSearchBar>
</template>
<script setup lang="ts">
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { computed, ref, useAttrs } from "vue";
import FaSearchBar from "./index.vue";
import type { SearchFormItem } from "./index.vue";
import {
getAuditSearchFormItems,
type GetAuditSearchFormItemsOptions,
} from "./auditSearchFormItems";
defineOptions({ name: "FaSearchBarWithAudit", inheritAttrs: false });
interface Props {
/** 仅业务条件表单项,不含审计四字段 */
items: SearchFormItem[];
/** 为 false 时与原生 FaSearchBar 一致,仅使用 `items` */
includeAudit?: boolean;
/** 传给 getAuditSearchFormItems 的选项 */
auditItemOptions?: GetAuditSearchFormItemsOptions;
}
const props = withDefaults(defineProps<Props>(), { includeAudit: true });
interface Emits {
search: [Record<string, any>];
reset: [];
}
const emit = defineEmits<Emits>();
const modelValue = defineModel<Record<string, any>>({ default: () => ({}) });
const attrs = useAttrs();
// @ts-expect-error 循环类型引用问题
const innerRef = ref<InstanceType<typeof FaSearchBar> | null>(null);
const forwardedAttrs = computed(() => attrs as Record<string, unknown>);
const auditItems = computed(() => getAuditSearchFormItems(props.auditItemOptions));
const mergedItems = computed(() => {
if (!props.includeAudit) return props.items;
return [...props.items, ...auditItems.value];
});
function patchField(key: "created_id" | "updated_id", val: number | undefined) {
modelValue.value = { ...modelValue.value, [key]: val };
}
function emitImmediateSearch() {
emit("search", { ...modelValue.value });
}
// @ts-expect-error 循环类型引用问题
defineExpose({
validate: (...args: any[]) => innerRef.value?.validate?.(...args),
reset: () => innerRef.value?.reset?.(),
getOutput: () => innerRef.value?.getOutput?.(),
ref: computed(() => innerRef.value?.ref) as unknown as any,
});
</script>
@@ -105,7 +105,7 @@ interface IUser {
gender?: string;
avatar?: string;
email?: string | null;
status?: string;
status?: number;
dept_name?: string;
role_names?: string[];
created_time?: string;
@@ -26,7 +26,8 @@ export interface GetAuditSearchFormItemsOptions {
/**
* 常见「创建人 / 更新人 / 创建时间 / 更新时间」搜索项,供 FaSearchBar `items` 使用。
* 创建人、更新人需配合 `#created_id`、`#updated_id` 插槽(如 FaSearchBarWithAudit)。
* 当 FaSearchBar 设置 `includeAudit` 为 true 时,会自动追加这四个字段。
* 创建人、更新人插槽由 FaSearchBar 内置提供,也可通过 `#created_id`、`#updated_id` 覆盖。
*/
export function getAuditSearchFormItems(
options?: GetAuditSearchFormItemsOptions
@@ -27,46 +27,70 @@
<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.key === 'created_id' && !$slots.created_id">
<div class="w-full min-w-0">
<FaUserTableSelect
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
@update:model-value="(v: number | undefined) => patchAuditField('created_id', v)"
@confirm-click="emitImmediateSearch"
@clear-click="emitImmediateSearch"
/>
</div>
</template>
<!-- 更新人插槽 -->
<template v-else-if="item.key === 'updated_id' && !$slots.updated_id">
<div class="w-full min-w-0">
<FaUserTableSelect
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
@update:model-value="(v: number | undefined) => patchAuditField('updated_id', v)"
@confirm-click="emitImmediateSearch"
@clear-click="emitImmediateSearch"
/>
</div>
</template>
<template v-else>
<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 === '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-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>
<!-- 动态插槽支持 -->
<template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
<component :is="slotFn" />
</template>
</component>
</slot>
</template>
</ElFormItem>
</ElCol>
<ElCol :xs="24" :sm="24" :md="span" :lg="span" :xl="span" class="action-column">
@@ -114,6 +138,11 @@ import { useWindowSize } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { toRaw, type Component } from "vue";
import FaDatePicker from "@/components/forms/fa-search-bar/FaDatePicker.vue";
import FaUserTableSelect from "./FaUserTableSelect.vue";
import {
getAuditSearchFormItems,
type GetAuditSearchFormItemsOptions,
} from "./auditSearchFormItems";
import {
ElCascader,
ElCheckbox,
@@ -217,6 +246,10 @@ interface Props {
disabledSearch?: boolean;
/** 搜索时是否清洗空值 */
sanitizeOutput?: Partial<SanitizeOutputOptions>;
/** 是否自动追加审计四字段(创建人/更新人/创建时间/更新时间) */
includeAudit?: boolean;
/** 传给 getAuditSearchFormItems 的选项 */
auditItemOptions?: GetAuditSearchFormItemsOptions;
}
interface SanitizeOutputOptions {
@@ -248,6 +281,7 @@ const props = withDefaults(defineProps<Props>(), {
showSearch: true,
disabledSearch: false,
sanitizeOutput: () => ({}),
includeAudit: false,
});
interface Emits {
@@ -260,6 +294,15 @@ const emit = defineEmits<Emits>();
const modelValue = defineModel<Record<string, any>>({ default: {} });
const initialModelValue = ref<Record<string, any>>({});
// 审计字段配置
const auditItems = computed(() => getAuditSearchFormItems(props.auditItemOptions));
// 合并业务字段和审计字段
const mergedItems = computed(() => {
if (!props.includeAudit) return props.items;
return [...props.items, ...auditItems.value];
});
// 保存组件初始化时的表单快照,用于 reset 时恢复默认筛选条件。
const cloneModelValue = (value: Record<string, any> | undefined) => {
if (!value) return {};
@@ -426,11 +469,22 @@ const getComponent = (item: SearchFormItem) => {
return componentMap[type as keyof typeof componentMap] || componentMap["input"];
};
/**
* 更新审计字段并立即触发搜索
*/
const patchAuditField = (key: "created_id" | "updated_id", val: number | undefined) => {
modelValue.value = { ...modelValue.value, [key]: val };
};
const emitImmediateSearch = () => {
emit("search", getSanitizedOutput());
};
/**
* 可见的表单项
*/
const visibleFormItems = computed(() => {
const filteredItems = props.items.filter((item) => !item.hidden);
const filteredItems = mergedItems.value.filter((item) => !item.hidden);
const shouldShowLess = !props.isExpand && !isExpanded.value;
if (shouldShowLess) {
const maxItemsPerRow = Math.floor(24 / props.span) - 1;
@@ -19,7 +19,7 @@
<script setup lang="ts">
import "@wangeditor-next/editor/dist/css/style.css";
import { onBeforeUnmount, onMounted, shallowRef, computed } from "vue";
import { onBeforeUnmount, onMounted, onUnmounted, shallowRef, computed } from "vue";
import { Editor, Toolbar } from "@wangeditor-next/editor-for-vue";
import { useUserStore } from "@stores";
import { request, EmojiText } from "@utils";
@@ -185,17 +185,21 @@ const onCreateEditor = (editor: IDomEditor) => {
};
// 应用自定义图标(带重试机制)
//
// 注意:递归 setTimeout 用于等待 wangEditor 工具栏 DOM 渲染完成。
// 组件卸载时必须清理,避免定时器在编辑器实例已销毁后继续执行。
const applyCustomIcons = () => {
let retryCount = 0;
const maxRetries = 10;
const retryDelay = 100;
let timerId: ReturnType<typeof setTimeout> | null = null;
const tryApplyIcons = () => {
const editor = editorRef.value;
if (!editor) {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(tryApplyIcons, retryDelay);
timerId = setTimeout(tryApplyIcons, retryDelay);
}
return;
}
@@ -205,7 +209,7 @@ const applyCustomIcons = () => {
if (!editorContainer) {
if (retryCount < maxRetries) {
retryCount++;
setTimeout(tryApplyIcons, retryDelay);
timerId = setTimeout(tryApplyIcons, retryDelay);
}
return;
}
@@ -220,7 +224,7 @@ const applyCustomIcons = () => {
// 如果工具栏还没渲染完成,继续重试
if (retryCount < maxRetries) {
retryCount++;
setTimeout(tryApplyIcons, retryDelay);
timerId = setTimeout(tryApplyIcons, retryDelay);
} else {
console.warn("工具栏渲染超时,无法应用自定义图标 - 编辑器实例:", editor.id);
}
@@ -228,6 +232,14 @@ const applyCustomIcons = () => {
// 使用 requestAnimationFrame 确保在下一帧执行
requestAnimationFrame(tryApplyIcons);
// 组件卸载时清理挂起的重试定时器
onUnmounted(() => {
if (timerId !== null) {
clearTimeout(timerId);
timerId = null;
}
});
};
// 暴露编辑器实例和方法