refactor: 优化通用组件实现与工具类型

- src/components/DictSelect: 改用 defineModel 与 computed,修复选项加载时清空已选值与父子值分叉
- src/components/ThemeSwitcher: ThemeSwitch 改名,修正 theneList 拼写
- src/composables/useTableSelection: 显式判空,避免 id 为 0 的行被过滤
- src/utils/download: 收窄入参类型,headers 改为可选
- src/api/system/user: 导出与模板下载接口声明 Blob 响应类型
- src/views/system/dict: 标签类型选项排除空串,el-tag 去掉 as any
- src/views/system/user/components/UserImportDialog: 去掉响应类型的 any 标注
- src/layouts、src/views/login: 删除可自动导入组件的冗余 import
This commit is contained in:
Ray.Hao committed 2026-09-01 07:26:09 +08:00
1 parent cf5768dbf4
commit c9bd327fc0
12 files changed
+158 -132

No files matched your search

+80 -63
View File
@@ -1,7 +1,7 @@
<template>
<div class="login-page">
<div class="login-toolbar">
<ThemeSwitch />
<ThemeSwitcher />
<LangSelect size="text-18px" />
</div>
@@ -21,10 +21,7 @@
<span class="login-brand__tag-dot" />
Enterprise Ready
</el-tag>
<h1 class="login-brand__title">企业级管理系统</h1>
<p class="login-brand__desc">
提供安全、高效、可扩展的管理解决方案,助力企业数字化转型与业务增长。
</p>
<h1 class="login-brand__title">开箱即用的企业级中后台解决方案</h1>
</div>
<div class="login-brand__features">
<div class="login-brand__feature">
@@ -171,7 +168,6 @@ import router from "@/router";
import { useUserStore } from "@/stores";
import { AuthStorage } from "@/utils/auth";
import { appConfig } from "@/settings";
import ThemeSwitch from "@/components/ThemeSwitch/index.vue";
import ResetPwd from "./components/ResetPwd.vue";
import QrCodeLogin from "./components/QrCodeLogin.vue";
import logo from "@/assets/images/logo.png";
@@ -251,7 +247,7 @@ onMounted(() => getCaptcha());
</script>
<style lang="scss" scoped>
$primary: #5d87ff;
$primary: #165dff;
$bg: #f8fafc;
$text-primary: #273248;
$text-secondary: #667085;
@@ -310,8 +306,13 @@ $input-h: 44px;
}
&__logo {
box-sizing: border-box;
width: 42px;
height: 42px;
padding: 5px;
background: rgb(37 99 235 / 8%);
border: 1px solid rgb(37 99 235 / 18%);
border-radius: 12px;
}
&__identity {
@@ -325,7 +326,7 @@ $input-h: 44px;
font-size: 24px;
font-weight: 600;
line-height: 1;
color: $text-primary;
color: #1e3a8a;
}
&__version {
@@ -336,9 +337,9 @@ $input-h: 44px;
font-size: 12px;
font-weight: 600;
line-height: 1;
color: rgba($primary, 0.88);
background: rgba($primary, 0.07);
border: 1px solid rgba($primary, 0.13);
color: rgb(30 58 138 / 80%);
background: rgb(37 99 235 / 8%);
border: 1px solid rgb(37 99 235 / 16%);
border-radius: 999px;
}
@@ -347,8 +348,8 @@ $input-h: 44px;
flex: 1;
flex-direction: column;
justify-content: center;
width: min(720px, 100%);
padding: 20px 0 88px;
width: min(760px, 100%);
padding: 40px 0 60px;
}
&__main {
@@ -357,13 +358,14 @@ $input-h: 44px;
&__tag {
gap: 8px;
height: 28px;
padding: 0 13px 0 11px;
margin-bottom: 18px;
height: 32px;
padding: 0 15px 0 13px;
margin-bottom: 20px;
font-size: 12px;
font-weight: 700;
color: $primary;
background: rgba($primary, 0.035);
border-color: rgba($primary, 0.14);
color: #1d4ed8;
background: rgb(37 99 235 / 8%);
border-color: rgb(37 99 235 / 16%);
:deep(.el-tag__content) {
display: inline-flex;
@@ -377,26 +379,26 @@ $input-h: 44px;
flex-shrink: 0;
width: 7px;
height: 7px;
background: $primary;
background: #2563eb;
border-radius: 50%;
box-shadow: 0 0 0 3px rgba($primary, 0.12);
box-shadow: 0 0 0 3px rgb(37 99 235 / 16%);
}
&__title {
margin: 0 0 18px;
font-size: 46px;
margin: 0;
font-size: 36px;
font-weight: 800;
line-height: 1.18;
color: #222b3a;
line-height: 1.15;
color: #1e3a8a;
letter-spacing: 0;
}
&__desc {
max-width: 560px;
margin: 0;
font-size: 16px;
line-height: 1.75;
color: $text-secondary;
font-size: 18px;
line-height: 1.6;
color: #475569;
}
&__features {
@@ -404,19 +406,19 @@ $input-h: 44px;
align-items: center;
width: fit-content;
max-width: 100%;
margin-top: 28px;
margin-top: 32px;
}
&__feature {
position: relative;
display: inline-flex;
gap: 8px;
gap: 10px;
align-items: center;
height: 28px;
padding: 0 13px;
font-size: 13px;
height: 32px;
padding: 0 15px;
font-size: 14px;
font-weight: 600;
color: $text-primary;
color: rgb(30 58 138 / 85%);
background: transparent;
&:first-child {
@@ -430,7 +432,7 @@ $input-h: 44px;
width: 1px;
height: 14px;
content: "";
background: rgba(39 50 72 / 12%);
background: rgb(37 99 235 / 18%);
}
}
@@ -438,11 +440,11 @@ $input-h: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
color: $primary;
background: rgba($primary, 0.08);
border: 1px solid rgba($primary, 0.1);
width: 22px;
height: 22px;
color: #1d4ed8;
background: rgb(37 99 235 / 8%);
border: 1px solid rgb(37 99 235 / 16%);
border-radius: 6px;
}
@@ -450,9 +452,9 @@ $input-h: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 13px;
height: 13px;
color: $primary;
width: 14px;
height: 14px;
color: #1d4ed8;
}
&__feature-text {
@@ -488,8 +490,8 @@ $input-h: 44px;
}
&__title {
margin: 0 0 4px;
font-size: 34px;
margin: 0 0 10px;
font-size: 28px;
font-weight: 750;
line-height: 1.1;
color: $text-primary;
@@ -497,8 +499,8 @@ $input-h: 44px;
}
&__desc {
margin: 8px 0 24px;
font-size: 14px;
margin: 10px 0 28px;
font-size: 15px;
color: $text-muted;
}
}
@@ -579,7 +581,7 @@ $input-h: 44px;
.login-btn {
width: 100%;
height: 44px;
font-size: 15px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
box-shadow: 0 12px 24px rgba($primary, 0.18);
@@ -627,7 +629,7 @@ $input-h: 44px;
justify-content: center;
height: 44px;
padding: 0;
font-size: 13px;
font-size: 14px;
color: $text-secondary;
cursor: pointer;
background: transparent;
@@ -664,41 +666,56 @@ $input-h: 44px;
.dark .login-brand {
background-image: url("@/assets/images/login/bg-dark.svg");
&__logo {
background: rgb(255 255 255 / 10%);
border-color: rgb(255 255 255 / 20%);
}
&__name {
color: rgb(255 255 255 / 86%);
color: rgb(255 255 255 / 92%);
}
&__version {
color: rgb(167 190 255 / 92%);
background: rgba($primary, 0.12);
border-color: rgba($primary, 0.2);
color: rgb(255 255 255 / 85%);
background: rgb(255 255 255 / 10%);
border-color: rgb(255 255 255 / 16%);
}
&__tag {
color: rgba($primary, 0.95);
background: rgba($primary, 0.08);
border-color: rgba($primary, 0.18);
color: #fff;
background: rgb(255 255 255 / 8%);
border-color: rgb(255 255 255 / 16%);
}
&__tag-dot {
background: #fff;
box-shadow: 0 0 0 3px rgb(255 255 255 / 18%);
}
&__title {
color: rgb(255 255 255 / 90%);
color: #fff;
}
&__desc {
color: rgb(226 232 240 / 62%);
color: rgb(255 255 255 / 65%);
}
&__feature {
color: rgb(255 255 255 / 76%);
color: rgb(255 255 255 / 85%);
&:not(:last-child)::after {
background: rgba(255 255 255 / 12%);
background: rgb(255 255 255 / 18%);
}
}
&__feature-mark {
background: rgba($primary, 0.15);
border-color: rgba($primary, 0.18);
color: #fff;
background: rgb(255 255 255 / 10%);
border-color: rgb(255 255 255 / 16%);
}
&__feature-icon {
color: #fff;
}
}
@@ -760,7 +777,7 @@ $input-h: 44px;
height: auto;
min-height: auto;
padding: 28px 40px 0;
background: #fff;
background: linear-gradient(180deg, #ffffff 0%, #dbeafe 100%);
&__hero {
display: none;
+2 -2
View File
@@ -136,7 +136,7 @@
</template>
<el-option v-for="type in tagTypeOptions" :key="type" :label="type" :value="type">
<div flex-y-center gap-10px>
<el-tag :type="type as any">{{ formData.label ?? "字典标签" }}</el-tag>
<el-tag :type="type">{{ formData.label ?? "字典标签" }}</el-tag>
<span>{{ type }}</span>
</div>
</el-option>
@@ -180,7 +180,7 @@ const queryFormRef = ref<FormInstance>();
const dictItemFormRef = ref<FormInstance>();
// 标签类型可选项。
const tagTypeOptions: NonNullable<DictItemForm["tagType"]>[] = [
const tagTypeOptions: Exclude<NonNullable<DictItemForm["tagType"]>, "">[] = [
"primary",
"success",
"info",
+2 -2
View File
@@ -533,8 +533,8 @@ const showRouteParams = computed(() => formData.type === MenuTypeEnum.MENU);
const routeNameTooltip = computed(() =>
isEmbeddedExternal.value
? "开启缓存时填写,需和内嵌页路由名称保持一致,例如 Apifox"
: "开启缓存时填写,需和页面组件 name 保持一致,例如 User"
? "路由唯一标识,需全局唯一,确保多个内嵌页面互不冲突,例如 Apifox"
: "路由唯一标识,需全局唯一,用于动态路由注册与命令面板检索,例如 User"
);
const routeNamePlaceholder = computed(() => (isEmbeddedExternal.value ? "Apifox" : "User"));
@@ -144,7 +144,7 @@ function handleFileExceed(): void {
* 下载导入模板
*/
function downloadTemplate(): void {
UserAPI.downloadTemplate().then((response: any) => {
UserAPI.downloadTemplate().then((response) => {
downloadFile(response);
});
}