mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 04:39:41 +00:00
chore: 清理冗余代码与配置,优化项目结构
1. 删除无用文件与废弃代码:移除locale枚举、element-plus插件、sse路由、api token模块等 2. 简化类型导入与依赖:移除大量未使用的类型导入,统一echarts导入方式 3. 优化配置与样式:调整gitignore、样式引入顺序,新增列表动画样式 4. 修复接口与模型:修正接口返回类型、查询参数配置,更新部门模型字段 5. 优化性能与体验:添加图片懒加载,优化加载逻辑与表格渲染 6. 调整环境配置:新增并更新开发/生产环境配置文件
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user