feat(web): 升级至 v3.0 并重构组件与引导流程

- 提取应用引导逻辑至 useAppBootstrap 组合式函数
- 增强日期选择器组件,支持全类型与格式自动推导
- 修复资源泄漏:清理 RAF、定时器、事件与 mitt 监听
- 修复退出登录逻辑与异步导出流程
- 迁移 ::v-deep 至 Vue 3 标准 :deep 语法
- 移除调试日志并改进类型定义与注释文档
This commit is contained in:
zhangtao
2026-05-11 23:37:11 +08:00
parent 7e7a5c97cb
commit 9a231aa58c
120 changed files with 2282 additions and 1931 deletions
+29 -6
View File
@@ -22,19 +22,19 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, onMounted } from "vue";
import { computed, onBeforeMount, onMounted, onUnmounted } from "vue";
import { useAppStore, useUserStore } from "./store";
import { useSettingsStore } from "./store/modules/setting.store";
import { defaultSettings } from "./config/setting";
import { ComponentSize } from "./enums/settings/layout.enum";
import AiAssistant from "./components/others/fa-ai-assistant/index.vue";
import { hexToRgba, toggleTransition } from "./utils/ui";
import { checkStorageCompatibility } from "./utils/storage";
import { initializeTheme } from "./hooks/core/useTheme";
import { systemUpgrade } from "./utils/sys";
import { useAppBootstrap } from "@/hooks/core/useAppBootstrap";
import { ThemeMode } from "./enums";
import en from "element-plus/es/locale/lang/en";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import { router } from "@/router";
const appStore = useAppStore();
const settingsStore = useSettingsStore();
@@ -67,14 +67,37 @@ const fontColor = computed(() => {
}
});
/**
* 应用根组件生命周期:
*
* onBeforeMount
* 1. toggleTransition(true) —— 临时禁用页面过渡,避免主题切换时的闪烁
* 2. initializeTheme() —— 加载主题配色(CSS 变量)、暗色模式 class、auto 监听
*
* onMounted
* 1. bootstrap() —— 存储检查 → 过渡恢复 → 版本升级 → 站点配置
* 2. 监听 "app:storage-invalidated" 事件 —— 存储异常时由 storage 模块派发
*/
onBeforeMount(() => {
toggleTransition(true);
initializeTheme();
});
// 存储失效时跳转登录页(由 storage 模块 detect 到异常后派发)
const handleStorageInvalidated = () => {
router.push({ name: "Login" });
};
const { bootstrap } = useAppBootstrap();
onMounted(() => {
checkStorageCompatibility();
toggleTransition(false);
systemUpgrade();
bootstrap();
// 存储检测到异常并已清除数据 → 由路由守卫完成登出清理
window.addEventListener("app:storage-invalidated", handleStorageInvalidated);
});
onUnmounted(() => {
window.removeEventListener("app:storage-invalidated", handleStorageInvalidated);
});
</script>
@@ -1,39 +1,91 @@
<!-- 日期选择器 -->
<!-- 封装日期选择器支持所有 ElDatePicker 类型带快捷选项 -->
<template>
<div class="custom-date-picker">
<ElDatePicker
:model-value="modelValue"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="shortcuts"
:type="pickerType"
:format="pickerFormat"
:value-format="pickerFormat"
:range-separator="rangeSeparator"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
:shortcuts="isRangeType ? shortcuts : undefined"
v-bind="$attrs"
@update:model-value="(val) => emit('update:model-value', val)"
/>
</div>
</template>
<script setup lang="ts">
// 定义组件属性
defineProps({
modelValue: {
type: Array as unknown as () => [Date, Date] | [],
default: () => [],
},
});
import { computed } from "vue";
import { ElDatePicker } from "element-plus";
type DatePickerType =
| "year"
| "month"
| "date"
| "dates"
| "week"
| "datetime"
| "datetimerange"
| "daterange"
| "monthrange";
const props = withDefaults(
defineProps<{
modelValue?: any;
type?: DatePickerType;
format?: string;
rangeSeparator?: string;
startPlaceholder?: string;
endPlaceholder?: string;
}>(),
{
type: "datetimerange",
rangeSeparator: "",
startPlaceholder: "开始日期",
endPlaceholder: "结束日期",
}
);
// 定义事件
const emit = defineEmits(["update:model-value"]);
// 快捷选项配置
/** 是否为范围选择模式 */
const isRangeType = computed(() => {
return ["datetimerange", "daterange", "monthrange"].includes(props.type);
});
/** 根据 type 推导默认格式 */
const pickerFormat = computed(() => {
if (props.format) return props.format;
switch (props.type) {
case "datetime":
case "datetimerange":
return "YYYY-MM-DD HH:mm:ss";
case "date":
case "daterange":
return "YYYY-MM-DD";
case "month":
case "monthrange":
return "YYYY-MM";
case "year":
return "YYYY";
case "week":
return "YYYY-wo";
default:
return "YYYY-MM-DD";
}
});
const pickerType = computed(() => props.type);
/** 快捷选项(仅范围选择器展示) */
const shortcuts = [
{
text: "近一天",
value: () => {
const end = new Date();
const start = new Date();
// 修正:起始时间为当前时间往前 24 小时
start.setTime(start.getTime() - 3600 * 1000 * 24);
return [start, end];
},
@@ -53,13 +53,12 @@
<!-- default slot -->
<slot></slot>
<!-- background image -->
<!-- background image首屏横幅不使用懒加载避免浏览器干预警告 -->
<img
v-if="imageConfig.src"
class="basic-banner__background-image"
:src="imageConfig.src"
:style="{ width: imageConfig.width, bottom: imageConfig.bottom, right: imageConfig.right }"
loading="lazy"
alt="背景图片"
/>
</div>
@@ -51,6 +51,7 @@ const props = withDefaults(defineProps<Props>(), {
const animationDuration = 500;
const currentPercentage = ref(0);
let animFrameId: number | null = null;
const animateProgress = () => {
const startTime = Date.now();
@@ -65,17 +66,26 @@ const animateProgress = () => {
currentPercentage.value = startValue + (endValue - startValue) * progress;
if (progress < 1) {
requestAnimationFrame(animate);
animFrameId = requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
// 取消旧动画后启动新动画
if (animFrameId !== null) cancelAnimationFrame(animFrameId);
animFrameId = requestAnimationFrame(animate);
};
onMounted(() => {
animateProgress();
});
onBeforeUnmount(() => {
if (animFrameId !== null) {
cancelAnimationFrame(animFrameId);
animFrameId = null;
}
});
// 当 percentage 属性变化时重新执行动画
watch(
() => props.percentage,
@@ -222,8 +222,6 @@ const handleMapClick = (params: Record<string, unknown>) => {
level: (data?.level as string) || "",
};
console.log(`选中区域: ${params.name}`, params);
// 高亮选中区域
chartInstance.value?.dispatchAction({
type: "select",
@@ -240,8 +238,14 @@ const resizeChart = () => {
chartInstance.value?.resize();
};
let initTimer: ReturnType<typeof setTimeout> | null = null;
// 处理组件销毁
const cleanupChart = () => {
if (initTimer !== null) {
clearTimeout(initTimer);
initTimer = null;
}
if (chartInstance.value) {
chartInstance.value.off("click", handleMapClick);
chartInstance.value.dispose();
@@ -253,9 +257,10 @@ const cleanupChart = () => {
// 生命周期钩子
onMounted(() => {
if (!isEmpty.value) {
initMap().then(() => {
setTimeout(resizeChart, 100);
});
(async () => {
await initMap();
initTimer = setTimeout(resizeChart, 100);
})();
}
window.addEventListener("resize", resizeChart);
});
@@ -25,28 +25,12 @@
<script setup lang="ts">
import { useAuth } from "@/hooks/core/useAuth";
import type { ButtonMoreItem } from "./types";
defineOptions({ name: "FaButtonMore" });
const { hasAuth } = useAuth();
export interface ButtonMoreItem {
/** 按钮标识,可用于点击事件 */
key: string | number;
/** 按钮文本 */
label: string;
/** 是否禁用 */
disabled?: boolean;
/** 权限标识 */
auth?: string;
/** 图标组件 */
icon?: string;
/** 文本颜色 */
color?: string;
/** 图标颜色(优先级高于 color) */
iconColor?: string;
}
interface Props {
/** 下拉项列表 */
list: ButtonMoreItem[];
@@ -0,0 +1,18 @@
/** FaButtonMore 组件相关类型 */
export interface ButtonMoreItem {
/** 按钮标识,可用于点击事件 */
key: string | number;
/** 按钮文本 */
label: string;
/** 是否禁用 */
disabled?: boolean;
/** 权限标识 */
auth?: string;
/** 图标组件 */
icon?: string;
/** 文本颜色 */
color?: string;
/** 图标颜色(优先级高于 color) */
iconColor?: string;
}
@@ -147,10 +147,6 @@ const onTouchMove = (e: any) => {
}
};
// 全局事件监听器添加
document.addEventListener("touchstart", onTouchStart);
document.addEventListener("touchmove", onTouchMove, { passive: false });
// 获取数值形式的宽度
const getNumericWidth = (): number => {
if (typeof props.width === "string") {
@@ -180,7 +176,7 @@ onMounted(() => {
dragVerify.value?.style.setProperty("--pwidth", -Math.floor(numericWidth / 2) + "px");
});
// 重复添加事件监听器(确保事件绑定)
// 注册 touch 事件监听器,由 onBeforeUnmount 统一清理
document.addEventListener("touchstart", onTouchStart);
document.addEventListener("touchmove", onTouchMove, { passive: false });
});
@@ -97,14 +97,22 @@
</template>
<script setup lang="ts">
/**
* FaForm —— 动态表单组件。
*
* 渲染策略:items 配置项数组 → componentMap[type] 映射组件 → 动态 :is 渲染。
* 支持所有 Element Plus 表单组件类型、自定义 render、插槽插写。
*
* @see SearchFormItem 接口定义
*/
import { useWindowSize } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { toRaw, type Component } from "vue";
import DatePicker from "@/components/DatePicker/index.vue";
import {
ElCascader,
ElCheckbox,
ElCheckboxGroup,
ElDatePicker,
ElInput,
ElInputTag,
ElInputNumber,
@@ -131,10 +139,10 @@ const componentMap = {
checkbox: ElCheckbox, // 复选框
checkboxgroup: ElCheckboxGroup, // 复选框组
radiogroup: ElRadioGroup, // 单选框组
date: ElDatePicker, // 日期选择器
daterange: ElDatePicker, // 日期范围选择器
datetime: ElDatePicker, // 日期时间选择器
datetimerange: ElDatePicker, // 日期时间范围选择器
date: DatePicker, // 日期选择器
daterange: DatePicker, // 日期范围选择器
datetime: DatePicker, // 日期时间选择器
datetimerange: DatePicker, // 日期时间范围选择器
rate: ElRate, // 评分
slider: ElSlider, // 滑块
cascader: ElCascader, // 级联选择器
@@ -113,11 +113,11 @@ import { ArrowUpBold, ArrowDownBold, Refresh, Search } from "@element-plus/icons
import { useWindowSize } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { toRaw, type Component } from "vue";
import DatePicker from "@/components/DatePicker/index.vue";
import {
ElCascader,
ElCheckbox,
ElCheckboxGroup,
ElDatePicker,
ElInput,
ElInputTag,
ElInputNumber,
@@ -144,10 +144,10 @@ const componentMap = {
checkbox: ElCheckbox, // 复选框
checkboxgroup: ElCheckboxGroup, // 复选框组
radiogroup: ElRadioGroup, // 单选框组
date: ElDatePicker, // 日期选择器
daterange: ElDatePicker, // 日期范围选择器
datetime: ElDatePicker, // 日期时间选择器
datetimerange: ElDatePicker, // 日期时间范围选择器
date: DatePicker, // 日期选择器
daterange: DatePicker, // 日期范围选择器
datetime: DatePicker, // 日期时间选择器
datetimerange: DatePicker, // 日期时间范围选择器
rate: ElRate, // 评分
slider: ElSlider, // 滑块
cascader: ElCascader, // 级联选择器
@@ -156,7 +156,7 @@ if (uploadConfig?.isCustomUpload && uploadConfig.server && editorConfig.MENU_CON
},
});
const { url, alt = "", href = "" } = response.data.data ?? ({} as any);
const { url, alt = "", href = "" } = response.data.data ?? ({} as Record<string, string>);
if (!url) {
throw new Error("上传失败,请检查服务端配置");
@@ -177,7 +177,7 @@ const onCreateEditor = (editor: IDomEditor) => {
// 监听全屏事件
editor.on("fullScreen", () => {
console.log("编辑器进入全屏模式");
// 全屏状态由 wangEditor 内部管理
});
// 确保在编辑器创建后应用自定义图标
@@ -128,6 +128,7 @@ onMounted(() => {
});
onUnmounted(() => {
mittBus.off("openSearchDialog", openSearchDialog);
document.removeEventListener("keydown", handleKeydown);
});
@@ -367,27 +368,27 @@ const highlightOnHoverHistory = (index: number) => {
</script>
<style lang="scss" scoped>
.layout-search {
::v-deep(.search-modal) {
:deep(.search-modal) {
background-color: rgb(0 0 0 / 20%);
}
::v-deep(.el-dialog__body) {
:deep(.el-dialog__body) {
padding: 5px 0 0 !important;
}
::v-deep(.el-dialog__header) {
:deep(.el-dialog__header) {
padding: 0;
}
.el-input {
::v-deep(.el-input__wrapper) {
:deep(.el-input__wrapper) {
background-color: var(--fa-gray-200);
border: 1px solid var(--default-border-dashed);
border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
box-shadow: none;
}
::v-deep(.el-input__inner) {
:deep(.el-input__inner) {
color: var(--fa-gray-800) !important;
}
}
@@ -395,18 +396,18 @@ const highlightOnHoverHistory = (index: number) => {
.dark .layout-search {
.el-input {
::v-deep(.el-input__wrapper) {
:deep(.el-input__wrapper) {
background-color: #333;
border: 1px solid #4c4d50;
}
}
::v-deep(.search-modal) {
:deep(.search-modal) {
background-color: rgb(23 23 26 / 60%);
backdrop-filter: none;
}
::v-deep(.el-dialog) {
:deep(.el-dialog) {
background-color: #252526;
}
}
@@ -463,35 +463,35 @@ const openChat = (): void => {
}
/* Hover animation classes */
.refresh-btn:hover ::v-deep(.fa-svg-icon) {
.refresh-btn:hover :deep(.fa-svg-icon) {
animation: rotate180 0.5s;
}
.language-btn:hover ::v-deep(.fa-svg-icon) {
.language-btn:hover :deep(.fa-svg-icon) {
animation: moveUp 0.4s;
}
.setting-btn:hover ::v-deep(.fa-svg-icon) {
.setting-btn:hover :deep(.fa-svg-icon) {
animation: rotate180 0.5s;
}
.full-screen-btn:hover ::v-deep(.fa-svg-icon) {
.full-screen-btn:hover :deep(.fa-svg-icon) {
animation: expand 0.6s forwards;
}
::v-deep(.size-select-btn:hover .fa-svg-icon) {
:deep(.size-select-btn:hover .fa-svg-icon) {
animation: expand 0.6s forwards;
}
.exit-full-screen-btn:hover ::v-deep(.fa-svg-icon) {
.exit-full-screen-btn:hover :deep(.fa-svg-icon) {
animation: shrink 0.6s forwards;
}
.notice-button:hover ::v-deep(.fa-svg-icon) {
.notice-button:hover :deep(.fa-svg-icon) {
animation: shake 0.5s ease-in-out;
}
.chat-button:hover ::v-deep(.fa-svg-icon) {
.chat-button:hover :deep(.fa-svg-icon) {
animation: shake 0.5s ease-in-out;
}
@@ -196,14 +196,17 @@ watch(
function handleLogout(): void {
closeUserMenu();
setTimeout(() => {
ElMessageBox.confirm(t("common.logoutTips"), t("common.tips"), {
confirmButtonText: t("common.confirm"),
cancelButtonText: t("common.cancel"),
customClass: "login-out-dialog",
}).then(() => {
userStore.logout();
});
setTimeout(async () => {
try {
await ElMessageBox.confirm(t("common.logoutTips"), t("common.tips"), {
confirmButtonText: t("common.confirm"),
cancelButtonText: t("common.cancel"),
customClass: "login-out-dialog",
});
await userStore.logout();
} catch {
// 用户取消
}
}, 200);
}
@@ -92,18 +92,18 @@ const filterMenuItems = (items: AppRouteRecord[]): AppRouteRecord[] => {
<style scoped>
/* Remove el-menu bottom border */
::v-deep(.el-menu) {
:deep(.el-menu) {
border-bottom: none !important;
}
/* Remove default styles for first-level menu items */
::v-deep(.el-menu-item[tabindex="0"]) {
:deep(.el-menu-item[tabindex="0"]) {
background-color: transparent !important;
border: none !important;
}
/* Remove bottom border from submenu titles */
::v-deep(.el-menu--horizontal .el-sub-menu__title) {
:deep(.el-menu--horizontal .el-sub-menu__title) {
padding: 0 30px 0 10px !important;
border: 0 !important;
}
@@ -106,7 +106,7 @@ const closeMenu = () => {
</script>
<style scoped>
::v-deep(.el-sub-menu__title .el-sub-menu__icon-arrow) {
:deep(.el-sub-menu__title .el-sub-menu__icon-arrow) {
right: 10px !important;
}
</style>
@@ -248,13 +248,13 @@ onMounted(initScrollState);
</style>
<style scoped>
::v-deep(.el-scrollbar__bar.is-horizontal) {
:deep(.el-scrollbar__bar.is-horizontal) {
bottom: 5px;
display: none;
height: 2px;
}
::v-deep(.scrollbar-wrapper) {
:deep(.scrollbar-wrapper) {
flex: 1;
min-width: 0;
margin: 0 50px 0 30px;
@@ -273,7 +273,7 @@ onMounted(initScrollState);
}
@media (width <= 1440px) {
::v-deep(.scrollbar-wrapper) {
:deep(.scrollbar-wrapper) {
margin: 0 45px;
}
}
@@ -375,18 +375,18 @@ const useTabManagement = (
// 业务逻辑处理
const useBusinessLogic = () => {
const handleNoticeAll = () => {
// 处理查看全部通知
console.log("查看全部通知");
// TODO: 实现查看全部通知逻辑
console.info("[TODO] 查看全部通知");
};
const handleMsgAll = () => {
// 处理查看全部消息
console.log("查看全部消息");
// TODO: 实现查看全部消息逻辑
console.info("[TODO] 查看全部消息");
};
const handlePendingAll = () => {
// 处理查看全部待办
console.log("查看全部待办");
// TODO: 实现查看全部待办逻辑
console.info("[TODO] 查看全部待办");
};
return {
@@ -209,6 +209,7 @@ export function useSettingsPanel() {
const cleanupSettings = () => {
stopWatch();
mittBus.off("openSetting", openSetting);
themeCleanup?.();
cleanup();
};
@@ -83,7 +83,7 @@ const getSettingValue = (key: string) => {
// 统一的设置变更处理
const handleSettingChange = (handlerName: string, value: any) => {
const handler = (basicHandlers as any)[handlerName];
const handler = (basicHandlers as Record<string, (...args: any[]) => any>)[handlerName];
if (typeof handler === "function") {
handler(value);
} else {
@@ -1,4 +1,4 @@
<!-- 三种模式 art-design-pro 一致tab-default / tab-card / tab-google参考 art-design-pro/src/components/core/layouts/art-work-tab -->
<!-- 三种模式tab-default / tab-card / tab-google -->
<template>
<div
v-if="showWorkTab"
@@ -156,6 +156,17 @@
</template>
<script setup lang="ts">
/**
* 工作栏标签页组件:多标签导航 + 右键菜单 + KeepAlive 缓存管理。
*
* 三种模式(通过 settingStore.tabStyle 切换):
* tab-default —— 默认模式,独立标签卡
* tab-card —— 卡片模式,标签有圆角边框
* tab-google —— 谷歌模式,连体标签
*
* 核心流程:路由切换 → setWorktab (utils/navigation) 同步 → 本组件响应式渲染。
* 关闭/切换/Pin 操作全部通过 worktabStore 管理。
*/
import { computed, onMounted, ref, watch, nextTick, onUnmounted } from "vue";
import { LocationQueryRaw, useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
+24 -1
View File
@@ -1,10 +1,19 @@
<!-- 布局容器 -->
<!--
布局根容器三区域结构
- #app-sidebar 左侧菜单导航收起/展开
- #app-main 右侧主区域顶栏 + 页面内容
- #app-header 顶栏面包屑搜索通知用户菜单
- #app-content 页面内容RouterView + 页签
- #app-global 全局浮层层ToastModal新手引导
-->
<template>
<div class="app-layout">
<!-- 左侧菜单导航 -->
<aside id="app-sidebar">
<FaSidebarMenu />
</aside>
<!-- 右侧主区域 -->
<main id="app-main">
<div id="app-header">
<FaHeaderBar />
@@ -14,6 +23,7 @@
</div>
</main>
<!-- 全局浮层层引导通知等跨页面组件 -->
<div id="app-global">
<FaGlobalComponent />
<Guide v-if="guideVisible" v-model="guideVisible" @skip="onGuideFinished" />
@@ -22,6 +32,17 @@
</template>
<script setup lang="ts">
/**
* 布局根组件 组装侧栏顶栏页面内容全局浮层
*
* 依赖
* appStore.guideVisible 控制新手指引显隐session 级状态
* settingStore.showGuide 用户是否关闭指引持久化
*
* 流程
* 首次访问 guideVisible=true 用户完成/跳过指引 onGuideFinished()
* settingStore.showGuide=false 后续不再显示
*/
import { computed } from "vue";
import Guide from "@/components/others/fa-guide/index.vue";
import { useAppStore } from "@stores/modules/app.store";
@@ -32,11 +53,13 @@ defineOptions({ name: "AppLayout" });
const appStore = useAppStore();
const settingStore = useSettingsStore();
/** 新手指引显隐 —— session 级状态,首次登录/注册后自动弹出 */
const guideVisible = computed({
get: () => appStore.guideVisible,
set: (v: boolean) => appStore.showGuide(v),
});
/** 指引完成后持久化标记「不再显示」 */
function onGuideFinished(): void {
settingStore.updateSetting("showGuide", false);
}
@@ -243,7 +243,6 @@ function handleClearAll() {
//
function downloadImg() {
console.log("下载图片");
const a = document.createElement("a");
a.href = temImgPath.value;
a.download = "image.png";
@@ -283,11 +282,11 @@ function downloadImg() {
}
}
::v-deep(.toolBoxControl) {
:deep(.toolBoxControl) {
z-index: 100;
}
::v-deep(.dockMain) {
:deep(.dockMain) {
right: 0;
bottom: -40px;
left: 0;
@@ -297,15 +296,15 @@ function downloadImg() {
opacity: 1;
}
::v-deep(.copyright) {
:deep(.copyright) {
display: none !important;
}
::v-deep(.i-dialog-footer) {
:deep(.i-dialog-footer) {
margin-top: 60px !important;
}
::v-deep(.dockBtn) {
:deep(.dockBtn) {
height: 26px;
padding: 0 10px;
font-size: 12px;
@@ -315,31 +314,31 @@ function downloadImg() {
border: 1px solid var(--el-color-primary-light-4) !important;
}
::v-deep(.dockBtnScrollBar) {
:deep(.dockBtnScrollBar) {
margin: 0 10px 0 6px;
background-color: var(--el-color-primary-light-1);
}
::v-deep(.scrollBarControl) {
:deep(.scrollBarControl) {
border-color: var(--el-color-primary);
}
::v-deep(.closeIcon) {
:deep(.closeIcon) {
line-height: 15px !important;
}
}
.dark {
.cutter-container {
::v-deep(.toolBox) {
:deep(.toolBox) {
border: transparent;
}
::v-deep(.dialogMain) {
:deep(.dialogMain) {
background-color: transparent !important;
}
::v-deep(.i-dialog-footer) {
:deep(.i-dialog-footer) {
.btn {
background-color: var(--el-color-primary) !important;
border: transparent;
@@ -119,7 +119,7 @@ const dialogAttrs = computed(() => {
align-items: center;
margin-left: auto;
::v-deep(.core-overlay-icon-btn.el-button) {
:deep(.core-overlay-icon-btn.el-button) {
min-width: 32px;
padding: 6px;
border-radius: var(--el-border-radius-base);
@@ -99,7 +99,7 @@ const drawerAttrs = computed(() => {
align-items: center;
margin-left: auto;
::v-deep(.core-overlay-icon-btn.el-button) {
:deep(.core-overlay-icon-btn.el-button) {
min-width: 32px;
padding: 6px;
border-radius: var(--el-border-radius-base);
@@ -185,7 +185,7 @@ function handleCloseExportsModal() {
}
//
function handleExports() {
async function handleExports() {
try {
const filename = exportsFormData.filename
? exportsFormData.filename
@@ -204,64 +204,29 @@ function handleExports() {
if (exportsFormData.origin === ExportsOriginEnum.REMOTE) {
const lastFormData = props.queryParams ?? {};
if (props.contentConfig.exportsBlobAction) {
props.contentConfig
.exportsBlobAction(lastFormData)
.then((blob) => {
saveBlobDownload(blob, filename as string);
ElMessage.success("导出成功");
})
.catch((error) => {
console.error("导出远程文件失败:", error);
ElMessage.error("导出远程文件失败");
});
const blob = await props.contentConfig.exportsBlobAction(lastFormData);
saveBlobDownload(blob, filename as string);
ElMessage.success("导出成功");
return;
}
if (props.contentConfig.exportsAction) {
props.contentConfig
.exportsAction(lastFormData)
.then((res) => {
worksheet.addRows(res);
workbook.xlsx
.writeBuffer()
.then((buffer) => {
saveXlsx(buffer, filename as string);
})
.catch((error) => {
console.error("导出远程数据失败:", error);
ElMessage.error("导出远程数据失败");
});
})
.catch((error) => {
console.error("获取远程数据失败:", error);
ElMessage.error("获取远程数据失败");
});
const res = await props.contentConfig.exportsAction(lastFormData);
worksheet.addRows(res);
const buffer = await workbook.xlsx.writeBuffer();
saveXlsx(buffer, filename as string);
} else {
ElMessage.error("未配置 exportsAction 或 exportsBlobAction");
}
} else if (exportsFormData.origin === ExportsOriginEnum.SELECTED) {
const rows = props.selectionData ?? [];
worksheet.addRows(rows);
workbook.xlsx
.writeBuffer()
.then((buffer) => {
saveXlsx(buffer, filename as string);
})
.catch((error) => {
console.error("导出选中数据失败:", error);
ElMessage.error("导出选中数据失败");
});
const buffer = await workbook.xlsx.writeBuffer();
saveXlsx(buffer, filename as string);
} else {
const rows = props.pageData ?? [];
worksheet.addRows(rows);
workbook.xlsx
.writeBuffer()
.then((buffer) => {
saveXlsx(buffer, filename as string);
})
.catch((error) => {
console.error("导出当前数据失败:", error);
ElMessage.error("导出当前数据失败");
});
const buffer = await workbook.xlsx.writeBuffer();
saveXlsx(buffer, filename as string);
}
} catch (error) {
console.error("导出失败:", error);
@@ -254,29 +254,28 @@ function saveXlsx(fileData: any, fileName: string) {
}
//
function handleDownloadTemplate() {
async function handleDownloadTemplate() {
try {
const importTemplate = props.contentConfig.importTemplate;
if (typeof importTemplate === "string") {
window.open(importTemplate);
} else if (typeof importTemplate === "function") {
importTemplate().then((response) => {
const fileData = response.data;
const cd = response.headers?.["content-disposition"] as string | undefined;
let fileName = props.defaultTemplateFileName || "template.xlsx";
if (cd) {
try {
const part = cd.split(";").find((s) => s.trim().startsWith("filename"));
if (part) {
const raw = part.split("=")[1]?.replace(/^"|"$/g, "");
if (raw) fileName = decodeURI(raw);
}
} catch {
/* 使用 defaultTemplateFileName */
const response = await importTemplate();
const fileData = response.data;
const cd = response.headers?.["content-disposition"] as string | undefined;
let fileName = props.defaultTemplateFileName || "template.xlsx";
if (cd) {
try {
const part = cd.split(";").find((s) => s.trim().startsWith("filename"));
if (part) {
const raw = part.split("=")[1]?.replace(/^"|"$/g, "");
if (raw) fileName = decodeURI(raw);
}
} catch {
/* 使用 defaultTemplateFileName */
}
saveXlsx(fileData, fileName);
});
}
saveXlsx(fileData, fileName);
} else {
ElMessage.error("未配置importTemplate");
}
@@ -205,45 +205,45 @@ const handleDeleteEvent = () => {
min-width: 0;
}
::v-deep(.el-calendar__header) {
:deep(.el-calendar__header) {
padding: 6px 4px;
border-bottom: none;
}
::v-deep(.el-calendar__title) {
:deep(.el-calendar__title) {
font-size: 14px;
font-weight: 600;
}
::v-deep(.el-calendar__header .el-button) {
:deep(.el-calendar__header .el-button) {
padding: 4px 8px;
}
::v-deep(.el-calendar__body) {
:deep(.el-calendar__body) {
padding: 2px 0 4px;
}
::v-deep(.el-calendar-table thead th) {
:deep(.el-calendar-table thead th) {
padding: 4px 0;
font-size: 11px;
font-weight: 500;
}
::v-deep(.is-selected) {
:deep(.is-selected) {
background-color: var(--el-color-warning-light-9) !important;
}
::v-deep(.el-calendar-day) {
:deep(.el-calendar-day) {
height: auto;
min-height: 3rem;
padding: 0;
}
::v-deep(.el-calendar-day:hover) {
:deep(.el-calendar-day:hover) {
background-color: transparent !important;
}
::v-deep(.el-dialog__body) {
:deep(.el-dialog__body) {
padding-top: 20px;
}
</style>
@@ -28,18 +28,16 @@ const props = defineProps({
},
});
function handleClipboard() {
async function handleClipboard() {
if (navigator.clipboard && navigator.clipboard.writeText) {
// 使 Clipboard API
navigator.clipboard
.writeText(props.text)
.then(() => {
ElMessage.success(t("common.copySuccess"));
})
.catch((error) => {
ElMessage.warning(t("common.copyFailed"));
console.log("[CopyButton] Copy failed", error);
});
try {
await navigator.clipboard.writeText(props.text);
ElMessage.success(t("common.copySuccess"));
} catch (error) {
ElMessage.warning(t("common.copyFailed"));
console.warn("[CopyButton] Copy failed:", error);
}
} else {
// useClipboard
const input = document.createElement("input");
@@ -57,7 +55,7 @@ function handleClipboard() {
}
} catch (err) {
ElMessage.warning(t("common.copyFailed"));
console.log("[CopyButton] Copy failed.", err);
console.warn("[CopyButton] Copy failed:", err);
} finally {
document.body.removeChild(input);
}
@@ -70,7 +70,7 @@ const renderedContent = computed(() => {
color: var(--el-text-color-primary);
word-wrap: break-word;
::v-deep(pre) {
:deep(pre) {
padding: 12px;
margin: 12px 0;
overflow-x: auto;
@@ -83,7 +83,7 @@ const renderedContent = computed(() => {
}
}
::v-deep(code) {
:deep(code) {
padding: 2px 6px;
font-family: "Courier New", Courier, monospace;
font-size: 13px;
@@ -91,21 +91,21 @@ const renderedContent = computed(() => {
border-radius: 3px;
}
::v-deep(p) {
:deep(p) {
margin: 8px 0;
}
::v-deep(ul),
::v-deep(ol) {
:deep(ul),
:deep(ol) {
padding-left: 24px;
margin: 8px 0;
}
::v-deep(li) {
:deep(li) {
margin: 4px 0;
}
::v-deep(a) {
:deep(a) {
color: var(--el-color-primary);
text-decoration: none;
@@ -114,14 +114,14 @@ const renderedContent = computed(() => {
}
}
::v-deep(blockquote) {
:deep(blockquote) {
padding: 8px 16px;
margin: 12px 0;
background: var(--el-fill-color-light);
border-left: 4px solid var(--el-color-primary);
}
::v-deep(table) {
:deep(table) {
width: 100%;
margin: 12px 0;
border-collapse: collapse;
@@ -384,7 +384,7 @@ defineExpose({
}
.menu-item.is-disabled i:not(.submenu-arrow),
.menu-item.is-disabled ::v-deep(.fa-svg-icon) {
.menu-item.is-disabled :deep(.fa-svg-icon) {
color: var(--el-text-color-disabled) !important;
}
@@ -124,7 +124,7 @@
</template>
</ElTable>
<!-- 分页 -->
<pagination
<FaPagination
v-model:total="total"
v-model:page="queryParams.page_no"
v-model:limit="queryParams.page_size"
@@ -153,6 +153,7 @@ defineSlots<{
import { ref, reactive, computed } from "vue";
import { useResizeObserver } from "@vueuse/core";
import type { FormInstance, PopoverProps, TableInstance } from "element-plus";
import FaPagination from "@/components/others/fa-pagination/index.vue";
//
export type IObject = Record<string, any>;
@@ -264,21 +265,19 @@ function handleQuery() {
}
//
function fetchPageData(isRestart = false) {
async function fetchPageData(isRestart = false) {
loading.value = true;
if (isRestart) {
queryParams.page_no = 1;
queryParams.page_size = page_size;
}
props.selectConfig
.indexAction(queryParams)
.then((data) => {
total.value = data.total;
pageData.value = data.list;
})
.finally(() => {
loading.value = false;
});
try {
const data = await props.selectConfig.indexAction(queryParams);
total.value = data.total;
pageData.value = data.list;
} finally {
loading.value = false;
}
}
//
@@ -352,8 +351,8 @@ const popoverContentRef = ref();
</script>
<style scoped lang="scss">
.reference ::v-deep(.el-input__wrapper),
.reference ::v-deep(.el-input__inner) {
.reference :deep(.el-input__wrapper),
.reference :deep(.el-input__inner) {
cursor: pointer;
}
@@ -363,7 +362,7 @@ const popoverContentRef = ref();
margin-top: 6px;
}
//
.radio ::v-deep(.el-table__header th.el-table__cell:nth-child(1) .el-checkbox) {
.radio :deep(.el-table__header th.el-table__cell:nth-child(1) .el-checkbox) {
visibility: hidden;
}
</style>
@@ -464,7 +464,7 @@ const onError = (error: any) => {
}
}
::v-deep(.el-upload--picture-card) {
:deep(.el-upload--picture-card) {
position: relative;
width: v-bind("props.style.width ?? '150px'");
height: v-bind("props.style.height ?? '150px'");
@@ -4,7 +4,7 @@
- 暴露 `elTableRef``scrollToTop`formatter 列见 TableFormatterOutlet 注释
-->
<template>
<div class="fa-table">
<div class="fa-table" :class="{ 'is-empty': isEmpty }">
<div class="fa-table__main">
<VueDraggable
class="fa-table__drag-wrap"
@@ -270,7 +270,7 @@ useResizeObserver(tableHeaderRef, (entries) => {
// showTableHeader
const PAGINATION_SPACING = computed(() => (props.showTableHeader ? 6 : 15));
// 使 Hook
// 使 Hook
useTableHeight({
showTableHeader: computed(() => props.showTableHeader),
paginationHeight,
@@ -286,7 +286,7 @@ const height = computed(() => {
if (isEmpty.value && !props.loading) return props.emptyHeight;
// 使
if (props.height) return props.height;
//
// flex .fa-table__main ElTable 100%
return "100%";
});
@@ -345,7 +345,7 @@ const onRowDragEnd = () => {
};
//
const showPagination = computed(() => props.pagination && !isEmpty.value);
const showPagination = computed(() => !!props.pagination);
// Element Plus $index = -1
// ElForm lineList.-1.xxx
@@ -1,10 +1,24 @@
.fa-table {
position: relative;
height: 100%;
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
.fa-table__main {
flex: 1;
min-height: 0;
overflow: hidden;
padding-top: 10px;
// VueDraggable 透传高度确保 ElTable height: 100% 能正确解析
> * {
height: 100%;
}
}
.el-table {
height: 100%;
margin-top: 10px;
}
:deep(.el-loading-mask) {
@@ -23,14 +37,17 @@
// 空状态垂直居中
&.is-empty {
:deep(.el-scrollbar__wrap) {
:deep(.el-table__body-wrapper) {
display: flex;
align-items: center;
justify-content: center;
}
}
.pagination {
display: flex;
margin-top: 13px;
flex-shrink: 0;
padding-top: 13px;
:deep(.el-select) {
width: 102px !important;
@@ -88,7 +88,7 @@ watchEffect(() => {
width: 100%;
height: 100%;
::v-deep(svg) {
:deep(svg) {
width: 100%;
height: 100%;
}
@@ -256,7 +256,7 @@ const changeThemeColor = (color: string) => {
transform: translateY(-2px);
}
.auth-top-bar__action:hover ::v-deep(.fa-svg-icon) {
.auth-top-bar__action:hover :deep(.fa-svg-icon) {
color: var(--el-color-primary);
}
@@ -314,15 +314,15 @@ const changeThemeColor = (color: string) => {
}
/* 调色盘:图标颜色与当前主题主色一致(含单独悬浮、整块调色区悬浮) */
.palette-btn ::v-deep(.fa-svg-icon) {
.palette-btn :deep(.fa-svg-icon) {
color: v-bind("themeColorForCss");
}
.auth-top-bar__action.palette-btn:hover ::v-deep(.fa-svg-icon) {
.auth-top-bar__action.palette-btn:hover :deep(.fa-svg-icon) {
color: v-bind("themeColorForCss");
}
.color-picker-expandable:hover .palette-btn ::v-deep(.fa-svg-icon) {
.color-picker-expandable:hover .palette-btn :deep(.fa-svg-icon) {
color: v-bind("themeColorForCss");
}
</style>
@@ -46,6 +46,14 @@ import hljs from "highlight.js";
export type HighlightDirective = Directive<HTMLElement>;
/** 扩展 HTMLElement 类型,消除指令内部 _highlightActive / _highlightObserver 的 as any 断言 */
declare global {
interface HTMLElement {
_highlightActive?: boolean;
_highlightObserver?: MutationObserver;
}
}
// 高亮代码
function highlightCode(block: HTMLElement) {
hljs.highlightElement(block);
@@ -68,12 +76,15 @@ function addCopyButton(block: HTMLElement) {
copyButton.className = "copy-button";
copyButton.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 0 1 3 21l.003-14c0-.552.45-1 1.006-1zM5.002 8L5 20h10V8zM9 6h8v10h2V4H9z"/></svg>';
copyButton.onclick = () => {
copyButton.onclick = async () => {
// 过滤掉行号,只复制代码内容
const codeContent = block.innerText.replace(/^\d+\s+/gm, "");
navigator.clipboard.writeText(codeContent).then(() => {
try {
await navigator.clipboard.writeText(codeContent);
ElMessage.success("复制成功");
});
} catch {
// 剪贴板写入被拒绝时静默失败(浏览器权限限制)
}
};
const preElement = block.parentElement;
@@ -125,6 +136,7 @@ function processBlock(block: HTMLElement) {
// 查找并处理所有代码块
function processAllCodeBlocks(el: HTMLElement) {
if (!el._highlightActive) return;
const blocks = Array.from(el.querySelectorAll<HTMLElement>("pre code"));
const unprocessedBlocks = blocks.filter((block) => !isBlockProcessed(block));
@@ -141,6 +153,7 @@ function processAllCodeBlocks(el: HTMLElement) {
let currentIndex = 0;
const processBatch = () => {
if (!el._highlightActive) return; // 组件已卸载则跳过
const batch = unprocessedBlocks.slice(currentIndex, currentIndex + batchSize);
batch.forEach((block) => {
@@ -165,6 +178,7 @@ function retryProcessing(el: HTMLElement, maxRetries: number = 3, delay: number
let retryCount = 0;
const tryProcess = () => {
if (!el._highlightActive) return; // 组件已卸载则跳过
processAllCodeBlocks(el);
// 检查是否还有未处理的代码块
@@ -172,7 +186,7 @@ function retryProcessing(el: HTMLElement, maxRetries: number = 3, delay: number
(block) => !isBlockProcessed(block)
);
if (remainingBlocks.length > 0 && retryCount < maxRetries) {
if (remainingBlocks.length > 0 && retryCount < maxRetries && el._highlightActive) {
retryCount++;
setTimeout(tryProcess, delay * retryCount); // 递增延迟
}
@@ -184,11 +198,15 @@ function retryProcessing(el: HTMLElement, maxRetries: number = 3, delay: number
// 代码高亮、插入行号、复制按钮
const highlightDirective: HighlightDirective = {
mounted(el: HTMLElement) {
// 标记元素活跃,用于 unmounted 时阻止 pending 回调执行
el._highlightActive = true;
// 立即尝试处理一次
processAllCodeBlocks(el);
// 延迟处理,确保 v-html 内容已经渲染
setTimeout(() => {
if (!el._highlightActive) return;
retryProcessing(el);
}, 100);
@@ -213,6 +231,7 @@ const highlightDirective: HighlightDirective = {
if (hasNewCodeBlocks) {
// 延迟处理新添加的代码块
setTimeout(() => {
if (!el._highlightActive) return;
processAllCodeBlocks(el);
}, 50);
}
@@ -225,22 +244,25 @@ const highlightDirective: HighlightDirective = {
});
// 将 observer 存储到元素上,以便在 unmounted 时清理
(el as any)._highlightObserver = observer;
el._highlightObserver = observer;
},
updated(el: HTMLElement) {
// 当组件更新时,重新处理代码块
setTimeout(() => {
if (!el._highlightActive) return;
processAllCodeBlocks(el);
}, 50);
},
unmounted(el: HTMLElement) {
// 标记失活,阻止所有 pending timeout/rAF 回调执行
el._highlightActive = false;
// 清理 MutationObserver
const observer = (el as any)._highlightObserver;
const observer = el._highlightObserver;
if (observer) {
observer.disconnect();
delete (el as any)._highlightObserver;
delete el._highlightObserver;
}
},
};
@@ -0,0 +1,23 @@
/**
* useAppBootstrap
*
* onMounted
* App.vue onMounted
*/
import { useSiteConfig } from "@/hooks/core/useSiteConfig";
import { checkStorageCompatibility } from "@utils/storage";
import { toggleTransition } from "@utils/ui";
import { systemUpgrade } from "@utils/sys";
export function useAppBootstrap() {
const { initSiteConfig } = useSiteConfig();
const bootstrap = () => {
checkStorageCompatibility();
toggleTransition(false);
systemUpgrade();
initSiteConfig();
};
return { bootstrap };
}
+2 -1
View File
@@ -46,7 +46,8 @@ export const useAuth = () => {
const { info } = storeToRefs(userStore);
// 前端按钮权限(例如:['add', 'edit']
const frontendAuthList: string[] = ((info.value as any)?.permissions as string[]) ?? [];
const frontendAuthList: string[] =
((info.value as Record<string, any>)?.permissions as string[]) ?? [];
// 后端路由 meta 配置的权限列表(例如:[{ authMark: 'add' }]
const backendAuthList: AuthItem[] = Array.isArray(route.meta.authList)
+9 -4
View File
@@ -198,10 +198,15 @@ export function useChart(options: UseChartOptions = {}) {
});
// 缓存样式配置以减少重复计算
const styleCache = {
axisLine: null as any,
splitLine: null as any,
axisLabel: null as any,
const styleCache: {
axisLine: Record<string, any> | null;
splitLine: Record<string, any> | null;
axisLabel: Record<string, any> | null;
lastDarkValue: boolean;
} = {
axisLine: null,
splitLine: null,
axisLabel: null,
lastDarkValue: isDark.value,
};
+1 -1
View File
@@ -123,7 +123,7 @@ export function useHeaderBar() {
// 获取快速入口的最小宽度
const fastEnterMinWidth = computed(() => {
const config = getFeatureConfig("fastEnter");
return (config as any)?.minWidth || 1200;
return (config as Record<string, any>)?.minWidth || 1200;
});
/**
@@ -78,10 +78,8 @@ export function useLayoutHeight(options: LayoutHeightOptions = {}) {
* ID
*
*
* @param headerIds ID
* @param headerIds ID ["app-header", "app-content-header"]
* @param options
*
* ```
*/
export function useAutoLayoutHeight(
headerIds: string[] = ["app-header", "app-content-header"],
@@ -120,8 +118,8 @@ export function useAutoLayoutHeight(
if (typeof document !== "undefined") {
// 使用 nextTick 确保 DOM 完全渲染
requestAnimationFrame(() => {
const header = document.getElementById(headerIds[0]);
const contentHeader = document.getElementById(headerIds[1]);
const header = headerIds.length > 0 ? document.getElementById(headerIds[0]) : null;
const contentHeader = headerIds.length > 1 ? document.getElementById(headerIds[1]) : null;
if (header) {
headerRef.value = header;
@@ -0,0 +1,45 @@
/**
* useSiteConfig - + favicon
*
* configStore favicon
* watch
*
* App.vue onMounted
*/
import { watch } from "vue";
import { useConfigStore } from "@stores/modules/config.store";
const updateFavicon = (url: string) => {
const link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
if (link) link.href = url;
};
const syncFromConfig = () => {
const { sys_web_title, sys_web_favicon } = useConfigStore().configData;
if (sys_web_title?.config_value) document.title = sys_web_title.config_value;
if (sys_web_favicon?.config_value) updateFavicon(sys_web_favicon.config_value);
};
export function useSiteConfig() {
const configStore = useConfigStore();
/** 初始化:拉取配置并同步标题/favicon */
const initSiteConfig = async () => {
try {
await configStore.getConfig();
syncFromConfig();
} catch (error) {
console.error("[SiteConfig] 获取配置失败:", error);
}
};
/** 配置更新后自动同步(管理员后台修改配置后重新拉取时) */
watch(
() => configStore.configData,
() => syncFromConfig(),
{ deep: false }
);
return { initSiteConfig };
}
+25 -34
View File
@@ -1,43 +1,34 @@
import App from "./App.vue";
import { createApp } from "vue";
// ---------------------------------------------------------------------------
// 样式(顺序:tailwind 基础 → 项目全局 → Element Plus 暗色 → 动画库)
// ---------------------------------------------------------------------------
import "@styles/core/tailwind.css";
import "@styles/index.scss";
import "element-plus/theme-chalk/dark/css-vars.css";
import "animate.css";
// ---------------------------------------------------------------------------
// 应用初始化
// ---------------------------------------------------------------------------
import App from "./App.vue";
import { createApp } from "vue";
import { printConsoleBanner } from "@utils/sys";
import { initPlugins } from "@/plugins";
import { useConfigStore } from "./store/modules/config.store";
/**
* iOS Safari touch passive `:active` CSS
* `touchstart` passive `:active`
*/
document.addEventListener("touchstart", function () {}, { passive: false });
const app = createApp(App);
printConsoleBanner();
initPlugins(app);
app.mount("#app");
/** 挂载后拉取站点标题 / favicon(依赖 Pinia 与接口) */
const setTitleAndFavicon = async () => {
try {
const configStore = useConfigStore();
await configStore.getConfig();
const webTitle = configStore.configData.sys_web_title?.config_value;
const webFavicon = configStore.configData.sys_web_favicon?.config_value;
if (webTitle) {
document.title = webTitle;
}
if (webFavicon) {
document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach((node) => {
if (node instanceof HTMLLinkElement) {
node.href = webFavicon;
}
});
}
} catch (error) {
console.error("获取配置数据失败:", error);
}
};
void setTitleAndFavicon();
/**
* 1. printConsoleBanner
* 2. initPlugins Vue Pinia Router Element Plus
* 3. mount
* App.vue onMounted /favicon Pinia store
*/
(async () => {
const app = createApp(App);
printConsoleBanner();
await initPlugins(app);
app.mount("#app");
})();
+15 -2
View File
@@ -20,10 +20,23 @@ import { initElementPlus } from "./element-plus";
import { initElIcons } from "./icons";
import { initTerminal } from "./terminal";
export function initPlugins(app: App<Element>): void {
/**
*
*
* 1. initElIcons
* 2. initStore Pinia store router
* 3. initRouter Vue Router store
* 4. initGlobDirectives v-authv-highlight router meta
* 5. initErrorHandle window.onerrorunhandledrejection
* 6. initTerminal /
* 7. initI18n Element Plus Element Plus
* 8. initCodeMirror CodeMirror
* 9. initElementPlus Element Plus
*/
export async function initPlugins(app: App<Element>): Promise<void> {
initElIcons(app);
initStore(app);
initRouter(app);
await initRouter(app);
initGlobDirectives(app);
initErrorHandle(app);
initTerminal(app);
+4 -2
View File
@@ -7,12 +7,11 @@ import { Router } from "vue-router";
import { NProgress } from "@utils/ui";
import { useCommon } from "@/hooks/core/useCommon";
import { loadingService } from "@utils/ui";
import { getPendingLoading, resetPendingLoading } from "./beforeEach";
/** 防止重复注册 afterEach(与 beforeEach 同理) */
let afterEachGuardRegistered = false;
export function setupAfterEachGuard(router: Router) {
export async function setupAfterEachGuard(router: Router) {
if (afterEachGuardRegistered) {
if (import.meta.env.DEV) {
console.warn("[Router] setupAfterEachGuard 已注册,跳过重复调用");
@@ -23,6 +22,9 @@ export function setupAfterEachGuard(router: Router) {
const { scrollToTop } = useCommon();
// 延迟加载 beforeEach 中导出的守卫状态函数,避免静态循环依赖
const { getPendingLoading, resetPendingLoading } = await import("./beforeEach");
router.afterEach(() => {
scrollToTop();
+40 -7
View File
@@ -1,6 +1,23 @@
/**
*
* `setupBeforeEachGuard` `handleRouteGuard`
*
*
*
* 1. storage
* 2. &
* 3. fetch addRoute
* 4. `/`
* 5.
* 6. 404 / 500
*
*
* setupBeforeEachGuard() `router.beforeEach`
* handleRouteGuard()
* checkStorageInvalidated()
* handleLoginStatus()
* routeInitFailed
* handleDynamicRoutes() + addRoute
* handleRootPathRedirect()
* setWorktab / setPageTitle / 404
*/
import type { AppRouteRecord } from "@/types/router";
import type { Router, RouteLocationNormalized, NavigationGuardNext } from "vue-router";
@@ -19,20 +36,33 @@ import { UserAPI } from "@/api/module_system/user";
import { ApiStatus, isHttpError } from "@utils/http";
import { RouteRegistry } from "./dynamicRoutes";
import { MenuProcessor } from "./MenuProcessor";
import { checkStorageInvalidated } from "@utils/storage";
import { resetStorageInvalidated, checkStorageInvalidated } from "@utils/storage";
// --- 模块级单例与守卫状态 ---
/** 动态路由注册表(惰性创建,首次导航时生成) */
let routeRegistry: RouteRegistry | null = null;
/** 菜单数据处理器(不含注册逻辑,只做列表拉取 + 树形组装) */
const menuProcessor = new MenuProcessor();
/** 供 afterEach 关闭全局 loading */
/**
* loading beforeEach
* afterEach
*/
let pendingLoading = false;
/** 动态路由拉取失败后为 true,避免反复请求造成死循环 */
/**
* / true
* 500
* `resetRouteInitState()`
*/
let routeInitFailed = false;
/** 并发导航时只允许一路执行动态路由初始化 */
/**
*
* `next(false)`
*/
let routeInitInProgress = false;
export function getPendingLoading(): boolean {
@@ -123,7 +153,10 @@ async function handleRouteGuard(
// 检查存储是否已失效(storage/index.ts 检测到异常时标记)
if (checkStorageInvalidated()) {
console.info("[RouteGuard] 检测到存储已失效,执行登出");
await userStore.logout();
// 传 { navigate: false } 防止 logout 内部调用 router.push() 造成重复导航,
// 导航由本守卫通过 next() 统一控制
await userStore.logout({ navigate: false });
resetStorageInvalidated();
next({ name: "Login", replace: true });
return;
}
+6 -2
View File
@@ -20,8 +20,12 @@ export interface ValidationResult {
warnings: string[];
}
/** 菜单注册前校验:重名、缺少 component、深层误用 layout 占位等 */
/**
* component layout
* errors warnings
*/
export class RouteValidator {
/** 已警告过的路由集合,避免重复打印校验警告 */
private warnedRoutes = new Set<string>();
validate(routes: AppRouteRecord[]): ValidationResult {
@@ -204,7 +208,7 @@ export class ComponentLoader {
onMounted(() => {
const iframeRoute = IframeRouteManager.getInstance().findByPath(route.path);
if (iframeRoute?.meta) {
iframeUrl.value = (iframeRoute.meta as any).link || "";
iframeUrl.value = iframeRoute.meta.link || "";
}
});
+7 -3
View File
@@ -1,13 +1,17 @@
import type { App } from "vue";
import { createRouter, createWebHashHistory } from "vue-router";
import { HOME_ROUTE_NAME, ROOT_LAYOUT_ROUTE_NAME, staticRoutes } from "./staticRoutes";
import { setupBeforeEachGuard } from "./beforeEach";
import { setupAfterEachGuard } from "./afterEach";
import "@utils/ui";
/**
* `staticRoutes` `beforeEach` `RouteRegistry`
* `initRouter` / `app.use(router)`
*
* Hash createWebHashHistory History
* - URL NGINX try_files
* - Electron HTTP
* - HMR
*/
export const router = createRouter({
history: createWebHashHistory(),
@@ -15,7 +19,8 @@ export const router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
});
export function initRouter(app: App<Element>): void {
export async function initRouter(app: App<Element>): Promise<void> {
const { setupBeforeEachGuard } = await import("./beforeEach");
setupBeforeEachGuard(router);
setupAfterEachGuard(router);
app.use(router);
@@ -31,4 +36,3 @@ export { RouteRegistry, ComponentLoader, RouteTransformer, RouteValidator } from
export type { ValidationResult } from "./dynamicRoutes";
export { IframeRouteManager } from "./staticRoutes";
export { MenuProcessor, builtinFrontendRoutes } from "./MenuProcessor";
export { RoutePermissionValidator } from "./beforeEach";
+9 -1
View File
@@ -1,3 +1,11 @@
/**
* + IframeRouteManager
*
* = Layout404/500iframe
* 访
*
* `beforeEach.ts` `RouteRegistry` `addRoute`
*/
import type { AppRouteRecordRaw } from "@utils/navigation";
import type { AppRouteRecord, RouteMeta } from "@/types/router";
import { defineComponent, h, onMounted, ref } from "vue";
@@ -109,7 +117,7 @@ const IframeView = defineComponent({
onMounted(() => {
const iframeRoute = IframeRouteManager.getInstance().findByPath(route.path);
if (iframeRoute?.meta) {
iframeUrl.value = (iframeRoute.meta as any).link || "";
iframeUrl.value = iframeRoute.meta.link || "";
}
});
+1 -2
View File
@@ -2,7 +2,6 @@ import type { App } from "vue";
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import { router } from "@/router";
import { resetDynamicRoutesSync } from "@/router/beforeEach";
import { useUserStore } from "./modules/user.store";
import { useDictStore } from "./modules/dict.store";
import { useNoticeStore } from "./modules/notice.store";
@@ -28,7 +27,6 @@ export * from "./modules/user.store";
export * from "./modules/worktab.store";
export { store };
export { useUserStore, useDictStore, useNoticeStore, useConfigStore, useWorktabStore };
export interface RefreshCacheOptions {
dictTypes?: string[];
@@ -75,6 +73,7 @@ export async function refreshAppCaches(opts: RefreshCacheOptions = {}) {
await Promise.allSettled(tasks);
if (refreshRoutes) {
const { resetDynamicRoutesSync } = await import("@/router/beforeEach");
resetDynamicRoutesSync();
await router.replace({
path: router.currentRoute.value.path,
@@ -142,11 +142,11 @@ export const useSettingsStore = defineStore(
});
const getMenuOpenWidth = computed((): string => {
return menuOpenWidth.value + "px" || SETTING_DEFAULT_CONFIG.menuOpenWidth + "px";
return (menuOpenWidth.value ?? SETTING_DEFAULT_CONFIG.menuOpenWidth) + "px";
});
const getCustomRadius = computed((): string => {
return customRadius.value + "rem" || SETTING_DEFAULT_CONFIG.customRadius + "rem";
return (customRadius.value ?? SETTING_DEFAULT_CONFIG.customRadius) + "rem";
});
/** festivalDate 存「上次完成烟花播放」的自然日 YYYY-MM-DD,与今天相同则当天不再播 */
@@ -175,9 +175,13 @@ export const useSettingsStore = defineStore(
watch(
[theme, themeColor],
([newTheme, newThemeColor]) => {
toggleDarkMode(newTheme === ThemeMode.DARK);
const colors = generateThemeColors(newThemeColor, newTheme);
applyTheme(colors);
try {
toggleDarkMode(newTheme === ThemeMode.DARK);
const colors = generateThemeColors(newThemeColor, newTheme);
applyTheme(colors);
} catch (error) {
console.error("[SettingStore] 主题初始化失败:", error);
}
},
{ immediate: true }
);
@@ -1,6 +1,11 @@
/**
* `FaTable` / `FaTableHeader` store
* options.persist
*
*
* Pinia store
* /
*
* options.persist localStorage "tableStore"
*/
import { defineStore } from "pinia";
import { ref } from "vue";
+27 -24
View File
@@ -8,7 +8,6 @@ import { useWorktabStore } from "./worktab.store";
import { useMenuStore } from "./menu.store";
import { AppRouteRecord } from "@/types/router";
import { setPageTitle } from "@utils/navigation";
import { resetRouterState, resetRouteInitState } from "@/router/beforeEach";
import { StorageConfig } from "@utils/storage";
import AuthAPI from "@/api/module_system/auth";
import UserAPI from "@/api/module_system/user";
@@ -19,6 +18,13 @@ import { ElNotification } from "element-plus";
import { store, useDictStore } from "@stores";
import type { UserInfo } from "@/api/module_system/user";
/** 延迟加载 beforeEach 工具函数,避免 user.store 与 beforeEach 的循环依赖 */
let _routerUtilsPromise: Promise<typeof import("@/router/beforeEach")> | null = null;
const getRouterUtils = () => {
if (!_routerUtilsPromise) _routerUtilsPromise = import("@/router/beforeEach");
return _routerUtilsPromise;
};
/** {@link useUserStore} 的 `logout` 可选参数 */
export interface LogoutOptions {
/**
@@ -59,14 +65,17 @@ export const useUserStore = defineStore(
const hasGetRoute = ref(false);
// 记住我状态
const rememberMe = ref(Auth.getRememberMe());
// 计算属性:基础用户信息(兼容 web 原有结构)
const basicInfo = computed(() => info.value as any);
/** info 扩展类型:兼容 API 返回 `user_id`(非标准 UserInfo 字段) */
type UserInfoLike = Partial<UserInfo> & Record<string, any>;
// 计算属性:基础用户信息
const basicInfo = computed(() => info.value as UserInfoLike);
// 计算属性:获取设置状态
const getSettingState = computed(() => useSettingsStore().$state);
// 计算属性:获取工作台状态
const getWorktabState = computed(() => useWorktabStore().$state);
// 计算属性:获取基础信息(兼容 web 项目)
const getBasicInfo = computed(() => info.value as any);
// 计算属性:获取基础信息
const getBasicInfo = computed(() => info.value as UserInfoLike);
// 计算属性:获取路由列表
const getRouteList = computed(() => routeList.value);
// 计算属性:获取权限列表
@@ -144,7 +153,8 @@ export const useUserStore = defineStore(
*/
const checkAndClearWorktabs = () => {
const lastUserId = localStorage.getItem(StorageConfig.LAST_USER_ID_KEY);
const currentUserId = (info.value as any).id || (info.value as any).user_id;
const ui = info.value as UserInfoLike;
const currentUserId = ui.id || ui.user_id;
// 无法获取当前用户 ID,跳过检查
if (!currentUserId) return;
@@ -241,7 +251,7 @@ export const useUserStore = defineStore(
}
rememberMe.value = loginForm.remember;
// 清除上次会话里「动态路由初始化失败」标记,避免重新登录后侧栏/菜单不注册
resetRouteInitState();
(await getRouterUtils()).resetRouteInitState();
Auth.setTokens(data.access_token, data.refresh_token, rememberMe.value);
setToken(data.access_token, data.refresh_token);
setLoginStatus(true);
@@ -254,7 +264,8 @@ export const useUserStore = defineStore(
async function logout(options?: LogoutOptions) {
const shouldNavigate = options?.navigate !== false;
const currentUserId = (info.value as any).id || (info.value as any).user_id;
const ui = info.value as UserInfoLike;
const currentUserId = ui.id || ui.user_id;
if (currentUserId) {
localStorage.setItem(StorageConfig.LAST_USER_ID_KEY, String(currentUserId));
}
@@ -279,7 +290,7 @@ export const useUserStore = defineStore(
resetAllState();
sessionStorage.removeItem("iframeRoutes");
useMenuStore().setHomePath("");
resetRouterState(500);
(await getRouterUtils()).resetRouterState(500);
if (shouldNavigate) {
const currentRoute = router.currentRoute.value;
@@ -325,26 +336,18 @@ export const useUserStore = defineStore(
/**
* token
*/
function refreshTokenFn() {
async function refreshTokenFn() {
const currentRefreshToken = Auth.getRefreshToken();
if (!currentRefreshToken) {
return Promise.reject(new Error("没有有效的刷新令牌"));
throw new Error("没有有效的刷新令牌");
}
return new Promise<void>((resolve, reject) => {
AuthAPI.refreshToken({ refresh_token: currentRefreshToken })
.then((response: any) => {
const data = response.data.data;
// 更新令牌,保持当前记住我状态
Auth.setTokens(data.access_token, data.refresh_token, Auth.getRememberMe());
setToken(data.access_token, data.refresh_token);
resolve();
})
.catch((error: any) => {
reject(error);
});
});
const response = await AuthAPI.refreshToken({ refresh_token: currentRefreshToken });
const data = response.data.data;
// 更新令牌,保持当前记住我状态
Auth.setTokens(data.access_token, data.refresh_token, Auth.getRememberMe());
setToken(data.access_token, data.refresh_token);
}
/**
+1 -1
View File
@@ -187,7 +187,7 @@
.el-card__body {
display: flex;
flex: 1 1 auto;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
overflow: hidden;
+11 -1
View File
@@ -1,4 +1,14 @@
/** Auth utilities (web-style, flat module). */
/**
* Auth
*
* localStorage / sessionStorage @utils/storage
* Storage
* 1. 使access_token / refresh_token
* 2. rememberMe localStorage / sessionStorage
* 3. JSON /
*
* @module Auth
*/
const AUTH_KEYS = {
ACCESS_TOKEN: "access_token",
+41 -40
View File
@@ -1,7 +1,7 @@
/**
* axios + blob Vue
*/
import axios, { AxiosResponse } from "axios";
import axios from "axios";
import { ElLoading, ElMessage } from "element-plus";
import { saveAs as fileSaverSaveAs } from "file-saver";
import { Auth } from "@utils/auth";
@@ -20,70 +20,71 @@ interface DownloadUtil {
}
const download: DownloadUtil = {
name(name: string, isDelete: boolean = true): void {
async name(name: string, isDelete: boolean = true) {
const url =
baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete;
axios({
method: "get",
url,
responseType: "blob",
headers: { Authorization: "Bearer " + Auth.getAccessToken() },
}).then((res: AxiosResponse<Blob>) => {
try {
const res = await axios.get<Blob>(url, {
responseType: "blob",
headers: { Authorization: `Bearer ${Auth.getAccessToken()}` },
});
const isBlob = blobValidate(res.data);
if (isBlob) {
const blob = new Blob([res.data]);
download.saveAs(blob, decodeURIComponent(res.headers["download-filename"]));
} else {
download.printErrMsg(res.data);
await download.printErrMsg(res.data);
}
});
} catch (error) {
console.error("[Download] 文件下载失败:", error);
ElMessage.error("下载文件失败,请稍后重试");
}
},
resource(resource: string): void {
async resource(resource: string) {
const url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource);
axios({
method: "get",
url,
responseType: "blob",
headers: { Authorization: "Bearer " + Auth.getAccessToken() },
}).then((res: AxiosResponse<Blob>) => {
try {
const res = await axios.get<Blob>(url, {
responseType: "blob",
headers: { Authorization: `Bearer ${Auth.getAccessToken()}` },
});
const isBlob = blobValidate(res.data);
if (isBlob) {
const blob = new Blob([res.data]);
download.saveAs(blob, decodeURIComponent(res.headers["download-filename"]));
} else {
download.printErrMsg(res.data);
await download.printErrMsg(res.data);
}
});
} catch (error) {
console.error("[Download] 资源下载失败:", error);
ElMessage.error("资源下载失败,请稍后重试");
}
},
zip(url: string, name: string): void {
async zip(url: string, name: string) {
const fullUrl = baseURL + url;
downloadLoadingInstance = ElLoading.service({
text: "正在下载数据,请稍候",
background: "rgba(0, 0, 0, 0.7)",
});
axios({
method: "get",
url: fullUrl,
responseType: "blob",
headers: { Authorization: "Bearer " + Auth.getAccessToken() },
})
.then((res: AxiosResponse<Blob>) => {
const isBlob = blobValidate(res.data);
if (isBlob) {
const blob = new Blob([res.data], { type: "application/zip" });
download.saveAs(blob, name);
} else {
download.printErrMsg(res.data);
}
downloadLoadingInstance.close();
})
.catch((r: any) => {
console.error(r);
ElMessage.error("下载文件出现错误,请联系管理员!");
downloadLoadingInstance.close();
try {
const res = await axios.get<Blob>(fullUrl, {
responseType: "blob",
headers: { Authorization: `Bearer ${Auth.getAccessToken()}` },
});
const isBlob = blobValidate(res.data);
if (isBlob) {
const blob = new Blob([res.data], { type: "application/zip" });
download.saveAs(blob, name);
} else {
await download.printErrMsg(res.data);
}
} catch (r: any) {
console.error(r);
ElMessage.error("下载文件出现错误,请联系管理员!");
} finally {
downloadLoadingInstance.close();
}
},
saveAs(text: Blob | string, name: string, opts?: any): void {
+90 -9
View File
@@ -15,10 +15,11 @@ import { ResultEnum } from "@/enums/api/result.enum";
import { Auth } from "@/utils/auth";
import { redirectToLogin } from "@/utils/auth";
import { $t } from "@/locales";
import AuthAPI from "@/api/module_system/auth";
// --- 配置常量 -----------------------------------------------------------------
/** 跳过鉴权:与单接口 `headers.Authorization` 约定一致 */
/** 跳过鉴权标记:接口 headers.Authorization 设为该值时不带 token 请求 */
export const NO_AUTH_FLAG = "no-auth";
export interface ExtendedRequestConfig extends AxiosRequestConfig {
@@ -153,6 +154,33 @@ export const isHttpError = (error: unknown): error is HttpError => {
return error instanceof HttpError;
};
// --- Token 刷新去重 -----------------------------------------------------------
/**
* token 401 refresh
* pendingRequests
*/
let isRefreshing = false;
let pendingRequests: Array<{
config: InternalAxiosRequestConfig;
resolve: (value: any) => void;
reject: (reason?: any) => void;
}> = [];
function onRefreshed(newToken: string) {
const list = pendingRequests;
pendingRequests = [];
list.forEach(({ config, resolve }) => {
config.headers.Authorization = `Bearer ${newToken}`;
resolve(request(config));
});
}
function onRefreshFailed() {
pendingRequests.forEach(({ reject }) => reject(new Error("Token refresh failed")));
pendingRequests = [];
}
// --- Axios 实例 ---------------------------------------------------------------
const request: AxiosInstance = axios.create({
@@ -185,6 +213,22 @@ request.interceptors.request.use(
}
);
/**
*
*
* 1. response
* - blob JSON
* - code SUCCESS
* - GET login/logout
*
* 2. response
* - ECONNREFUSED / timeout / Network Error
*
* 3. / response
* - Blob JSON msg
* - 401 / TOKEN_EXPIRED token
* - code
*/
request.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
if (response.config.responseType === "blob") {
@@ -209,6 +253,7 @@ request.interceptors.response.use(
return response;
},
async (error: AxiosError<ApiResponse>) => {
// ── 网络错误(无响应体) ──
if (!error.response) {
let errorMessage = "网络连接异常";
@@ -227,6 +272,7 @@ request.interceptors.response.use(
const data = error.response?.data;
// ── Blob 响应错误(文件下载场景) ──
if (error.response?.config.responseType === "blob" && error.response.data instanceof Blob) {
try {
const text = await new Response(error.response.data).text();
@@ -246,6 +292,7 @@ request.interceptors.response.use(
}
}
// ── 鉴权错误(401 / TOKEN_EXPIRED):静默续期 ──
const status = error.response.status;
const hasApiCode =
@@ -255,16 +302,50 @@ request.interceptors.response.use(
"code" in data &&
typeof (data as ApiResponse).code === "number";
if (status === 401 && !hasApiCode) {
await redirectToLogin("登录已失效,请重新登录");
return Promise.reject(new HttpError("Unauthorized", ApiStatus.unauthorized));
if ((status === 401 && !hasApiCode) || data?.code === ResultEnum.TOKEN_EXPIRED) {
const config = error.config as InternalAxiosRequestConfig | undefined;
// 若 refresh 接口自身返回 401,不再递归续期,直接跳转登录
if (!config || config.url?.includes("auth/token/refresh")) {
await redirectToLogin("登录状态异常,请重新登录");
return Promise.reject(new HttpError("Unauthorized", ApiStatus.unauthorized));
}
// 首次 401:发起 refresh;后续并发 401 入队等待
if (!isRefreshing) {
isRefreshing = true;
try {
// 直接请求刷新令牌接口,避免动态导入 user.store 造成循环依赖
const refreshResp = await AuthAPI.refreshToken({
refresh_token: Auth.getRefreshToken(),
});
const tokenData = refreshResp.data.data;
Auth.setTokens(tokenData.access_token, tokenData.refresh_token, Auth.getRememberMe());
isRefreshing = false;
const newToken = Auth.getAccessToken();
// 重放等待队列中的所有请求
onRefreshed(newToken);
// 用新 token 重试当前请求
config.headers.Authorization = `Bearer ${newToken}`;
return request(config);
} catch {
isRefreshing = false;
// refresh 失败:拒绝队列中所有等待请求 + 跳转登录
onRefreshFailed();
const msg = data?.msg || "登录已失效,请重新登录";
await redirectToLogin(msg);
return Promise.reject(new HttpError(msg, ApiStatus.unauthorized));
}
} else {
// 已有 refresh 进行中,将当前请求加入等待队列,刷新完成后自动重放
return new Promise((resolve, reject) => {
pendingRequests.push({ config: config!, resolve, reject });
});
}
}
if (data?.code === ResultEnum.TOKEN_EXPIRED) {
await redirectToLogin("登录已过期,请重新登录");
const msg = data.msg || "登录已过期,请重新登录";
return Promise.reject(new HttpError(msg, ApiStatus.unauthorized));
} else if (data?.code === ResultEnum.ERROR) {
// ── 业务错误(按 code 分类) ──
if (data?.code === ResultEnum.ERROR) {
ElMessage.error(data.msg || "请求错误");
return Promise.reject(new Error(data.msg || "请求错误"));
} else if (data?.code === ResultEnum.UNAUTHORIZED) {
+9
View File
@@ -37,15 +37,24 @@ export function resolveElementPlusIconComponent(icon?: string | null): Component
const mod = ElementPlusIconsVue as Record<string, Component | undefined>;
// 1. 精确匹配(如 PieChart
let comp = mod[body];
if (comp) return comp;
// 2. kebab/snake → Pascal(如 pie-chart → PieChart
if (/[-_]/.test(body)) {
const pascal = kebabSnakeBodyToPascalKey(body);
comp = mod[pascal];
if (comp) return comp;
}
// 3. 首字母大写(旧版存值全小写如 delete → Delete
const capitalized = body.charAt(0).toUpperCase() + body.slice(1);
if (capitalized !== body) {
comp = mod[capitalized];
if (comp) return comp;
}
return null;
}
+7 -1
View File
@@ -9,7 +9,13 @@ import { IframeRouteManager } from "@/router";
import { useCommon } from "@/hooks/core/useCommon";
/**
* `setWorktab``setPageTitle`
* `setWorktab``setPageTitle`
*
*
* - `setPageTitle` `meta.title - 站点名`
* - `formatMenuTitle` i18n `menus.xxx`
* - `handleMenuJump` iframe
* - `setWorktab`
*/
export type AppRouteRecordRaw = RouteRecordRaw & { hidden?: boolean };
+13 -13
View File
@@ -62,7 +62,7 @@ export function setupWebSocket() {
try {
isInitialized = true;
console.log("[WebSocket] 初始化成功");
console.info("[WebSocket] 初始化成功");
} catch (error) {
console.error("[WebSocket] 初始化失败:", error);
}
@@ -72,7 +72,7 @@ export function setupWebSocket() {
* WebSocket
*/
export function cleanupWebSocket() {
console.log("[WebSocket] 开始清理连接...");
console.info("[WebSocket] 开始清理连接...");
websocketInstances.forEach((instance, key) => {
try {
@@ -83,7 +83,7 @@ export function cleanupWebSocket() {
} else if (instance.cleanup) {
instance.cleanup();
}
console.log(`[WebSocket] ${key} 已断开`);
console.info(`[WebSocket] ${key} 已断开`);
} catch (error) {
console.error(`[WebSocket] ${key} 清理失败:`, error);
}
@@ -91,7 +91,7 @@ export function cleanupWebSocket() {
websocketInstances.clear();
isInitialized = false;
console.log("[WebSocket] 清理完成");
console.info("[WebSocket] 清理完成");
}
/**
@@ -192,7 +192,7 @@ export default class WebSocketClient {
private connect(resetReconnectAttempts: boolean = false): void {
// 如果正在连接中,不重复连接
if (this.isConnecting) {
console.log("正在建立WebSocket连接中...");
console.info("正在建立WebSocket连接中...");
return;
}
@@ -267,7 +267,7 @@ export default class WebSocketClient {
// 如果未连接且不要求立即发送,则加入消息队列
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
console.log("WebSocket未连接,消息已加入队列等待发送");
console.info("WebSocket未连接,消息已加入队列等待发送");
this.messageQueue.push(data);
// 如果未在重连中,则尝试重连
if (!this.isConnecting && !this.stopReconnect) {
@@ -289,7 +289,7 @@ export default class WebSocketClient {
// 发送队列中的消息
private flushMessageQueue(): void {
if (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) {
console.log(`发送队列中的${this.messageQueue.length}条消息`);
console.info(`发送队列中的${this.messageQueue.length}条消息`);
while (this.messageQueue.length > 0) {
const data = this.messageQueue.shift();
if (data) {
@@ -308,7 +308,7 @@ export default class WebSocketClient {
// 处理连接打开
private handleOpen(event: Event): void {
console.log("WebSocket连接成功", event);
console.info("WebSocket连接成功", event);
this.clearTimer("connectionTimer"); // 清除连接超时定时器
this.isConnected = true;
this.isConnecting = false;
@@ -322,14 +322,14 @@ export default class WebSocketClient {
// 处理收到的消息
private handleMessage(event: MessageEvent): void {
console.log("收到WebSocket消息:", event);
console.debug("收到WebSocket消息:", event);
this.resetHeartbeat();
this.messageHandler(event);
}
// 处理连接关闭
private handleClose(event: CloseEvent): void {
console.log(
console.info(
`WebSocket断开: 代码=${event.code}, 原因=${event.reason}, 干净关闭=${event.wasClean}`
);
@@ -440,7 +440,7 @@ export default class WebSocketClient {
try {
this.ws.send("ping");
console.log("发送ping消息");
console.debug("发送ping消息");
} catch (error) {
console.error("发送ping消息失败:", error);
this.clearTimer("pingTimer");
@@ -467,13 +467,13 @@ export default class WebSocketClient {
this.closeCurrentSocketForReconnect();
const delay = this.calculateReconnectDelay();
console.log(
console.info(
`将在${delay / 1000}秒后尝试重新连接(第${this.reconnectAttempts}/${this.maxReconnectAttempts}次)`
);
this.clearTimer("reconnectTimer");
this.reconnectTimer = setTimeout(() => {
console.log(`尝试重新连接WebSocket(第${this.reconnectAttempts}次)`);
console.info(`尝试重新连接WebSocket(第${this.reconnectAttempts}次)`);
this.connect(false);
}, delay);
}
+87 -247
View File
@@ -1,22 +1,35 @@
/** LocalStorage compatibility checks + recovery helpers.
/**
* LocalStorage +
*
*
*
*
* - `markStorageInvalidated` / `checkStorageInvalidated` / `resetStorageInvalidated`
* Pinia/router
* - `checkStorageCompatibility()` / `validateStorageData()`
* - `StorageKeyManager` / `Storage`
*/
/** 存储是否已失效(由路由守卫检查并处理登出) */
// ---------------------------------------------------------------------------
// 存储失效标志位(路由守卫消费)
// ---------------------------------------------------------------------------
/** 标志:存储数据已因异常而被清除,等待路由守卫处理登出 */
let invalidated = false;
/** 标记存储已失效(供路由守卫检查) */
/** 标记存储已失效路由守卫将在下次导航时执行登出 */
export function markStorageInvalidated(): void {
invalidated = true;
}
/** 查存储是否已失效 */
/** 查存储是否已失效 */
export function checkStorageInvalidated(): boolean {
return invalidated;
}
/** 守卫处理完毕后重置标志位,避免同一会话内重复拦截 */
export function resetStorageInvalidated(): void {
invalidated = false;
}
/** Storage config + versioned key helpers. */
export class StorageConfig {
/** 当前应用版本 */
@@ -135,27 +148,27 @@ class StorageCompatibilityManager {
}
/**
*
* localStorage key
* hasCurrentVersionStorage / hasAnyVersionStorage
*/
private hasCurrentVersionStorage(): boolean {
const storageKeys = Object.keys(localStorage);
const currentVersionPattern = StorageConfig.createCurrentVersionPattern();
private analyzeStorageKeys(): { hasCurrent: boolean; hasAny: boolean } {
const keys = Object.keys(localStorage);
const currentPtrn = StorageConfig.createCurrentVersionPattern();
const anyPtrn = StorageConfig.createVersionPattern();
let hasCurrent = false;
let hasAny = false;
return storageKeys.some(
(key) => currentVersionPattern.test(key) && localStorage.getItem(key) !== null
);
}
/**
*
*/
private hasAnyVersionStorage(): boolean {
const storageKeys = Object.keys(localStorage);
const versionPattern = StorageConfig.createVersionPattern();
return storageKeys.some(
(key) => versionPattern.test(key) && localStorage.getItem(key) !== null
);
for (const key of keys) {
if (!hasCurrent && currentPtrn.test(key) && localStorage.getItem(key) !== null) {
hasCurrent = true;
if (hasAny) break;
}
if (!hasAny && anyPtrn.test(key) && localStorage.getItem(key) !== null) {
hasAny = true;
if (hasCurrent) break;
}
}
return { hasCurrent, hasAny };
}
/**
@@ -184,16 +197,24 @@ class StorageCompatibilityManager {
}
/**
*
*
*
*
* 1. localStoragePinia
* 2.
* 3. App.vue router.push()
* 4. userStore.logout()
*
* 使 CustomEvent window.location.href
* Pinia Vue
*/
private performSystemLogout(): void {
setTimeout(() => {
try {
localStorage.clear();
markStorageInvalidated();
console.info("[Storage] 已标记存储失效,跳转到登录页");
// 强制刷新触发路由守卫中的存储检查
window.location.href = window.location.origin + "/login";
console.info("[Storage] 已标记存储失效,触发路由守卫登出流程");
window.dispatchEvent(new CustomEvent("app:storage-invalidated"));
} catch (error) {
console.error("[Storage] 标记存储失效失败:", error);
}
@@ -209,42 +230,45 @@ class StorageCompatibilityManager {
}
/**
*
* @param requireAuth false
*
*
*
* 1.
* 2.
* 3. storeId
* 4.
* - requireAuth=false访 /
* - requireAuth=true
*
* @param requireAuth true performSystemLogout
*/
validateStorageData(requireAuth: boolean = false): boolean {
try {
// 优先检查新版本存储结构
if (this.hasCurrentVersionStorage()) {
// console.debug('[Storage] 发现当前版本存储数据')
return true;
}
const { hasCurrent, hasAny } = this.analyzeStorageKeys();
// 检查是否有任何版本的存储数据
if (this.hasAnyVersionStorage()) {
// console.debug('[Storage] 发现其他版本存储数据,可能需要迁移')
return true;
}
// 1. 当前版本 → 一切正常
if (hasCurrent) return true;
// 检查旧版本存储结构
// 2. 其他版本 → 待迁移,暂时也能用
if (hasAny) return true;
// 3. 旧格式(无 storeId 的老系统)
const legacyData = this.getLegacyStorageData();
if (Object.keys(legacyData).length === 0) {
// 只有在需要验证登录状态时才执行登出操作
if (requireAuth) {
console.warn("[Storage] 未发现任何存储数据,需要重新登录");
this.performSystemLogout();
return false;
}
// 首次访问或访问静态路由,不需要登出
// console.debug('[Storage] 未发现存储数据,首次访问或访问静态路由')
if (Object.keys(legacyData).length > 0) {
console.debug("[Storage] 发现旧版本存储数据");
return true;
}
console.debug("[Storage] 发现旧版本存储数据");
// 4. 完全空存储
if (requireAuth) {
console.warn("[Storage] 未发现任何存储数据,需要重新登录");
this.performSystemLogout();
return false;
}
// 首次访问或静态路由无需登出
return true;
} catch (error) {
console.error("[Storage] 存储数据验证失败:", error);
// 只有在需要验证登录状态时才处理错误
if (requireAuth) {
this.handleStorageError();
return false;
@@ -252,47 +276,6 @@ class StorageCompatibilityManager {
return true;
}
}
/**
*
*/
isStorageEmpty(): boolean {
// 检查新版本存储结构
if (this.hasCurrentVersionStorage()) {
return false;
}
// 检查是否有任何版本的存储数据
if (this.hasAnyVersionStorage()) {
return false;
}
// 检查旧版本存储结构
const legacyData = this.getLegacyStorageData();
return Object.keys(legacyData).length === 0;
}
/**
*
* @param requireAuth false
*/
checkCompatibility(requireAuth: boolean = false): boolean {
try {
const isValid = this.validateStorageData(requireAuth);
const isEmpty = this.isStorageEmpty();
if (isValid || isEmpty) {
// console.debug('[Storage] 存储兼容性检查通过')
return true;
}
console.warn("[Storage] 存储兼容性检查失败");
return false;
} catch (error) {
console.error("[Storage] 兼容性检查异常:", error);
return false;
}
}
}
// 创建存储兼容性管理器实例
@@ -321,11 +304,18 @@ export function validateStorageData(requireAuth: boolean = false): boolean {
}
/**
*
* @param requireAuth false
* try-catch validateStorageData
*
* @param requireAuth false
* true
*/
export function checkStorageCompatibility(requireAuth: boolean = false): boolean {
return storageManager.checkCompatibility(requireAuth);
try {
return storageManager.validateStorageData(requireAuth);
} catch (error) {
console.error("[Storage] 兼容性检查异常:", error);
return false;
}
}
export class StorageKeyManager {
@@ -364,153 +354,3 @@ export class StorageKeyManager {
return currentKey;
}
}
export class Storage {
/**
* localStorage
*
* JSON localStorage
*
* @param {string} key -
* @param {any} value -
*
* @example
* ```typescript
* Storage.set('user', { name: 'John', age: 25 });
* ```
*/
static set(key: string, value: any): void {
localStorage.setItem(key, JSON.stringify(value));
}
/**
* localStorage
*
* localStorage
*
*
* @param {string} key -
* @param {T} [defaultValue] -
* @returns {T}
*
* @example
* ```typescript
* const user = Storage.get<User>('user', { name: '', age: 0 });
* ```
*/
static get<T>(key: string, defaultValue?: T): T {
const value = localStorage.getItem(key);
if (!value) return defaultValue as T;
try {
return JSON.parse(value);
} catch {
// 如果解析失败,返回原始字符串
return value as unknown as T;
}
}
/**
* localStorage
*
* localStorage
*
* @param {string} key -
*
* @example
* ```typescript
* Storage.remove('user');
* ```
*/
static remove(key: string): void {
localStorage.removeItem(key);
}
/**
* localStorage
*
* localStorage
*
* @example
* ```typescript
* Storage.clear();
* ```
*/
static clear(): void {
localStorage.clear();
}
/**
* sessionStorage
*
* JSON sessionStorage
*
* @param {string} key -
* @param {any} value -
*
* @example
* ```typescript
* Storage.sessionSet('temp', { token: 'abc123' });
* ```
*/
static sessionSet(key: string, value: any): void {
sessionStorage.setItem(key, JSON.stringify(value));
}
/**
* sessionStorage
*
* sessionStorage
*
*
* @param {string} key -
* @param {T} [defaultValue] -
* @returns {T}
*
* @example
* ```typescript
* const temp = Storage.sessionGet<string>('temp', '');
* ```
*/
static sessionGet<T>(key: string, defaultValue?: T): T {
const value = sessionStorage.getItem(key);
if (!value) return defaultValue as T;
try {
return JSON.parse(value);
} catch {
// 如果解析失败,返回原始字符串
return value as unknown as T;
}
}
/**
* sessionStorage
*
* sessionStorage
*
* @param {string} key -
*
* @example
* ```typescript
* Storage.sessionRemove('temp');
* ```
*/
static sessionRemove(key: string): void {
sessionStorage.removeItem(key);
}
/**
* sessionStorage
*
* sessionStorage
*
* @example
* ```typescript
* Storage.sessionClear();
* ```
*/
static sessionClear(): void {
sessionStorage.clear();
}
}
+10
View File
@@ -125,6 +125,16 @@ export function initErrorHandle(app: App) {
// Upgrade
// -----------------------------
/**
*
*
*
* 1. 1.0.0
* 2. 访
* 3.
* 4. + key
* 5. +
*/
class VersionManager {
private normalizeVersion(version: string): string {
return version.replace(/^v/, "");
+14 -4
View File
@@ -1,8 +1,18 @@
/**
* `useTable`
* `useTable` `TableCache`
*
* - `PageResult` `data` `defaultResponseAdapter`
* - `TableCache` hash + `useTable.clearCache` 使
*
* API `PageResult` total + items
* `data` `defaultResponseAdapter`
*
* TableCache
* hash key CacheInvalidationStrategy
* Timestamp
* Tag
* None clearCache
*
* @see useTable (src/hooks/core/useTable.ts)
* @see TableCache
*/
import { h } from "vue";
@@ -10,7 +20,7 @@ import type { VNode } from "vue";
import { ElTooltip } from "element-plus";
import { hash } from "ohash";
import ArtButtonMore from "@/components/forms/fa-button-more/index.vue";
import type { ButtonMoreItem } from "@/components/forms/fa-button-more/index.vue";
import type { ButtonMoreItem } from "@/components/forms/fa-button-more/types";
import ArtButtonTable from "@/components/forms/fa-button-table/index.vue";
// --- 全局分页字段名(与 PageQuery 对齐) ---
+1 -1
View File
@@ -609,7 +609,7 @@ onMounted(async () => {
bottom: 0;
z-index: 2;
::v-deep(.el-upload) {
:deep(.el-upload) {
display: inline-flex;
}
@@ -73,8 +73,6 @@
<span class="text-sm text-gray-700">{{ clickItem.userName }}</span>
</div>
</div>
<CommentWidget />
</div>
</template>
</ElDrawer>
@@ -127,12 +127,12 @@ watch(
.article-detail-markdown {
margin-top: 8px;
::v-deep(img) {
:deep(img) {
width: 100%;
border: 1px solid var(--fa-gray-200);
}
::v-deep(pre) {
:deep(pre) {
position: relative;
&:hover {
@@ -152,11 +152,11 @@ watch(
}
}
::v-deep(.code-wrapper) {
:deep(.code-wrapper) {
overflow-x: auto;
}
::v-deep(.line-number) {
:deep(.line-number) {
position: sticky;
left: 0;
z-index: 2;
@@ -169,7 +169,7 @@ watch(
text-align: center;
}
::v-deep(.copy-button) {
:deep(.copy-button) {
position: absolute;
top: 6px;
right: 6px;
@@ -164,7 +164,7 @@ onMounted(() => {
<style lang="scss" scoped>
.fa-card {
::v-deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
color: var(--el-color-primary) !important;
background: transparent !important;
}
+38 -37
View File
@@ -1,3 +1,4 @@
<!-- 仪表盘首页快捷入口 + 收藏夹 + 数据概览卡片 -->
<template>
<div class="workplace-page">
<!-- 顶栏放入 gutter 与下方栅格列边缘对齐 -->
@@ -627,39 +628,39 @@ function getQuickLinkStableIndex(item: QuickLink): number {
}
//
const handleDeleteLink = (item: QuickLink) => {
ElMessageBox.confirm(`确定要取消收藏"${item.title}"吗?`, "取消收藏确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
if (item.id) {
quickStartManager.removeQuickLink(item.id);
} else if (item.href) {
quickStartManager.removeQuickLinkByHref(item.href);
} else {
ElMessage.warning("无法移除:缺少标识");
return;
}
ElMessage.success(`已取消收藏:${item.title}`);
})
.catch(() => {
//
const handleDeleteLink = async (item: QuickLink) => {
try {
await ElMessageBox.confirm(`确定要取消收藏"${item.title}"吗?`, "取消收藏确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
if (item.id) {
quickStartManager.removeQuickLink(item.id);
} else if (item.href) {
quickStartManager.removeQuickLinkByHref(item.href);
} else {
ElMessage.warning("无法移除:缺少标识");
return;
}
ElMessage.success(`已取消收藏:${item.title}`);
} catch {
//
}
};
const clearBookmarks = () => {
ElMessageBox.confirm("确定要清空收藏吗?", "清空收藏确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
quickStartManager.clearQuickLinks();
ElMessage.success("已清空收藏");
})
.catch(() => {});
const clearBookmarks = async () => {
try {
await ElMessageBox.confirm("确定要清空收藏吗?", "清空收藏确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
quickStartManager.clearQuickLinks();
ElMessage.success("已清空收藏");
} catch {
//
}
};
//
@@ -727,13 +728,13 @@ const currentUser = {
border: 1px solid var(--el-border-color-lighter);
border-radius: var(--workplace-radius);
::v-deep(.el-card__header) {
:deep(.el-card__header) {
border-bottom-color: var(--el-border-color-extra-light);
}
}
.workplace-calendar-card {
::v-deep(.el-card__body) {
:deep(.el-card__body) {
padding: 8px 10px 10px;
}
}
@@ -756,7 +757,7 @@ const currentUser = {
/* —— 顶栏 —— */
.workplace-hero-card {
::v-deep(.el-card__body) {
:deep(.el-card__body) {
padding: 20px 22px;
}
}
@@ -841,7 +842,7 @@ const currentUser = {
}
.workplace-modules-card {
::v-deep(.el-card__body) {
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
@@ -857,7 +858,7 @@ const currentUser = {
}
.workplace-bookmarks-card {
::v-deep(.el-card__body) {
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
@@ -937,7 +938,7 @@ const currentUser = {
}
.workplace-section-card {
::v-deep(.el-card__body) {
:deep(.el-card__body) {
padding: 12px 18px 18px;
}
}
@@ -976,7 +977,7 @@ const currentUser = {
width: 100%;
min-width: 0;
::v-deep(.el-card__body) {
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
@@ -1012,7 +1012,7 @@ function handleAnchorClick(ev: MouseEvent) {
&__tabs {
width: 100%;
::v-deep(.el-tab-pane) {
:deep(.el-tab-pane) {
box-sizing: border-box;
}
}
@@ -1,7 +1,7 @@
<template>
<div class="dashboard-container">
<!-- github 角标 -->
<GithubCorner class="github-corner" />
<FaGithubCorner class="github-corner" />
<ElCard shadow="hover">
<div class="flex flex-wrap items-center gap-y-3">
@@ -300,7 +300,7 @@
</template>
<ElScrollbar height="calc(100vh - 550px)">
<ElTimeline class="p-3">
<ElTimeline class="p-2">
<ElTimelineItem
v-for="(item, index) in vesionList"
:key="index"
@@ -308,7 +308,6 @@
placement="top"
:color="index === 0 ? '#67C23A' : '#909399'"
:hollow="index !== 0"
size="large"
>
<div class="version-item" :class="{ 'latest-item': index === 0 }">
<div class="flex items-center justify-between">
@@ -592,7 +591,7 @@ onMounted(() => {
}
.version-item {
padding: 16px;
padding: 10px 12px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
transition: all 0.2s;
@@ -607,9 +606,11 @@ onMounted(() => {
}
.version-content {
margin-bottom: 12px;
display: block;
margin-top: 6px;
margin-bottom: 8px;
font-size: 13px;
line-height: 1.5;
line-height: 1.4;
color: var(--el-text-color-secondary);
}
}
@@ -225,7 +225,7 @@ defineExpose({
flex: 1;
min-width: 0;
::v-deep(.el-textarea__inner) {
:deep(.el-textarea__inner) {
padding: 0;
line-height: 1.6;
color: var(--el-text-color-primary);
@@ -235,7 +235,7 @@ defineExpose({
box-shadow: none;
}
::v-deep(.el-textarea) {
:deep(.el-textarea) {
padding: 0;
}
}
@@ -145,7 +145,7 @@ const toggleSidebar = () => {
align-items: center;
/* EP 相邻按钮自带 margin-left,叠在 flex gap 上会导致间距忽大忽小 */
::v-deep(.el-button) {
:deep(.el-button) {
margin: 0;
}
@@ -159,7 +159,7 @@ const toggleSidebar = () => {
font-size: 14px;
line-height: 1;
::v-deep(.el-tag__content) {
:deep(.el-tag__content) {
display: inline-flex;
gap: 6px;
align-items: center;
@@ -371,7 +371,7 @@ const formatFileSize = (bytes: number): string => {
}
}
::v-deep(pre) {
:deep(pre) {
padding: 12px;
margin: 12px 0;
overflow-x: auto;
@@ -384,7 +384,7 @@ const formatFileSize = (bytes: number): string => {
}
}
::v-deep(code) {
:deep(code) {
padding: 2px 6px;
font-family: "Courier New", Courier, monospace;
font-size: 13px;
@@ -392,21 +392,21 @@ const formatFileSize = (bytes: number): string => {
border-radius: 3px;
}
::v-deep(p) {
:deep(p) {
margin: 8px 0;
}
::v-deep(ul),
::v-deep(ol) {
:deep(ul),
:deep(ol) {
padding-left: 24px;
margin: 8px 0;
}
::v-deep(li) {
:deep(li) {
margin: 4px 0;
}
::v-deep(a) {
:deep(a) {
color: var(--el-color-primary);
text-decoration: none;
@@ -415,14 +415,14 @@ const formatFileSize = (bytes: number): string => {
}
}
::v-deep(blockquote) {
:deep(blockquote) {
padding: 8px 16px;
margin: 12px 0;
background: var(--el-fill-color-light);
border-left: 4px solid var(--el-color-primary);
}
::v-deep(table) {
:deep(table) {
width: 100%;
margin: 12px 0;
border-collapse: collapse;
@@ -501,11 +501,11 @@ const formatFileSize = (bytes: number): string => {
background: linear-gradient(transparent, var(--el-color-primary-light-9));
}
::v-deep(pre) {
:deep(pre) {
border: 1px solid var(--el-border-color-lighter);
}
::v-deep(code:not(pre code)) {
:deep(code:not(pre code)) {
border: 1px solid var(--el-border-color-lighter);
}
}
@@ -400,7 +400,7 @@ defineExpose({
.search-section {
margin-bottom: 16px;
::v-deep(.el-input__wrapper) {
:deep(.el-input__wrapper) {
border-radius: 20px;
box-shadow: 0 0 0 1px var(--el-border-color) inset;
transition: all 0.2s ease;
@@ -414,7 +414,7 @@ defineExpose({
}
}
::v-deep(.el-input__inner) {
:deep(.el-input__inner) {
font-size: 13px;
}
}
@@ -450,4 +450,8 @@ onMounted(() => {
mittBus.on("openChat", openChat);
selectedPerson.value = personList.value[0];
});
onUnmounted(() => {
mittBus.off("openChat", openChat);
});
</script>
@@ -286,41 +286,41 @@ function onTableSelectionChange(rows: ChatSession[]) {
selectedRows.value = rows;
}
function deleteSessionRow(id: string) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await AiChatAPI.deleteSession([id]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {});
async function deleteSessionRow(id: string) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await AiChatAPI.deleteSession([id]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
}
}
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await AiChatAPI.deleteSession(ids);
ElMessage.success("删除成功");
selectedRows.value = [];
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
batchDeleting.value = true;
await AiChatAPI.deleteSession(ids);
ElMessage.success("删除成功");
selectedRows.value = [];
await refreshRemove();
} catch {
//
} finally {
batchDeleting.value = false;
}
}
const {
@@ -625,17 +625,17 @@ pre {
border-radius: 4px;
}
::v-deep(.session-detail-dialog .el-dialog__body) {
:deep(.session-detail-dialog .el-dialog__body) {
max-height: 60vh;
padding: 20px;
overflow-y: auto;
}
.crud-dialog-art-form ::v-deep(.el-row > .el-col:last-child) {
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form ::v-deep(.el-form-item__content) {
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
</style>
@@ -532,12 +532,12 @@ async function handleSubmit() {
opacity: 0.55;
}
::v-deep(.el-card__header) {
:deep(.el-card__header) {
padding: 14px 14px 12px;
border-bottom: none;
}
::v-deep(.el-card__body) {
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
@@ -546,7 +546,7 @@ async function handleSubmit() {
padding: 12px 14px;
}
::v-deep(.el-card__footer) {
:deep(.el-card__footer) {
padding: 10px 14px 14px;
margin-top: auto;
}
@@ -634,11 +634,11 @@ async function handleSubmit() {
}
}
.crud-dialog-art-form ::v-deep(.el-row > .el-col:last-child) {
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form ::v-deep(.el-form-item__content) {
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
</style>
@@ -472,7 +472,7 @@ const exportQueryParams = computed(() => {
const demoImportContentConfig = computed<IContentConfig>(() => ({
permPrefix: "module_example:demo",
cols: demoCrudCols.value,
indexAction: async () => ({}) as any,
indexAction: async () => ({}),
importTemplate: () => DemoAPI.downloadTemplateDemo(),
}));
@@ -685,84 +685,91 @@ async function handleSubmit() {
});
}
const deleteDemoRow = (row: DemoTable) => {
const deleteDemoRow = async (row: DemoTable) => {
if (!row.id) return;
ElMessageBox.confirm(`确定删除「${row.name ?? row.id}」吗?此操作不可恢复!`, "删除确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await DemoAPI.deleteDemo([row.id!]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {
ElMessage.info("已取消删除");
});
try {
await ElMessageBox.confirm(
`确定删除「${row.name ?? row.id}」吗?此操作不可恢复!`,
"删除确认",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
);
await DemoAPI.deleteDemo([row.id!]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
ElMessage.info("已取消删除");
}
};
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?此操作不可恢复!`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await DemoAPI.deleteDemo(ids);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} finally {
batchDeleting.value = false;
try {
await ElMessageBox.confirm(
`确定删除选中的 ${ids.length} 条数据吗?此操作不可恢复!`,
"批量删除",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
})
.catch(() => {
ElMessage.info("已取消删除");
});
);
batchDeleting.value = true;
await DemoAPI.deleteDemo(ids);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
ElMessage.info("已取消删除");
} finally {
batchDeleting.value = false;
}
}
function runBatchStatus(status: string) {
async function runBatchStatus(status: string) {
const ids = selectedIds.value;
if (ids.length === 0) {
ElMessage.warning("请先在列表中勾选数据");
return;
}
ElMessageBox.confirm(
`确认对选中的 ${ids.length} 条数据${status === "0" ? "启用" : "停用"}`,
"批量设置",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
)
.then(async () => {
await DemoAPI.batchDemo({ ids, status });
ElMessage.success("操作成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshData();
})
.catch(() => {});
try {
await ElMessageBox.confirm(
`确认对选中的 ${ids.length} 条数据${status === "0" ? "启用" : "停用"}`,
"批量设置",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
);
await DemoAPI.batchDemo({ ids, status });
ElMessage.success("操作成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshData();
} catch {
//
}
}
function openImportModal() {
importModalVisible.value = true;
}
function handleCrudImportUpload(formData: FormData) {
DemoAPI.importDemo(formData)
.then((res) => {
if (res.data.code !== ResultEnum.SUCCESS) {
ElMessage.error(res.data.msg || "导入失败");
return;
}
ElMessage.success(res.data.msg || "导入成功");
importModalVisible.value = false;
return refreshData();
})
.catch(console.error);
async function handleCrudImportUpload(formData: FormData) {
try {
const res = await DemoAPI.importDemo(formData);
if (res.data.code !== ResultEnum.SUCCESS) {
ElMessage.error(res.data.msg || "导入失败");
return;
}
ElMessage.success(res.data.msg || "导入成功");
importModalVisible.value = false;
await refreshData();
} catch (error) {
console.error("[Import]", error);
ElMessage.error("导入失败");
}
}
function openExportModal() {
@@ -541,84 +541,83 @@ async function handleSubmit() {
});
}
const deleteDemo01Row = (row: Demo01Table) => {
const deleteDemo01Row = async (row: Demo01Table) => {
if (!row.id) return;
ElMessageBox.confirm(`确定删除「${row.name ?? row.id}」吗?此操作不可恢复!`, "删除确认", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await Demo01API.deleteDemo01([row.id!]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {
ElMessage.info("已取消删除");
});
try {
await ElMessageBox.confirm(
`确定删除「${row.name ?? row.id}」吗?此操作不可恢复!`,
"删除确认",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
);
await Demo01API.deleteDemo01([row.id!]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
ElMessage.info("已取消删除");
}
};
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?此操作不可恢复!`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await Demo01API.deleteDemo01(ids);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {
ElMessage.info("已取消删除");
});
try {
await ElMessageBox.confirm(
`确定删除选中的 ${ids.length} 条数据吗?此操作不可恢复!`,
"批量删除",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
);
batchDeleting.value = true;
await Demo01API.deleteDemo01(ids);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
ElMessage.info("已取消删除");
} finally {
batchDeleting.value = false;
}
}
function runBatchStatus(status: string) {
async function runBatchStatus(status: string) {
const ids = selectedIds.value;
if (ids.length === 0) {
ElMessage.warning("请先在列表中勾选数据");
return;
}
ElMessageBox.confirm(
`确认对选中的 ${ids.length} 条数据${status === "0" ? "启用" : "停用"}`,
"批量设置",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
)
.then(async () => {
await Demo01API.batchDemo01({ ids, status });
ElMessage.success("操作成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshData();
})
.catch(() => {});
try {
await ElMessageBox.confirm(
`确认对选中的 ${ids.length} 条数据${status === "0" ? "启用" : "停用"}`,
"批量设置",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
);
await Demo01API.batchDemo01({ ids, status });
ElMessage.success("操作成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshData();
} catch {
//
}
}
function openImportModal() {
importModalVisible.value = true;
}
function handleCrudImportUpload(formDataUpload: FormData) {
Demo01API.importDemo01(formDataUpload)
.then((res) => {
if (res.data.code !== ResultEnum.SUCCESS) {
ElMessage.error(res.data.msg || "导入失败");
return;
}
ElMessage.success(res.data.msg || "导入成功");
importModalVisible.value = false;
return refreshData();
})
.catch(console.error);
async function handleCrudImportUpload(formDataUpload: FormData) {
try {
const res = await Demo01API.importDemo01(formDataUpload);
if (res.data.code !== ResultEnum.SUCCESS) {
ElMessage.error(res.data.msg || "导入失败");
return;
}
ElMessage.success(res.data.msg || "导入成功");
importModalVisible.value = false;
await refreshData();
} catch (error) {
console.error("[Import]", error);
ElMessage.error("导入失败");
}
}
function openExportModal() {
@@ -627,11 +626,11 @@ function openExportModal() {
</script>
<style scoped lang="scss">
.crud-dialog-art-form ::v-deep(.el-row > .el-col:last-child) {
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form ::v-deep(.el-form-item__content) {
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
</style>
@@ -426,7 +426,7 @@ function handleCancel() {
position: relative;
}
.visual-pane ::v-deep(.el-textarea__inner) {
.visual-pane :deep(.el-textarea__inner) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
@@ -437,20 +437,20 @@ function handleCancel() {
/* 表结构:描述列表表格化,标签列对齐、内容区可伸缩 */
.visual-structure {
.visual-desc {
::v-deep(.el-descriptions__label) {
:deep(.el-descriptions__label) {
width: 108px;
vertical-align: middle;
}
::v-deep(.el-descriptions__cell) {
:deep(.el-descriptions__cell) {
vertical-align: middle;
}
::v-deep(.el-descriptions__content) {
:deep(.el-descriptions__content) {
min-width: 0;
}
::v-deep(.el-descriptions__title) {
:deep(.el-descriptions__title) {
margin-bottom: 8px;
}
}
@@ -237,7 +237,7 @@ const props = defineProps<{
function findOptionByValue(options: OptionType[], value: number | string): any | null {
for (const opt of options) {
if (String(opt.value) === String(value)) return opt as any;
if (String(opt.value) === String(value)) return opt;
if (opt.children?.length) {
const hit = findOptionByValue(opt.children, value);
if (hit) return hit;
@@ -352,15 +352,15 @@ onUnmounted(() => {
min-width: 0;
}
.gen-basic-step ::v-deep(.el-col) {
.gen-basic-step :deep(.el-col) {
min-width: 0;
}
.gen-basic-step ::v-deep(.el-form-item__content) {
.gen-basic-step :deep(.el-form-item__content) {
min-width: 0;
}
.gen-basic-step ::v-deep(.el-input-group) {
.gen-basic-step :deep(.el-input-group) {
width: 100%;
min-width: 0;
max-width: 100%;
@@ -383,11 +383,11 @@ onUnmounted(() => {
border: 1px solid var(--el-border-color-lighter);
}
.master-sub-card ::v-deep(.el-card__header) {
.master-sub-card :deep(.el-card__header) {
padding: 8px 10px;
}
.master-sub-card ::v-deep(.el-card__body) {
.master-sub-card :deep(.el-card__body) {
padding: 10px;
}
@@ -396,7 +396,7 @@ onUnmounted(() => {
border: 1px solid var(--el-border-color-lighter);
}
.gen-form-card ::v-deep(.el-card__body) {
.gen-form-card :deep(.el-card__body) {
overflow-x: hidden;
}
@@ -425,7 +425,7 @@ onUnmounted(() => {
border-bottom: 1px solid var(--el-border-color-lighter);
}
.gen-echo-card ::v-deep(.el-card__body) {
.gen-echo-card :deep(.el-card__body) {
padding: 6px 8px;
}
@@ -249,7 +249,7 @@
import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from "vue";
import { Search } from "@element-plus/icons-vue";
import { useDraggable } from "vue-draggable-plus";
import type { GenTableSchema } from "@/api/module_generator/gencode";
import type { GenTableSchema, GenTableColumnSchema } from "@/api/module_generator/gencode";
import type { DictTable } from "@/api/module_system/dict";
defineOptions({ name: "GenColumnsStep" });
@@ -276,7 +276,7 @@ const columnsModel = computed({
get: () => props.info.columns || [],
set: (v) => {
// props.info reactive
props.info.columns = v as any;
props.info.columns = v as GenTableColumnSchema[];
},
});
@@ -66,18 +66,18 @@ const rows = [
border-radius: var(--el-border-radius-base);
}
.gencode-help-collapse ::v-deep(.el-collapse-item__header) {
.gencode-help-collapse :deep(.el-collapse-item__header) {
padding: 8px 12px;
font-size: 13px;
font-weight: 500;
background: var(--el-fill-color-light);
}
.gencode-help-collapse ::v-deep(.el-collapse-item__wrap) {
.gencode-help-collapse :deep(.el-collapse-item__wrap) {
border-top: 1px solid var(--el-border-color-lighter);
}
.gencode-help-collapse ::v-deep(.el-collapse-item__content) {
.gencode-help-collapse :deep(.el-collapse-item__content) {
padding: 10px 12px 12px;
}
@@ -91,7 +91,7 @@ const rows = [
flex-shrink: 0;
}
.gencode-rules-table ::v-deep(.el-table__cell) {
.gencode-rules-table :deep(.el-table__cell) {
font-size: 12px;
}
@@ -73,7 +73,7 @@
></ElTableColumn>
<ElTableColumn prop="table_type" label="表类型"></ElTableColumn>
</ElTable>
<pagination
<FaPagination
v-model:page="query.page_no"
v-model:limit="query.page_size"
:total="total"
@@ -94,6 +94,7 @@ import { ref, computed } from "vue";
import type { FormInstance, TableInstance } from "element-plus";
import type { DBTableSchema, GenTablePageQuery } from "@/api/module_generator/gencode";
import FaDialog from "@/components/modal/fa-dialog/index.vue";
import FaPagination from "@/components/others/fa-pagination/index.vue";
defineOptions({ name: "ImportDbTableDialog" });
@@ -619,7 +619,7 @@ async function handleSynchDb(row: GenTableSchema): Promise<void> {
try {
loading.value = true;
const previewRes = await GencodeAPI.syncDbPreview(tableName);
const preview = previewRes.data?.data as any;
const preview = previewRes.data?.data as Record<string, any>;
const mainHtml = renderHtml(`主表:${tableName}`, preview);
const subHtml =
preview?.sub_table_name && preview?.sub
+145 -113
View File
@@ -1,3 +1,4 @@
<!-- 缓存监控 -->
<template>
<div class="fa-full-height">
<el-tabs>
@@ -143,44 +144,46 @@
</div>
</div>
</template>
<ElTable :loading="loading" :data="cacheNames" row-key="cache_name">
<template #empty>
<ElEmpty :image-size="80" description="暂无数据" />
</template>
<ElTableColumn prop="cache_name" label="缓存名称" show-overflow-tooltip>
<template #default="{ row }">
<ElButton
v-hasPerm="['module_monitor:cache:query']"
type="primary"
link
@click="getCacheKeyList(row)"
>
{{ row.cache_name }}
</ElButton>
<div class="cache-table-wrap">
<ElTable :loading="loading" :data="cacheNames" row-key="cache_name" height="100%">
<template #empty>
<ElEmpty :image-size="80" description="暂无数据" />
</template>
</ElTableColumn>
<ElTableColumn prop="remark" label="备注" show-overflow-tooltip />
<ElTableColumn label="操作" width="60" align="center">
<template #default="{ row }">
<ElPopconfirm
class="box-item"
:title="`确认删除缓存 ${row.cache_name} 吗?`"
placement="top"
@confirm="handleClearCacheName(row)"
>
<template #reference>
<ElButton
v-hasPerm="['module_monitor:cache:delete']"
type="danger"
size="small"
link
icon="delete"
/>
</template>
</ElPopconfirm>
</template>
</ElTableColumn>
</ElTable>
<ElTableColumn prop="cache_name" label="缓存名称" show-overflow-tooltip>
<template #default="{ row }">
<ElButton
v-hasPerm="['module_monitor:cache:query']"
type="primary"
link
@click="getCacheKeyList(row)"
>
{{ row.cache_name }}
</ElButton>
</template>
</ElTableColumn>
<ElTableColumn prop="remark" label="备注" show-overflow-tooltip />
<ElTableColumn label="操作" width="60" align="center">
<template #default="{ row }">
<ElPopconfirm
class="box-item"
:title="`确认删除缓存 ${row.cache_name} 吗?`"
placement="top"
@confirm="handleClearCacheName(row)"
>
<template #reference>
<ElButton
v-hasPerm="['module_monitor:cache:delete']"
type="danger"
size="small"
link
icon="delete"
/>
</template>
</ElPopconfirm>
</template>
</ElTableColumn>
</ElTable>
</div>
</ElCard>
</ElCol>
@@ -211,53 +214,56 @@
</div>
</div>
</template>
<ElTable
:loading="subLoading"
:data="cacheKeys.map((key) => ({ cacheKey: key }))"
row-key="cacheKey"
>
<template #empty>
<ElEmpty :image-size="80" description="暂无数据" />
</template>
<ElTableColumn prop="cacheKey" label="缓存键名" show-overflow-tooltip>
<template #default="{ row }">
<ElButton
v-hasPerm="['module_monitor:cache:detail']"
type="primary"
link
@click="handleCacheValue(row.cacheKey)"
>
{{ row.cacheKey }}
</ElButton>
<div class="cache-table-wrap">
<ElTable
:loading="subLoading"
:data="cacheKeys.map((key) => ({ cacheKey: key }))"
row-key="cacheKey"
height="100%"
>
<template #empty>
<ElEmpty :image-size="80" description="暂无数据" />
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="60" align="center">
<template #default="{ row }">
<ElPopconfirm
class="box-item"
:title="`确认删除键 ${row.cacheKey} 吗?`"
placement="top"
@confirm="handleClearCacheKey(row.cacheKey)"
>
<template #reference>
<ElButton
v-hasPerm="['module_monitor:cache:delete']"
type="danger"
size="small"
link
icon="delete"
/>
</template>
</ElPopconfirm>
</template>
</ElTableColumn>
</ElTable>
<ElTableColumn prop="cacheKey" label="缓存键名" show-overflow-tooltip>
<template #default="{ row }">
<ElButton
v-hasPerm="['module_monitor:cache:detail']"
type="primary"
link
@click="handleCacheValue(row.cacheKey)"
>
{{ row.cacheKey }}
</ElButton>
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="60" align="center">
<template #default="{ row }">
<ElPopconfirm
class="box-item"
:title="`确认删除键 ${row.cacheKey} 吗?`"
placement="top"
@confirm="handleClearCacheKey(row.cacheKey)"
>
<template #reference>
<ElButton
v-hasPerm="['module_monitor:cache:delete']"
type="danger"
size="small"
link
icon="delete"
/>
</template>
</ElPopconfirm>
</template>
</ElTableColumn>
</ElTable>
</div>
</ElCard>
</ElCol>
<!-- 缓存内容 -->
<ElCol :span="8">
<ElCard :loading="loading" shadow="hover">
<ElCol :span="8" class="cache-mgmt-col">
<ElCard :loading="loading" shadow="hover" class="cache-mgmt-card">
<template #header>
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
@@ -284,23 +290,29 @@
</div>
</div>
</template>
<ElForm :model="cacheForm" label-suffix=":" label-width="auto" label-position="top">
<ElFormItem label="缓存名称">
<ElInput v-model="cacheForm.cache_name" readonly placeholder="缓存名称" />
</ElFormItem>
<ElFormItem label="缓存键名">
<ElInput v-model="cacheForm.cache_key" readonly placeholder="缓存键名" />
</ElFormItem>
<ElFormItem label="缓存内容">
<ElInput
v-model="cacheForm.cache_value"
type="textarea"
:rows="20"
readonly
placeholder="缓存内容"
/>
</ElFormItem>
</ElForm>
<div class="cache-form-wrap">
<ElForm
:model="cacheForm"
label-suffix=":"
label-width="auto"
label-position="top"
>
<ElFormItem label="缓存名称">
<ElInput v-model="cacheForm.cache_name" readonly placeholder="缓存名称" />
</ElFormItem>
<ElFormItem label="缓存键名">
<ElInput v-model="cacheForm.cache_key" readonly placeholder="缓存键名" />
</ElFormItem>
<ElFormItem label="缓存内容" class="cache-value-item">
<ElInput
v-model="cacheForm.cache_value"
type="textarea"
readonly
placeholder="缓存内容"
/>
</ElFormItem>
</ElForm>
</div>
</ElCard>
</ElCol>
</ElRow>
@@ -432,22 +444,19 @@ async function handleCacheValue(cacheKey: string) {
//
const handleClearCacheAll = async () => {
ElMessageBox.confirm("确定要清理全部缓存吗?", "危险!", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
return await CacheAPI.deleteCacheAll();
})
.then(() => {
getCacheNameList();
})
.catch((error: unknown) => {
if (error !== "cancel") {
console.error("清理全部缓存失败:", error);
}
try {
await ElMessageBox.confirm("确定要清理全部缓存吗?", "危险!", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await CacheAPI.deleteCacheAll();
getCacheNameList();
} catch (error: unknown) {
if (error !== "cancel") {
console.error("清理全部缓存失败:", error);
}
}
};
//
@@ -567,7 +576,7 @@ onUnmounted(() => {
flex-direction: column;
min-height: 0;
::v-deep(.el-card__body) {
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
@@ -607,7 +616,30 @@ onUnmounted(() => {
flex-direction: column;
min-height: 0;
::v-deep(.el-card__body) {
:deep(.el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
> .cache-table-wrap,
> .cache-form-wrap {
flex: 1;
min-height: 0;
}
}
}
.cache-table-wrap {
overflow: hidden;
}
.cache-form-wrap {
display: flex;
flex-direction: column;
overflow: hidden;
.el-form {
display: flex;
flex: 1;
flex-direction: column;
@@ -125,21 +125,20 @@ const onlineSearchItems = computed<SearchFormItem[]>(() => [
const clearAllLoading = ref(false);
function kickSession(sessionId: string) {
ElMessageBox.confirm(`确认强制退出会话 ${sessionId}?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
await OnlineAPI.deleteOnline(sessionId);
ElMessage.success("操作成功");
await refreshData();
} catch (error: unknown) {
console.error(error);
}
})
.catch(() => {});
(async () => {
try {
await ElMessageBox.confirm(`确认强制退出会话 ${sessionId}?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await OnlineAPI.deleteOnline(sessionId);
ElMessage.success("操作成功");
await refreshData();
} catch {
//
}
})();
}
const {
@@ -259,24 +258,23 @@ async function onResetSearch() {
}
function handleClearAll() {
ElMessageBox.confirm("确认强制退出所有用户?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
clearAllLoading.value = true;
await OnlineAPI.clearOnline();
ElMessage.success("操作成功");
await refreshData();
} catch (error: unknown) {
console.error(error);
} finally {
clearAllLoading.value = false;
}
})
.catch(() => {});
(async () => {
try {
await ElMessageBox.confirm("确认强制退出所有用户?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
clearAllLoading.value = true;
await OnlineAPI.clearOnline();
ElMessage.success("操作成功");
await refreshData();
} catch {
//
} finally {
clearAllLoading.value = false;
}
})();
}
</script>
@@ -92,7 +92,7 @@
</FaTableHeader>
<!-- 面包屑与表头已占用高度表格高度须在独立 flex 子项内计算否则分页会被挤出卡片 -->
<div class="resource-table-region min-h-0 flex flex-1 flex-col overflow-hidden pb-3">
<div class="resource-table-region min-h-0 flex flex-1 flex-col overflow-hidden">
<FaTable
row-key="file_url"
:show-table-header="false"
@@ -633,14 +633,14 @@ async function handleBatchDelete() {
</script>
<style lang="scss" scoped>
.resource-monitor-page ::v-deep(.resource-monitor-card.el-card > .el-card__body) {
.resource-monitor-page :deep(.resource-monitor-card.el-card > .el-card__body) {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
::v-deep(.el-breadcrumb__item) {
:deep(.el-breadcrumb__item) {
&.is-link {
color: var(--el-color-primary);
cursor: pointer;
@@ -22,8 +22,8 @@ defineEmits<{ link: [] }>();
<style scoped lang="scss">
.login-auth-link-row {
::v-deep(.el-link),
::v-deep(.el-link__inner) {
:deep(.el-link),
:deep(.el-link__inner) {
line-height: inherit;
}
}
@@ -246,11 +246,11 @@ html.dark {
/* EP 全局规则:相邻 .el-button { margin-left: 12px },纵向叠放时仍会命中,导致第二颗按钮横向错位 */
.login-mobile-actions {
::v-deep(.el-button + .el-button) {
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
::v-deep(.el-button) {
:deep(.el-button) {
box-sizing: border-box;
width: 100%;
min-width: 0;
@@ -78,7 +78,7 @@ const qrPayload = computed(() => {
/* EP:相邻按钮横向间距;单列返回按钮无需处理 */
.login-mobile-actions {
::v-deep(.el-button) {
:deep(.el-button) {
box-sizing: border-box;
width: 100%;
min-width: 0;
@@ -546,12 +546,14 @@ const showVoteNotification = () => {
});
};
let voteTimer: ReturnType<typeof setTimeout> | null = null;
onMounted(async () => {
setupAccount("super");
await configStore.getConfig();
await tryConsumeOAuthCallback();
getCaptcha();
setTimeout(showVoteNotification, 500);
voteTimer = setTimeout(showVoteNotification, 500);
});
onActivated(() => {
@@ -561,6 +563,7 @@ onActivated(() => {
});
onBeforeUnmount(() => {
if (voteTimer !== null) clearTimeout(voteTimer);
notificationInstance?.close();
notificationInstance = null;
});
@@ -661,68 +664,68 @@ async function submitForget() {
<style lang="scss" scoped>
.login-page-panel {
::v-deep(.btn) {
:deep(.btn) {
border-radius: 8px;
}
}
html.dark .login-page-panel {
::v-deep(.text-g-800) {
:deep(.text-g-800) {
color: rgb(255 255 255 / 78%) !important;
}
}
/* 副标题与表单块间距(login-page-form 在子组件内,须 :deep */
::v-deep(.login-page-form) {
:deep(.login-page-form) {
margin-top: 1rem;
}
::v-deep(.login-page-form .el-form-item) {
:deep(.login-page-form .el-form-item) {
margin-bottom: 1.1rem;
}
::v-deep(.el-select__wrapper) {
:deep(.el-select__wrapper) {
min-height: 42px !important;
}
/* 仅账号登录底部「手机号 / 扫码」双列按钮需要 42px;手机号登录页的返回钮与主按钮同为 h-11,勿全局压高度 */
::v-deep(.login-secondary-actions .login-secondary-btn) {
:deep(.login-secondary-actions .login-secondary-btn) {
height: 42px;
border-radius: 8px;
}
html.dark {
::v-deep(.login-page-form .el-input__wrapper) {
:deep(.login-page-form .el-input__wrapper) {
background-color: rgb(255 255 255 / 6%) !important;
box-shadow: 0 0 0 1px rgb(255 255 255 / 10%) inset !important;
}
::v-deep(.login-page-form .el-input__inner) {
:deep(.login-page-form .el-input__inner) {
color: rgb(255 255 255 / 92%);
}
::v-deep(.login-page-form .el-input__inner::placeholder) {
:deep(.login-page-form .el-input__inner::placeholder) {
color: rgb(255 255 255 / 35%);
}
::v-deep(.login-page-form .el-select .el-select__wrapper) {
:deep(.login-page-form .el-select .el-select__wrapper) {
background-color: rgb(255 255 255 / 6%) !important;
box-shadow: 0 0 0 1px rgb(255 255 255 / 10%) inset !important;
}
::v-deep(.login-page-form .el-select__placeholder) {
:deep(.login-page-form .el-select__placeholder) {
color: rgb(255 255 255 / 40%);
}
::v-deep(.login-page-form .el-select__selected-item) {
:deep(.login-page-form .el-select__selected-item) {
color: rgb(255 255 255 / 92%);
}
::v-deep(.login-remember .el-checkbox__label) {
:deep(.login-remember .el-checkbox__label) {
color: rgb(255 255 255 / 65%);
}
::v-deep(.login-secondary-btn) {
:deep(.login-secondary-btn) {
color: rgb(255 255 255 / 85%) !important;
background: transparent !important;
border-color: rgb(255 255 255 / 22%) !important;
@@ -735,7 +738,7 @@ html.dark {
}
}
::v-deep(.login-secondary-actions .login-secondary-btn) {
:deep(.login-secondary-actions .login-secondary-btn) {
height: 42px;
}
}
@@ -310,20 +310,21 @@ async function loadDeptData() {
}
}
function deleteDeptRow(id: number) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await DeptAPI.deleteDept([id]);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadDeptData();
})
.catch(() => {});
async function deleteDeptRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DeptAPI.deleteDept([id]);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadDeptData();
} catch {
//
}
}
const opCtx = {
@@ -545,50 +546,46 @@ async function handleSubmit() {
});
}
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await DeptAPI.deleteDept(ids);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadDeptData();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
batchDeleting.value = true;
await DeptAPI.deleteDept(ids);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadDeptData();
} catch {
//
} finally {
batchDeleting.value = false;
}
}
function handleMoreClick(status: string) {
async function handleMoreClick(status: string) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
return;
}
ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
await DeptAPI.batchDept({ ids, status });
await loadDeptData();
await userStore.getUserInfo();
} catch (error: unknown) {
console.error(error);
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DeptAPI.batchDept({ ids, status });
await loadDeptData();
await userStore.getUserInfo();
} catch {
//
}
}
function toggleExpand() {
@@ -614,15 +611,15 @@ onMounted(() => {
</script>
<style scoped lang="scss">
.crud-dialog-art-form ::v-deep(.el-row > .el-col:last-child) {
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form ::v-deep(.el-form-item__content) {
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
::v-deep(.dept-table-actions .inline-flex) {
:deep(.dept-table-actions .inline-flex) {
vertical-align: middle;
}
</style>
@@ -786,71 +786,66 @@ function formatDictDataOperationCell(row: DictDataTable) {
});
}
function deleteDictDataRow(id: number) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await DictAPI.deleteDictData([id]);
dictStore.clearDictData();
if (props.dictType) {
await dictStore.getDict([props.dictType]);
}
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {});
async function deleteDictDataRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DictAPI.deleteDictData([id]);
dictStore.clearDictData();
if (props.dictType) await dictStore.getDict([props.dictType]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
}
}
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await DictAPI.deleteDictData(ids);
dictStore.clearDictData();
if (props.dictType) {
await dictStore.getDict([props.dictType]);
}
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
batchDeleting.value = true;
await DictAPI.deleteDictData(ids);
dictStore.clearDictData();
if (props.dictType) await dictStore.getDict([props.dictType]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
} finally {
batchDeleting.value = false;
}
}
function handleMoreClick(status: string) {
async function handleMoreClick(status: string) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
return;
}
ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await DictAPI.batchDictData({ ids, status });
await refreshData();
dictStore.clearDictData();
if (props.dictType) {
await dictStore.getDict([props.dictType]);
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DictAPI.batchDictData({ ids, status });
await refreshData();
dictStore.clearDictData();
if (props.dictType) await dictStore.getDict([props.dictType]);
} catch {
//
}
}
function openExportModal() {
@@ -890,11 +885,11 @@ function openExportModal() {
border-color: var(--el-border-color-lighter);
}
.crud-dialog-art-form ::v-deep(.el-row > .el-col:last-child) {
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form ::v-deep(.el-form-item__content) {
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
</style>
@@ -560,74 +560,69 @@ async function handleSubmit() {
});
}
function deleteDictTypeRow(id: number) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await DictAPI.deleteDictType([id]);
dictStore.clearDictData();
const dictTypes = Object.keys(dictStore.dictData);
if (dictTypes.length > 0) {
await dictStore.getDict(dictTypes);
}
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {});
async function deleteDictTypeRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DictAPI.deleteDictType([id]);
dictStore.clearDictData();
const dictTypes = Object.keys(dictStore.dictData);
if (dictTypes.length > 0) await dictStore.getDict(dictTypes);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
}
}
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await DictAPI.deleteDictType(ids);
dictStore.clearDictData();
const dictTypes = Object.keys(dictStore.dictData);
if (dictTypes.length > 0) {
await dictStore.getDict(dictTypes);
}
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
batchDeleting.value = true;
await DictAPI.deleteDictType(ids);
dictStore.clearDictData();
const dictTypes = Object.keys(dictStore.dictData);
if (dictTypes.length > 0) await dictStore.getDict(dictTypes);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
} finally {
batchDeleting.value = false;
}
}
function handleMoreClick(status: string) {
async function handleMoreClick(status: string) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
return;
}
ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await DictAPI.batchDictType({ ids, status });
await refreshData();
dictStore.clearDictData();
const dictTypes = Object.keys(dictStore.dictData);
if (dictTypes.length > 0) {
await dictStore.getDict(dictTypes);
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确认${status === "0" ? "启用" : "停用"}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await DictAPI.batchDictType({ ids, status });
await refreshData();
dictStore.clearDictData();
const dictTypes = Object.keys(dictStore.dictData);
if (dictTypes.length > 0) await dictStore.getDict(dictTypes);
} catch {
//
}
}
function openExportModal() {
@@ -636,11 +631,11 @@ function openExportModal() {
</script>
<style scoped lang="scss">
.crud-dialog-art-form ::v-deep(.el-row > .el-col:last-child) {
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form ::v-deep(.el-form-item__content) {
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
</style>
@@ -461,19 +461,20 @@ async function handleOpenDialog(id: number) {
dialogVisible.value.visible = true;
}
function deleteLogRow(id: number) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await LogAPI.deleteLog([id]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {});
async function deleteLogRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await LogAPI.deleteLog([id]);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
}
}
function buildLogRowActions(row: LogTable): TableOperationAction[] {
@@ -507,26 +508,25 @@ function formatLogOperationCell(row: LogTable) {
});
}
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await LogAPI.deleteLog(ids);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
batchDeleting.value = true;
await LogAPI.deleteLog(ids);
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
} finally {
batchDeleting.value = false;
}
}
function openExportModal() {
@@ -436,7 +436,7 @@
<ElFormItem v-if="formData.type !== MenuTypeEnum.BUTTON" label="图标" prop="icon">
<!-- 图标选择器 -->
<icon-select v-model="formData.icon" />
<FaIconSelect v-model="formData.icon" />
</ElFormItem>
<ElFormItem
@@ -527,6 +527,7 @@ import FaTableHeaderLeft from "@/components/tables/fa-table-header-left/index.vu
import FaSearchBar 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 FaIconSelect from "@/components/others/fa-icon-select/index.vue";
import { ElMessage, ElMessageBox, ElTag, ElTooltip } from "element-plus";
import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@utils/table";
@@ -820,20 +821,22 @@ function toggleExpand() {
});
}
function deleteMenuRow(id: number) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await MenuAPI.deleteMenu([id]);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadMenuData();
})
.catch(() => {});
async function deleteMenuRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await MenuAPI.deleteMenu([id]);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadMenuData();
} catch {
//
}
}
const opCtx = {
@@ -1040,11 +1043,11 @@ async function handleOpenDialog(
Object.assign(detailFormData.value, response.data.data ?? {});
} else if (type === "update") {
dialogVisible.title = "修改菜单";
Object.assign(formData, response.data.data);
Object.assign(formData.value, response.data.data);
}
} else {
dialogVisible.title = "新增菜单";
Object.assign(formData, initialFormData);
formData.value = { ...initialFormData };
if (parentRow?.id != null) {
formData.value.parent_id = parentRow.id;
formData.value.client = (parentRow.client as MenuClientEnum) || menuClientTab.value;
@@ -1102,27 +1105,26 @@ async function handleSubmit() {
});
}
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await MenuAPI.deleteMenu(ids);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadMenuData();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
batchDeleting.value = true;
await MenuAPI.deleteMenu(ids);
await userStore.getUserInfo();
ElMessage.success("删除成功");
selectedRows.value = [];
await loadMenuData();
} catch {
//
} finally {
batchDeleting.value = false;
}
}
async function handleMoreClick(status: string) {
@@ -1135,16 +1137,18 @@ async function handleMoreClick(status: string) {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
await MenuAPI.batchMenu({ ids, status });
await loadMenuData();
} catch (error: unknown) {
console.error(error);
}
})
.catch(() => {});
});
try {
await ElMessageBox.confirm("确认启用或停用该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await MenuAPI.batchMenu({ ids, status });
await loadMenuData();
} catch {
//
}
}
onMounted(() => {
@@ -1153,7 +1157,7 @@ onMounted(() => {
</script>
<style scoped lang="scss">
::v-deep(.menu-table-actions .inline-flex) {
:deep(.menu-table-actions .inline-flex) {
vertical-align: middle;
}
</style>
@@ -637,20 +637,22 @@ async function handleSubmit() {
});
}
function deleteNoticeRow(id: number) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await NoticeAPI.deleteNotice([id]);
await noticeStore.getNotice();
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {});
async function deleteNoticeRow(id: number) {
try {
await ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await NoticeAPI.deleteNotice([id]);
await noticeStore.getNotice();
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
}
}
function buildNoticeRowActions(row: NoticeTable): TableOperationAction[] {
@@ -694,30 +696,29 @@ function formatNoticeOperationCell(row: NoticeTable) {
});
}
function handleBatchDelete() {
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await NoticeAPI.deleteNotice(ids);
await noticeStore.getNotice();
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条数据吗?`, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
batchDeleting.value = true;
await NoticeAPI.deleteNotice(ids);
await noticeStore.getNotice();
ElMessage.success("删除成功");
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
//
} finally {
batchDeleting.value = false;
}
}
function handleMoreClick(status: string) {
async function handleMoreClick(status: string) {
const ids = selectedIds.value;
if (!ids.length) {
ElMessage.warning("请先选择要操作的数据");
@@ -727,13 +728,14 @@ function handleMoreClick(status: string) {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await NoticeAPI.batchNotice({ ids, status });
await refreshData();
await noticeStore.getNotice();
})
.catch(() => {});
});
try {
await NoticeAPI.batchNotice({ ids, status });
await refreshData();
await noticeStore.getNotice();
} catch {
//
}
}
function openExportModal() {
@@ -747,11 +749,11 @@ onMounted(async () => {
<style scoped lang="scss">
/* FaForm 底部预留的操作栏列在弹窗内不需要占位 */
.crud-dialog-art-form ::v-deep(.el-row > .el-col:last-child) {
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form ::v-deep(.el-form-item__content) {
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
@@ -773,27 +775,27 @@ onMounted(async () => {
color: var(--el-text-color-placeholder);
}
.notice-html-preview ::v-deep(h1),
.notice-html-preview ::v-deep(h2),
.notice-html-preview ::v-deep(h3) {
.notice-html-preview :deep(h1),
.notice-html-preview :deep(h2),
.notice-html-preview :deep(h3) {
margin: 12px 0 8px;
}
.notice-html-preview ::v-deep(p) {
.notice-html-preview :deep(p) {
margin: 8px 0;
line-height: 1.6;
}
.notice-html-preview ::v-deep(table) {
.notice-html-preview :deep(table) {
margin: 12px 0;
}
.notice-html-preview ::v-deep(table th),
.notice-html-preview ::v-deep(table td) {
.notice-html-preview :deep(table th),
.notice-html-preview :deep(table td) {
padding: 8px 12px;
}
.notice-html-preview ::v-deep(pre) {
.notice-html-preview :deep(pre) {
padding: 12px;
margin: 12px 0;
overflow-x: auto;
@@ -801,14 +803,14 @@ onMounted(async () => {
border-radius: 4px;
}
.notice-html-preview ::v-deep(blockquote) {
.notice-html-preview :deep(blockquote) {
padding-left: 16px;
margin: 12px 0;
color: var(--el-text-color-regular);
border-left: 4px solid var(--el-color-primary);
}
.notice-html-preview ::v-deep(img) {
.notice-html-preview :deep(img) {
max-width: 100%;
height: auto;
}

Some files were not shown because too many files have changed in this diff Show More