chore: 清理冗余代码与配置,优化项目结构

1.  删除无用文件与废弃代码:移除locale枚举、element-plus插件、sse路由、api token模块等
2.  简化类型导入与依赖:移除大量未使用的类型导入,统一echarts导入方式
3.  优化配置与样式:调整gitignore、样式引入顺序,新增列表动画样式
4.  修复接口与模型:修正接口返回类型、查询参数配置,更新部门模型字段
5.  优化性能与体验:添加图片懒加载,优化加载逻辑与表格渲染
6.  调整环境配置:新增并更新开发/生产环境配置文件
This commit is contained in:
zhangtao
2026-07-23 20:49:52 +08:00
parent 096e2216d6
commit 0ce31936aa
360 changed files with 8072 additions and 22122 deletions
@@ -124,7 +124,7 @@ defineOptions({ name: "FaAiAssistant" });
import { resolveIconForFaSvgIcon } from "@utils";
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { ElMessage } from "@/utils/message";
import { useSettingsStore } from "@stores";
import { AiChatAPI, ChatSession, ChatSessionDetail } from "@/api/module_ai/chat";
@@ -76,7 +76,7 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { dayjs } from "element-plus";
import dayjs from "dayjs";
defineOptions({ name: "FaCalendar" });
@@ -91,7 +91,7 @@
defineOptions({ name: "FaDescriptions" });
import { computed, useAttrs } from "vue";
import { useNamespace } from "element-plus";
import { useNamespace } from "element-plus/es/hooks/use-namespace/index";
export type TagType = "primary" | "success" | "warning" | "danger" | "info";
@@ -60,9 +60,9 @@
<script setup lang="ts">
defineOptions({ name: "FaGuide" });
import { MenuTypeEnum } from "@/enums/appEnum";
import { computed } from "vue";
import { useSettingsStore } from "@stores";
import { MenuTypeEnum } from "@/enums/appEnum";
const settingStore = useSettingsStore();
const { t } = useI18n();
@@ -67,7 +67,7 @@
@click="selectIcon(icon)"
>
<ElIcon>
<component :is="icon" />
<component :is="elementPlusIconsVue[icon]" />
</ElIcon>
</li>
</ul>
@@ -81,7 +81,6 @@
<script setup lang="ts">
defineOptions({ name: "FaIconSelect" });
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import {
listLocalIconBasenames,
isIconifyStoredIcon,
@@ -111,7 +110,16 @@ const popoverVisible = ref(false);
const activeTab = ref("svg");
const svgIcons = ref<string[]>([]);
const elementIcons = ref<string[]>(Object.keys(ElementPlusIconsVue));
const elementIcons = ref<string[]>([]);
const elementPlusIconsVue = ref<Record<string, any>>({});
// 异步加载 Element Plus 图标,避免影响首屏
async function loadElementIcons() {
if (elementIcons.value.length > 0) return;
const icons = await import("@element-plus/icons-vue");
elementPlusIconsVue.value = icons;
elementIcons.value = Object.keys(icons);
}
const selectedIcon = defineModel<string | undefined>("modelValue", {
default: "",
});
@@ -127,8 +135,11 @@ function loadIcons() {
filteredSvgIcons.value = svgIcons.value;
}
function handleTabClick(tabPane: any) {
async function handleTabClick(tabPane: any) {
activeTab.value = tabPane.props.name;
if (tabPane.props.name === "element") {
await loadElementIcons();
}
filterIcons();
}
@@ -167,13 +178,18 @@ function clearSelectedIcon() {
selectedIcon.value = "";
}
onMounted(() => {
onMounted(async () => {
loadIcons();
if (selectedIcon.value) {
const raw = selectedIcon.value.trim();
const epKey = raw.replace(/^el-icon-/i, "");
if (elementIcons.value.includes(epKey)) {
activeTab.value = "element";
if (raw.startsWith("el-icon-")) {
await loadElementIcons();
if (elementIcons.value.includes(epKey)) {
activeTab.value = "element";
} else {
activeTab.value = "svg";
}
} else if (isIconifyStoredIcon(raw)) {
activeTab.value = "svg";
} else {
@@ -5,11 +5,36 @@
<script setup lang="ts">
import { computed } from "vue";
import MarkdownIt from "markdown-it";
import markdownItHighlightjs from "markdown-it-highlightjs";
import hljs from "highlight.js";
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import typescript from "highlight.js/lib/languages/typescript";
import python from "highlight.js/lib/languages/python";
import json from "highlight.js/lib/languages/json";
import html from "highlight.js/lib/languages/xml";
import css from "highlight.js/lib/languages/css";
import scss from "highlight.js/lib/languages/scss";
import sql from "highlight.js/lib/languages/sql";
import bash from "highlight.js/lib/languages/bash";
import yaml from "highlight.js/lib/languages/yaml";
import markdown from "highlight.js/lib/languages/markdown";
import DOMPurify from "dompurify";
import "highlight.js/styles/atom-one-light.css";
// 注册语言(按需导入,减少打包体积)
hljs.registerLanguage("javascript", javascript);
hljs.registerLanguage("typescript", typescript);
hljs.registerLanguage("python", python);
hljs.registerLanguage("json", json);
hljs.registerLanguage("html", html);
hljs.registerLanguage("xml", html);
hljs.registerLanguage("vue", html);
hljs.registerLanguage("css", css);
hljs.registerLanguage("scss", scss);
hljs.registerLanguage("sql", sql);
hljs.registerLanguage("bash", bash);
hljs.registerLanguage("yaml", yaml);
hljs.registerLanguage("markdown", markdown);
defineOptions({ name: "FaMarkdownRenderer" });
interface Props {
@@ -41,7 +66,7 @@ const md: MarkdownIt = new MarkdownIt({
}
return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`;
},
}).use(markdownItHighlightjs);
});
const defaultRender =
md.renderer.rules.link_open ||
@@ -1,6 +1,5 @@
<template>
<div class="flex flex-col h-full">
<!-- 搜索栏 + 操作按钮 -->
<div class="flex flex-col h-full" v-loading="loading">
<div class="mb-3 flex items-center gap-3 shrink-0">
<ElInput
v-model="filterText"
@@ -17,8 +16,7 @@
<ElCheckbox v-model="parentChildLinked">父子联动</ElCheckbox>
</div>
<!-- 菜单树 -->
<div class="flex-1 overflow-auto" v-loading="loading">
<ElScrollbar class="flex-1" :native="false">
<ElTree
ref="treeRef"
node-key="id"
@@ -28,6 +26,7 @@
:default-expand-all="isExpanded"
:filter-node-method="filterNode"
:props="{ children: 'children', label: 'name' }"
@expand-change="handleExpandChange"
>
<template #default="{ data }">
<div class="menu-node flex items-center gap-2">
@@ -41,18 +40,17 @@
</div>
</template>
</ElTree>
</div>
</ElScrollbar>
</div>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from "vue";
import { ref, watch, nextTick, shallowRef } from "vue";
import { Search, Switch as SwitchIcon } from "@element-plus/icons-vue";
import FaMenuRouteIcon from "@/components/others/fa-menu-route-icon/index.vue";
defineOptions({ name: "FaMenuTreeTable" });
// ==================== 类型 ====================
interface MenuNode {
id?: number;
type?: number; // 1=目录 2=菜单 3=按钮 4=链接
@@ -62,7 +60,6 @@ interface MenuNode {
children?: MenuNode[];
}
// el-tree 内部节点结构(仅用到部分字段)
interface TreeNode {
data: MenuNode;
checked: boolean;
@@ -83,10 +80,8 @@ const props = withDefaults(defineProps<Props>(), {
loading: false,
});
// 节点类型常量(1=目录 2=菜单 3=按钮 4=链接)
const isLeaf = (t?: number) => t === 3 || t === 4; // 按钮 / 链接
const isLeaf = (t?: number) => t === 3 || t === 4;
// 节点类型 → 标签样式 / 文案
type TagType = "primary" | "success" | "warning" | "danger" | "info";
const NODE_META: Record<number, { type: TagType; label: string }> = {
1: { type: "warning", label: "目录" },
@@ -96,16 +91,14 @@ const NODE_META: Record<number, { type: TagType; label: string }> = {
};
const nodeMeta = (n: MenuNode) => NODE_META[n.type ?? 2] ?? NODE_META[2];
// ==================== 状态 ====================
const treeRef = ref<any>(null);
const filterText = ref("");
const isExpanded = ref(true);
const isExpanded = ref(false);
const parentChildLinked = ref(true);
const expandedKeys = shallowRef<Set<number>>(new Set());
// 工具:安全获取 el-tree 内部 nodesMap
const getNodesMap = () => treeRef.value?.store?.nodesMap as Record<number, TreeNode> | undefined;
// ==================== 搜索 / 展开 ====================
function filterNode(value: string, data: any) {
if (!value) return true;
return (data.name ?? "").toLowerCase().includes(value.toLowerCase());
@@ -126,8 +119,35 @@ function toggleExpandAll() {
setAllExpanded(isExpanded.value);
}
// ==================== 父级状态计算 ====================
// 单次遍历子节点:统计 fully checked + 是否存在 indeterminate
function handleExpandChange(data: MenuNode, expanded: boolean) {
if (data.id != null) {
if (expanded) {
expandedKeys.value.add(data.id);
} else {
expandedKeys.value.delete(data.id);
}
}
}
function expandMatchingNodes(value: string) {
nextTick(() => {
const tree = treeRef.value;
if (!tree) return;
const nodesMap = getNodesMap();
if (!nodesMap) return;
for (const node of Object.values(nodesMap)) {
if ((node.data.name ?? "").toLowerCase().includes(value.toLowerCase())) {
let p: TreeNode | null = node;
while (p) {
p.expanded = true;
p = p.parent;
}
}
}
});
}
function recomputeNode(p: TreeNode) {
let fully = 0;
let hasIndeterminate = false;
@@ -140,21 +160,18 @@ function recomputeNode(p: TreeNode) {
p.indeterminate = (fully > 0 || hasIndeterminate) && !p.checked;
}
// ==================== 初始化 ====================
// 回显策略:只勾叶子(按钮/链接),父级状态自动向上传播半选
function initFromProps() {
nextTick(() => {
const tree = treeRef.value;
const nodesMap = getNodesMap();
if (!tree || !nodesMap) return;
// 1. 清空
for (const node of Object.values(nodesMap)) {
node.checked = false;
node.indeterminate = false;
node.expanded = expandedKeys.value.has(node.data.id ?? -1);
}
// 2. 勾叶子 + 收集受影响父级(去重,每个父级只重算一次)
const affected = new Set<TreeNode>();
for (const id of props.checkedIds ?? []) {
const node = tree.getNode(id) as TreeNode | null;
@@ -163,21 +180,17 @@ function initFromProps() {
for (let p = node.parent; p; p = p.parent) affected.add(p);
}
// 3. 统一重算
for (const p of affected) recomputeNode(p);
});
}
// ==================== 对外 API ====================
function getCheckedIds(): number[] {
const tree = treeRef.value;
if (!tree) return [];
const ids = new Set<number>();
// 完全选中的菜单 / 按钮 / 链接
for (const n of (tree.getCheckedNodes() ?? []) as MenuNode[]) {
if (n.id != null && n.type !== 1) ids.add(n.id);
}
// 半选父级(菜单 + 目录)—— 作为父级路径传后端
for (const n of (tree.getHalfCheckedNodes() ?? []) as MenuNode[]) {
if (n.id != null) ids.add(n.id);
}
@@ -186,19 +199,20 @@ function getCheckedIds(): number[] {
defineExpose({ getCheckedIds, refresh: initFromProps });
// ==================== 监听 ====================
watch(
() => [props.menuTree, props.checkedIds] as const,
() => props.menuTree,
() => initFromProps(),
{ immediate: true, deep: true }
{ immediate: true }
);
watch(
() => props.checkedIds,
() => initFromProps()
);
// 切换父子联动时重新初始化:check-strictly 改变需要重置半选状态
watch(parentChildLinked, () => initFromProps());
watch(filterText, (val) => {
treeRef.value?.filter(val);
if (val) {
isExpanded.value = true;
setAllExpanded(true);
expandMatchingNodes(val);
}
});
</script>
@@ -28,7 +28,7 @@ import { ComponentSize } from "@/enums/settings/layout.enum";
import { useAppStore } from "@stores";
import { resolveIconForFaSvgIcon } from "@utils";
import { computed } from "vue";
import { ElMessage } from "element-plus";
import { ElMessage } from "@/utils/message";
const { t } = useI18n();
const sizeOptions = computed(() => {
@@ -5,8 +5,6 @@
</template>
<script setup lang="ts">
import { ElTag } from "element-plus";
defineOptions({ name: "FaStatusTag" });
interface Props {
@@ -154,7 +154,7 @@ defineSlots<{
import { ref, reactive, computed } from "vue";
import { useResizeObserver } from "@vueuse/core";
import type { FormInstance, PopoverProps, TableInstance } from "element-plus";
import { ElMessage } from "element-plus";
import { ElMessage } from "@/utils/message";
// 对象类型
export type IObject = Record<string, any>;
@@ -302,10 +302,14 @@ function handleSelect(selection: any[]) {
selectedItems.value = selection;
} else {
// 单选
selectedItems.value = [selection[selection.length - 1]];
const lastItem = selection[selection.length - 1];
selectedItems.value = [lastItem];
tableRef.value?.clearSelection();
tableRef.value?.toggleRowSelection(selectedItems.value[0], true);
tableRef.value?.setCurrentRow(selectedItems.value[0]);
tableRef.value?.toggleRowSelection(
lastItem as Parameters<TableInstance["toggleRowSelection"]>[0],
true
);
tableRef.value?.setCurrentRow(lastItem as Parameters<TableInstance["setCurrentRow"]>[0]);
}
}
function handleSelectAll(selection: any[]) {
@@ -79,7 +79,8 @@
defineOptions({ name: "FaUpload" });
import { ref, watch } from "vue";
import { UploadRawFile, UploadRequestOptions, ElMessage, type UploadUserFile } from "element-plus";
import { ElMessage } from "@/utils/message";
import type { UploadRawFile, UploadRequestOptions, UploadUserFile } from "element-plus";
import { CircleCloseFilled } from "@element-plus/icons-vue";
import ParamsAPI from "@/api/module_system/params";
import { dataURLToFile } from "@utils";