mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-26 14:23:48 +00:00
chore: 完成多批次代码优化与重构
- 重构工作流模块目录结构,迁移代码文件 - 修复类型断言空值安全问题,添加 ! 操作符 - 优化样式类名,替换 flex-cc 为标准 flex 工具类 - 更新路由标签简化文案,移除冗余注释 - 调整 ruff 配置,放宽行长度限制 - 更新 README 与多语言文案,优化项目描述 - 修复表单、图表组件的类型与样式问题 - 简化搜索表单、数据卡片的布局代码
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露编辑器实例和方法
|
||||
|
||||
Reference in New Issue
Block a user