mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-06 09:41:53 +00:00
feat(web): 升级至 v3.0 并重构组件与引导流程
- 提取应用引导逻辑至 useAppBootstrap 组合式函数 - 增强日期选择器组件,支持全类型与格式自动推导 - 修复资源泄漏:清理 RAF、定时器、事件与 mitt 监听 - 修复退出登录逻辑与异步导出流程 - 迁移 ::v-deep 至 Vue 3 标准 :deep 语法 - 移除调试日志并改进类型定义与注释文档
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user