mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-23 13:13:09 +00:00
feat(web): 升级至 v3.0 并重构组件与引导流程
- 提取应用引导逻辑至 useAppBootstrap 组合式函数 - 增强日期选择器组件,支持全类型与格式自动推导 - 修复资源泄漏:清理 RAF、定时器、事件与 mitt 监听 - 修复退出登录逻辑与异步导出流程 - 迁移 ::v-deep 至 Vue 3 标准 :deep 语法 - 移除调试日志并改进类型定义与注释文档
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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";
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<!-- 布局容器 -->
|
||||
<!--
|
||||
布局根容器:三区域结构
|
||||
- #app-sidebar ← 左侧菜单导航(收起/展开)
|
||||
- #app-main ← 右侧主区域(顶栏 + 页面内容)
|
||||
- #app-header 顶栏(面包屑、搜索、通知、用户菜单)
|
||||
- #app-content 页面内容(RouterView + 页签)
|
||||
- #app-global ← 全局浮层层(Toast、Modal、新手引导)
|
||||
-->
|
||||
<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 };
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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");
|
||||
})();
|
||||
|
||||
@@ -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-auth、v-highlight 等,依赖 router 的 meta 权限)
|
||||
* 5. initErrorHandle 全局错误处理(window.onerror、unhandledrejection)
|
||||
* 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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 || "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 静态路由定义 + IframeRouteManager。
|
||||
*
|
||||
* 静态路由 = 首屏即注册的路由(Layout、登录页、404/500、iframe 占位等),
|
||||
* 不依赖菜单权限,用户未登录时即可访问。
|
||||
*
|
||||
* 动态路由由 `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 || "";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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. 清除 localStorage(Pinia 持久化数据)
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/, "");
|
||||
|
||||
@@ -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 对齐) ---
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user