Merge branch 'master' of https://gitee.com/youlaiorg/vue3-element-admin
This commit is contained in:
16
README.md
16
README.md
@@ -1,19 +1,3 @@
|
|||||||
<div align="center">
|
|
||||||
|
|
||||||
## 🎉 正在参加 Gitee 2025 最受欢迎开源软件评选
|
|
||||||
|
|
||||||
<a href="https://gitee.com/activity/2025opensource?ident=I6VXEH" target="_blank">
|
|
||||||
<img src="https://img.shields.io/badge/VUE3--ELEMENT--ADMIN-点击投票支持-f97316?style=for-the-badge&logo=gitee&logoColor=white&labelColor=111827" alt="投票" height="50"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>📢 投票最后1天!恳请助力一票 🙏 无需重复投,您的支持是我们前行的最大动力!</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="vue3-element-admin" width="80" height="80" src="./src/assets/images/logo.png">
|
<img alt="vue3-element-admin" width="80" height="80" src="./src/assets/images/logo.png">
|
||||||
<h1>vue3-element-admin</h1>
|
<h1>vue3-element-admin</h1>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
"vue-draggable-plus": "^0.6.0",
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-i18n": "^11.2.7",
|
"vue-i18n": "^11.2.7",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
"vxe-table": "~4.17.33"
|
"vxe-table": "~4.6.25"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.2.0",
|
"@commitlint/cli": "^20.2.0",
|
||||||
|
|||||||
7304
pnpm-lock.yaml
generated
7304
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -30,8 +30,8 @@ const size = computed(() => appStore.size as ComponentSize);
|
|||||||
const showWatermark = computed(() => settingsStore.showWatermark);
|
const showWatermark = computed(() => settingsStore.showWatermark);
|
||||||
const watermarkContent = appConfig.name;
|
const watermarkContent = appConfig.name;
|
||||||
|
|
||||||
// 只有在启<EFBFBD>?AI 助手且用户已登录时才显示
|
// 只有在启用 AI 助手且用户已登录时才显示
|
||||||
// 使用 userInfo 作为响应式依赖,当用户退出登录时会自动更<EFBFBD>?
|
// 使用 userInfo 作为响应式依赖,当用户退出登录时会自动更新
|
||||||
const enableAiAssistant = computed(() => {
|
const enableAiAssistant = computed(() => {
|
||||||
const isEnabled = settingsStore.enableAiAssistant;
|
const isEnabled = settingsStore.enableAiAssistant;
|
||||||
const isLoggedIn = userStore.userInfo && Object.keys(userStore.userInfo).length > 0;
|
const isLoggedIn = userStore.userInfo && Object.keys(userStore.userInfo).length > 0;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
UserProfileDetail,
|
UserProfileDetail,
|
||||||
UserProfileForm,
|
UserProfileForm,
|
||||||
PasswordChangeForm,
|
PasswordChangeForm,
|
||||||
|
PasswordVerifyForm,
|
||||||
MobileUpdateForm,
|
MobileUpdateForm,
|
||||||
EmailUpdateForm,
|
EmailUpdateForm,
|
||||||
OptionItem,
|
OptionItem,
|
||||||
@@ -193,6 +194,15 @@ const UserAPI = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 解绑手机号 */
|
||||||
|
unbindMobile(data: PasswordVerifyForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/mobile`,
|
||||||
|
method: "delete",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/** 发送邮箱验证码(绑定或更换邮箱)*/
|
/** 发送邮箱验证码(绑定或更换邮箱)*/
|
||||||
sendEmailCode(email: string) {
|
sendEmailCode(email: string) {
|
||||||
return request({
|
return request({
|
||||||
@@ -211,6 +221,15 @@ const UserAPI = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 解绑邮箱 */
|
||||||
|
unbindEmail(data: PasswordVerifyForm) {
|
||||||
|
return request({
|
||||||
|
url: `${USER_BASE_URL}/email`,
|
||||||
|
method: "delete",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取用户下拉列表
|
* 获取用户下拉列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-table-column
|
<el-table-column :prop :label :fixed :align :show-overflow-tooltip :width="finalWidth">
|
||||||
:label="label"
|
|
||||||
:fixed="fixed"
|
|
||||||
:align="align"
|
|
||||||
:show-overflow-tooltip="showOverflowTooltip"
|
|
||||||
:width="finalWidth"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-auto-width class="operation-buttons">
|
<div v-auto class="operation-button">
|
||||||
<slot :row="row"></slot>
|
<slot :row="row" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/**
|
||||||
|
* 表格数据长度
|
||||||
|
* 用于性能优化,避免多次计算宽度
|
||||||
|
*/
|
||||||
listDataLength: number;
|
listDataLength: number;
|
||||||
prop?: string;
|
prop?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
fixed?: string;
|
fixed?: string;
|
||||||
align?: string;
|
align?: string;
|
||||||
width?: number;
|
|
||||||
showOverflowTooltip?: boolean;
|
showOverflowTooltip?: boolean;
|
||||||
|
/**
|
||||||
|
* 最小宽度,优先级高于自动计算宽度,默认80px
|
||||||
|
* @default 80px
|
||||||
|
*/
|
||||||
minWidth?: number;
|
minWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,62 +31,43 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
label: "操作",
|
label: "操作",
|
||||||
fixed: "right",
|
fixed: "right",
|
||||||
align: "center",
|
align: "center",
|
||||||
minWidth: 80,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const count = ref(0);
|
const count = ref(0);
|
||||||
const operationWidth = ref(props.minWidth || 80);
|
const maxWidth = ref(80);
|
||||||
|
|
||||||
// 霈∠<E99C88><E288A0>滢<EFBFBD><E6BBA2>堒捐摨?
|
|
||||||
const calculateWidth = () => {
|
const calculateWidth = () => {
|
||||||
count.value++;
|
count.value++;
|
||||||
|
|
||||||
if (count.value !== props.listDataLength) return;
|
if (count.value !== props.listDataLength) return;
|
||||||
const maxWidth = getOperationMaxWidth();
|
|
||||||
operationWidth.value = Math.max(maxWidth, props.minWidth);
|
let totalWidth = 0;
|
||||||
|
maxWidth.value = 80; // 重置为初始值
|
||||||
|
const els = document.getElementsByClassName("operation-button");
|
||||||
|
Array.from(els).forEach((el) => {
|
||||||
|
const buttons = el.querySelectorAll(".el-button");
|
||||||
|
totalWidth = Array.from(buttons).reduce((prev, button) => {
|
||||||
|
// 14 是按钮之间的距离
|
||||||
|
// 组成:按钮的左边距(Element Plus默认为12px)+按钮的padding(Element Plus默认为2px)
|
||||||
|
return prev + button.scrollWidth + 14;
|
||||||
|
}, 24); // 24 是左右内边距
|
||||||
|
|
||||||
|
maxWidth.value = Math.max(maxWidth.value, totalWidth);
|
||||||
|
});
|
||||||
|
|
||||||
count.value = 0;
|
count.value = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 霈∠<E99C88><E288A0><EFBFBD>蝏<EFBFBD>捐摨?
|
const vAuto = {
|
||||||
|
mounted: () => {
|
||||||
|
// 初次挂载的时候计算一次
|
||||||
|
calculateWidth();
|
||||||
|
},
|
||||||
|
updated: () => {
|
||||||
|
// 数据更新时重新计算一次
|
||||||
|
calculateWidth();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const finalWidth = computed(() => {
|
const finalWidth = computed(() => {
|
||||||
return props.width || operationWidth.value || props.minWidth;
|
return props.minWidth || maxWidth.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
// <20>芷<EFBFBD><E88AB7><EFBFBD>摰賢漲<E8B3A2><E6BCB2>誘
|
|
||||||
const vAutoWidth = {
|
|
||||||
mounted() {
|
|
||||||
// <20>脲活<E884B2><E6B4BB>蝸<EFBFBD><E89DB8>𧒄<EFBFBD>躰恣蝞𦯀<E89D9E>甈?
|
|
||||||
calculateWidth();
|
|
||||||
},
|
|
||||||
updated() {
|
|
||||||
// <20>唳旿<E594B3>湔鰵<E6B994>園<EFBFBD><E59C92>啗恣蝞𦯀<E89D9E>甈?
|
|
||||||
calculateWidth();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <20>瑕<EFBFBD><E79195>厰僼<E58EB0>圈<EFBFBD><E59C88><EFBFBD>捐撣行䔉<E8A18C>瑕<EFBFBD><E79195>滢<EFBFBD>蝏<EFBFBD><E89D8F><EFBFBD><EFBFBD>憭批捐摨?
|
|
||||||
* 瘜冽<E7989C>雿輻鍂<E8BCBB>園<EFBFBD>閬<EFBFBD>蝙<EFBFBD>?`class="operation-buttons"` <20><><EFBFBD>蝑曉<E89D91>鋆寞<E98B86>雿𨀣<E99BBF><F0A880A3>?
|
|
||||||
* @returns {number} 餈𥪜<E9A488><F0A5AA9C>滢<EFBFBD>蝏<EFBFBD><E89D8F><EFBFBD><EFBFBD>憭批捐摨?
|
|
||||||
*/
|
|
||||||
const getOperationMaxWidth = () => {
|
|
||||||
const el = document.getElementsByClassName("operation-buttons");
|
|
||||||
|
|
||||||
// <20>𡝗<EFBFBD>雿𦦵<E99BBF><F0A6A6B5><EFBFBD><EFBFBD>憭批捐摨?
|
|
||||||
let maxWidth = 0;
|
|
||||||
let totalWidth: any = 0;
|
|
||||||
Array.prototype.forEach.call(el, (item) => {
|
|
||||||
// <20>瑕<EFBFBD>瘥譍葵item<65><6D>om
|
|
||||||
const buttons = item.querySelectorAll(".el-button");
|
|
||||||
// <20>瑕<EFBFBD>瘥讛<E798A5><E8AE9B>厰僼<E58EB0><E583BC><EFBFBD>餃捐摨?
|
|
||||||
totalWidth = Array.from(buttons).reduce((acc, button: any) => {
|
|
||||||
return acc + button.scrollWidth + 22; // 瘥譍葵<E8AD8D>厰僼<E58EB0><E583BC>捐摨血<E691A8>銝𢠃<E98A9D><F0A2A083>坔捐摨?
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// <20>瑕<EFBFBD><E79195><EFBFBD>憭抒<E686AD>摰賢漲
|
|
||||||
if (totalWidth > maxWidth) maxWidth = totalWidth;
|
|
||||||
});
|
|
||||||
|
|
||||||
return maxWidth;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import type { FileInfo } from "@/types/api";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
* 请求携带的额外参<EFBFBD>?
|
* 请求携带的额外参数
|
||||||
*/
|
*/
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -122,7 +122,7 @@ const modelValue = defineModel("modelValue", {
|
|||||||
|
|
||||||
const fileList = ref([] as UploadFile[]);
|
const fileList = ref([] as UploadFile[]);
|
||||||
|
|
||||||
// 监听 modelValue 转换用于显示<EFBFBD>?fileList
|
// 监听 modelValue 转换用于显示到 fileList
|
||||||
watch(
|
watch(
|
||||||
modelValue,
|
modelValue,
|
||||||
(value) => {
|
(value) => {
|
||||||
@@ -142,7 +142,7 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传前校<EFBFBD>?
|
* 上传前校验
|
||||||
*/
|
*/
|
||||||
function handleBeforeUpload(file: UploadRawFile) {
|
function handleBeforeUpload(file: UploadRawFile) {
|
||||||
// 限制文件大小
|
// 限制文件大小
|
||||||
@@ -194,7 +194,7 @@ function handleExceed() {
|
|||||||
*/
|
*/
|
||||||
const handleSuccess = (response: any, uploadFile: UploadFile, files: UploadFiles) => {
|
const handleSuccess = (response: any, uploadFile: UploadFile, files: UploadFiles) => {
|
||||||
ElMessage.success("上传成功");
|
ElMessage.success("上传成功");
|
||||||
//只有当状态为success或者fail,代表文件上传全部完成了,失败也算完<EFBFBD>?
|
// 只有当状态为 success 或者 fail,代表文件上传全部完成了,失败也算完成
|
||||||
if (
|
if (
|
||||||
files.every((file: UploadFile) => {
|
files.every((file: UploadFile) => {
|
||||||
return file.status === "success" || file.status === "fail";
|
return file.status === "success" || file.status === "fail";
|
||||||
@@ -203,7 +203,7 @@ const handleSuccess = (response: any, uploadFile: UploadFile, files: UploadFiles
|
|||||||
const fileInfos = [] as FileInfo[];
|
const fileInfos = [] as FileInfo[];
|
||||||
files.map((file: UploadFile) => {
|
files.map((file: UploadFile) => {
|
||||||
if (file.status === "success") {
|
if (file.status === "success") {
|
||||||
//只取携带response的才是刚上传<EFBFBD>?
|
// 只取携带 response 的才是刚上传的
|
||||||
const res = file.response as FileInfo;
|
const res = file.response as FileInfo;
|
||||||
if (res) {
|
if (res) {
|
||||||
fileInfos.push({ name: res.name, url: res.url } as FileInfo);
|
fileInfos.push({ name: res.name, url: res.url } as FileInfo);
|
||||||
@@ -251,7 +251,7 @@ function handleDownload(file: UploadUserFile) {
|
|||||||
|
|
||||||
/** 获取一个不重复的id */
|
/** 获取一个不重复的id */
|
||||||
function getUid(): number {
|
function getUid(): number {
|
||||||
// 时间戳左<EFBFBD>?3位(相当于乘<EFBFBD>?192<39>?+ 4位随机数
|
// 时间戳左移 13 位(相当于乘以 8192)+ 13 位随机数
|
||||||
return (Date.now() << 13) | Math.floor(Math.random() * 8192);
|
return (Date.now() << 13) | Math.floor(Math.random() * 8192);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const props = defineProps({
|
|||||||
default: 10,
|
default: 10,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 单个文件的最大允许大<EFBFBD>?
|
* 单个文件的最大允许大小
|
||||||
*/
|
*/
|
||||||
maxFileSize: {
|
maxFileSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -84,7 +84,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const previewVisible = ref(false); // 是否显示预览
|
const previewVisible = ref(false); // 是否显示预览
|
||||||
const previewImageIndex = ref(0); // 预览图片的索<EFBFBD>?
|
const previewImageIndex = ref(0); // 预览图片的索引
|
||||||
|
|
||||||
const modelValue = defineModel("modelValue", {
|
const modelValue = defineModel("modelValue", {
|
||||||
type: [Array] as PropType<string[]>,
|
type: [Array] as PropType<string[]>,
|
||||||
@@ -108,22 +108,22 @@ function handleRemove(imageUrl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传前校<EFBFBD>?
|
* 上传前校验
|
||||||
*/
|
*/
|
||||||
function handleBeforeUpload(file: UploadRawFile) {
|
function handleBeforeUpload(file: UploadRawFile) {
|
||||||
// 校验文件类型:虽<EFBFBD>?accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符<EFBFBD>?accept 的规<EFBFBD>?
|
// 校验文件类型:虽然 accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符合 accept 的规则
|
||||||
const acceptTypes = props.accept.split(",").map((type) => type.trim());
|
const acceptTypes = props.accept.split(",").map((type) => type.trim());
|
||||||
|
|
||||||
// 检查文件格式是否符合 accept
|
// 检查文件格式是否符合 accept
|
||||||
const isValidType = acceptTypes.some((type) => {
|
const isValidType = acceptTypes.some((type) => {
|
||||||
if (type === "image/*") {
|
if (type === "image/*") {
|
||||||
// 如果<EFBFBD>?image/*,检<EFBFBD>?MIME 类型是否<EFBFBD>?"image/" 开<EFBFBD>?
|
// 如果是 image/*,检查 MIME 类型是否以 "image/" 开头
|
||||||
return file.type.startsWith("image/");
|
return file.type.startsWith("image/");
|
||||||
} else if (type.startsWith(".")) {
|
} else if (type.startsWith(".")) {
|
||||||
// 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾
|
// 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾
|
||||||
return file.name.toLowerCase().endsWith(type);
|
return file.name.toLowerCase().endsWith(type);
|
||||||
} else {
|
} else {
|
||||||
// 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹<EFBFBD>?
|
// 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹配
|
||||||
return file.type === type;
|
return file.type === type;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import type { FileInfo } from "@/types/api";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/**
|
/**
|
||||||
* 请求携带的额外参<EFBFBD>?
|
* 请求携带的额外参数
|
||||||
*/
|
*/
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -54,7 +54,7 @@ const props = defineProps({
|
|||||||
default: "file",
|
default: "file",
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 最大文件大小(单位:M<EFBFBD>?
|
* 最大文件大小(单位:MB)
|
||||||
*/
|
*/
|
||||||
maxFileSize: {
|
maxFileSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -62,7 +62,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传图片格式,默认支持所有图<EFBFBD>?image/*),指定格式示例:'.png,.jpg,.jpeg,.gif,.bmp'
|
* 上传图片格式,默认支持所有图片 (image/*),指定格式示例:'.png,.jpg,.jpeg,.gif,.bmp'
|
||||||
*/
|
*/
|
||||||
accept: {
|
accept: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -70,7 +70,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义样式,用于设置组件的宽度和高度等其他样<EFBFBD>?
|
* 自定义样式,用于设置组件的宽度和高度等其他样式
|
||||||
*/
|
*/
|
||||||
style: {
|
style: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -92,19 +92,19 @@ const modelValue = defineModel("modelValue", {
|
|||||||
* 限制用户上传文件的格式和大小
|
* 限制用户上传文件的格式和大小
|
||||||
*/
|
*/
|
||||||
function handleBeforeUpload(file: UploadRawFile) {
|
function handleBeforeUpload(file: UploadRawFile) {
|
||||||
// 校验文件类型:虽<EFBFBD>?accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符<EFBFBD>?accept 的规<EFBFBD>?
|
// 校验文件类型:虽然 accept 属性限制了用户在文件选择器中可选的文件类型,但仍需在上传时再次校验文件实际类型,确保符合 accept 的规则
|
||||||
const acceptTypes = props.accept.split(",").map((type) => type.trim());
|
const acceptTypes = props.accept.split(",").map((type) => type.trim());
|
||||||
|
|
||||||
// 检查文件格式是否符<EFBFBD>?accept
|
// 检查文件格式是否符合 accept
|
||||||
const isValidType = acceptTypes.some((type) => {
|
const isValidType = acceptTypes.some((type) => {
|
||||||
if (type === "image/*") {
|
if (type === "image/*") {
|
||||||
// 如果<EFBFBD>?image/*,检<EFBFBD>?MIME 类型是否<EFBFBD>?"image/" 开<EFBFBD>?
|
// 如果是 image/*,检查 MIME 类型是否以 "image/" 开头
|
||||||
return file.type.startsWith("image/");
|
return file.type.startsWith("image/");
|
||||||
} else if (type.startsWith(".")) {
|
} else if (type.startsWith(".")) {
|
||||||
// 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾
|
// 如果是扩展名 (.png, .jpg),检查文件名是否以指定扩展名结尾
|
||||||
return file.name.toLowerCase().endsWith(type);
|
return file.name.toLowerCase().endsWith(type);
|
||||||
} else {
|
} else {
|
||||||
// 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹<EFBFBD>?
|
// 如果是具体的 MIME 类型 (image/png, image/jpeg),检查是否完全匹配
|
||||||
return file.type === type;
|
return file.type === type;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const STORAGE_KEYS = {
|
|||||||
SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`,
|
SHOW_TAGS_VIEW: `${APP_PREFIX}:ui:show_tags_view`,
|
||||||
SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`,
|
SHOW_APP_LOGO: `${APP_PREFIX}:ui:show_app_logo`,
|
||||||
SHOW_WATERMARK: `${APP_PREFIX}:ui:show_watermark`,
|
SHOW_WATERMARK: `${APP_PREFIX}:ui:show_watermark`,
|
||||||
|
PAGE_SWITCHING_ANIMATION: `${APP_PREFIX}:ui:page_switching_animation`,
|
||||||
ENABLE_AI_ASSISTANT: `${APP_PREFIX}:ui:enable_ai_assistant`,
|
ENABLE_AI_ASSISTANT: `${APP_PREFIX}:ui:enable_ai_assistant`,
|
||||||
LAYOUT: `${APP_PREFIX}:ui:layout`,
|
LAYOUT: `${APP_PREFIX}:ui:layout`,
|
||||||
SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:ui:sidebar_color_scheme`,
|
SIDEBAR_COLOR_SCHEME: `${APP_PREFIX}:ui:sidebar_color_scheme`,
|
||||||
|
|||||||
@@ -121,3 +121,31 @@ export const enum DeviceEnum {
|
|||||||
*/
|
*/
|
||||||
MOBILE = "mobile",
|
MOBILE = "mobile",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面切换动画枚举
|
||||||
|
*/
|
||||||
|
export const enum PageSwitchingAnimationEnum {
|
||||||
|
/**
|
||||||
|
* 无动画
|
||||||
|
*/
|
||||||
|
NONE = "none",
|
||||||
|
/**
|
||||||
|
* 淡入淡出
|
||||||
|
*/
|
||||||
|
FADE = "fade",
|
||||||
|
/**
|
||||||
|
* 平滑切换
|
||||||
|
*/
|
||||||
|
FADE_SLIDE = "fade-slide",
|
||||||
|
/**
|
||||||
|
* 缩放切换
|
||||||
|
*/
|
||||||
|
FADE_SCALE = "fade-scale",
|
||||||
|
}
|
||||||
|
export const PageSwitchingAnimationOptions: Record<string, OptionItem> = {
|
||||||
|
none: { value: "none", label: "无动画" },
|
||||||
|
fade: { value: "fade", label: "淡入淡出" },
|
||||||
|
"fade-slide": { value: "fade-slide", label: "平滑切换" },
|
||||||
|
"fade-scale": { value: "fade-scale", label: "缩放切换" },
|
||||||
|
};
|
||||||
|
|||||||
@@ -72,6 +72,11 @@
|
|||||||
"showAppLogo": "Show App Logo",
|
"showAppLogo": "Show App Logo",
|
||||||
"sidebarColorScheme": "Sidebar Color Scheme",
|
"sidebarColorScheme": "Sidebar Color Scheme",
|
||||||
"showWatermark": "Show Watermark",
|
"showWatermark": "Show Watermark",
|
||||||
|
"pageSwitchingAnimation": "Page Switching Animation",
|
||||||
|
"none": "None",
|
||||||
|
"fade": "Fade",
|
||||||
|
"fade-slide": "Fade Slide",
|
||||||
|
"fade-scale": "Fade Scale",
|
||||||
"classicBlue": "Classic Blue",
|
"classicBlue": "Classic Blue",
|
||||||
"minimalWhite": "Minimal White",
|
"minimalWhite": "Minimal White",
|
||||||
"copyConfig": "Copy Config",
|
"copyConfig": "Copy Config",
|
||||||
|
|||||||
@@ -75,6 +75,11 @@
|
|||||||
"showTagsView": "显示页签",
|
"showTagsView": "显示页签",
|
||||||
"showAppLogo": "显示Logo",
|
"showAppLogo": "显示Logo",
|
||||||
"showWatermark": "显示水印",
|
"showWatermark": "显示水印",
|
||||||
|
"pageSwitchingAnimation": "页面切换动画",
|
||||||
|
"none": "无动画",
|
||||||
|
"fade": "淡入淡出",
|
||||||
|
"fade-slide": "平滑切换",
|
||||||
|
"fade-scale": "缩放切换",
|
||||||
"classicBlue": "经典蓝",
|
"classicBlue": "经典蓝",
|
||||||
"minimalWhite": "极简白",
|
"minimalWhite": "极简白",
|
||||||
"copyConfig": "复制配置",
|
"copyConfig": "复制配置",
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ defineProps({
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
// 顶部布局和混合布局的特殊处<EFBFBD>?
|
// 顶部布局和混合布局的特殊处理
|
||||||
.layout-top,
|
.layout-top,
|
||||||
.layout-mix {
|
.layout-mix {
|
||||||
.logo {
|
.logo {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<section class="app-main" :style="{ height: appMainHeight }">
|
<section class="app-main" :style="{ height: appMainHeight }">
|
||||||
<router-view>
|
<router-view>
|
||||||
<template #default="{ Component, route }">
|
<template #default="{ Component, route }">
|
||||||
<transition enter-active-class="animate__animated animate__fadeIn" mode="out-in">
|
<transition :name="transitionName" mode="out-in">
|
||||||
<keep-alive :include="cachedViews">
|
<keep-alive :include="cachedViews">
|
||||||
<component :is="currentComponent(Component, route)" :key="route.fullPath" />
|
<component :is="currentComponent(Component, route)" :key="route.fullPath" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
@@ -25,6 +25,8 @@ import Error404 from "@/views/error/404.vue";
|
|||||||
|
|
||||||
const { cachedViews } = toRefs(useTagsViewStore());
|
const { cachedViews } = toRefs(useTagsViewStore());
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
// 当前组件
|
// 当前组件
|
||||||
const wrapperMap = new Map<string, Component>();
|
const wrapperMap = new Map<string, Component>();
|
||||||
const currentComponent = (component: Component, route: RouteLocationNormalized) => {
|
const currentComponent = (component: Component, route: RouteLocationNormalized) => {
|
||||||
@@ -60,12 +62,17 @@ const currentComponent = (component: Component, route: RouteLocationNormalized)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const appMainHeight = computed(() => {
|
const appMainHeight = computed(() => {
|
||||||
if (useSettingsStore().showTagsView) {
|
if (settingsStore.showTagsView) {
|
||||||
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
|
return `calc(100vh - ${variables["navbar-height"]} - ${variables["tags-view-height"]})`;
|
||||||
} else {
|
} else {
|
||||||
return `calc(100vh - ${variables["navbar-height"]})`;
|
return `calc(100vh - ${variables["navbar-height"]})`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 页面切换动画名称
|
||||||
|
const transitionName = computed(() => {
|
||||||
|
return settingsStore.pageSwitchingAnimation ?? "";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -74,18 +81,42 @@ const appMainHeight = computed(() => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: var(--el-bg-color-page);
|
background-color: var(--el-bg-color-page);
|
||||||
|
|
||||||
/* 布局切换动画优化 */
|
/* fade */
|
||||||
&.animate__animated {
|
.fade-enter-active,
|
||||||
animation-duration: 0.4s;
|
.fade-leave-active {
|
||||||
animation-fill-mode: forwards;
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animate__fadeOut {
|
/* fade-slide */
|
||||||
animation-timing-function: ease-in;
|
.fade-slide-leave-active,
|
||||||
|
.fade-slide-enter-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.fade-slide-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
.fade-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animate__fadeIn {
|
/* fade-scale */
|
||||||
animation-timing-function: ease-out;
|
.fade-scale-leave-active,
|
||||||
|
.fade-scale-enter-active {
|
||||||
|
transition: all 0.28s;
|
||||||
|
}
|
||||||
|
.fade-scale-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
.fade-scale-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -49,6 +49,18 @@
|
|||||||
<el-switch v-model="settingsStore.showWatermark" />
|
<el-switch v-model="settingsStore.showWatermark" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="config-item flex-x-between">
|
||||||
|
<span class="text-xs">{{ t("settings.pageSwitchingAnimation") }}</span>
|
||||||
|
<el-select v-model="settingsStore.pageSwitchingAnimation" style="width: 150px">
|
||||||
|
<el-option
|
||||||
|
v-for="(item, key) in pageSwitchingAnimationOptions"
|
||||||
|
:key
|
||||||
|
:label="t(`settings.${item.value}`)"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="config-item flex-x-between">
|
<div class="config-item flex-x-between">
|
||||||
<span class="text-xs">灰色模式</span>
|
<span class="text-xs">灰色模式</span>
|
||||||
<el-switch v-model="settingsStore.grayMode" />
|
<el-switch v-model="settingsStore.grayMode" />
|
||||||
@@ -159,10 +171,13 @@
|
|||||||
import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
|
import { DocumentCopy, RefreshLeft, Check } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
import { LayoutMode, SidebarColor, ThemeMode } from "@/enums";
|
import { LayoutMode, PageSwitchingAnimationOptions, SidebarColor, ThemeMode } from "@/enums";
|
||||||
import { useSettingsStore } from "@/store";
|
import { useSettingsStore } from "@/store";
|
||||||
import { themeColorPresets, appConfig } from "@/settings";
|
import { themeColorPresets, appConfig } from "@/settings";
|
||||||
|
|
||||||
|
// 页面切换动画选项
|
||||||
|
const pageSwitchingAnimationOptions: Record<string, OptionItem> = PageSwitchingAnimationOptions;
|
||||||
|
|
||||||
// 按钮图标
|
// 按钮图标
|
||||||
const copyIcon = markRaw(DocumentCopy);
|
const copyIcon = markRaw(DocumentCopy);
|
||||||
const resetIcon = markRaw(RefreshLeft);
|
const resetIcon = markRaw(RefreshLeft);
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const showTenantSelect = computed(() => {
|
|||||||
if (tenantStore.tenantList.length === 0) {
|
if (tenantStore.tenantList.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 如果只有一个租户,也不显示(单租户模式,用户无感知<EFBFBD>?
|
// 如果只有一个租户,也不显示(单租户模式,用户无感知)
|
||||||
if (tenantStore.tenantList.length === 1) {
|
if (tenantStore.tenantList.length === 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -128,20 +128,20 @@ function handleProfileClick() {
|
|||||||
router.push({ name: "Profile" });
|
router.push({ name: "Profile" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据主题和侧边栏配色方案选择样式<EFBFBD>?
|
// 根据主题和侧边栏配色方案选择样式类
|
||||||
const navbarActionsClass = computed(() => {
|
const navbarActionsClass = computed(() => {
|
||||||
const { theme, sidebarColorScheme, layout } = settingStore;
|
const { theme, sidebarColorScheme, layout } = settingStore;
|
||||||
|
|
||||||
// 暗黑主题下,所有布局都使用白色文<EFBFBD>?
|
// 暗黑主题下,所有布局都使用白色文字
|
||||||
if (theme === ThemeMode.DARK) {
|
if (theme === ThemeMode.DARK) {
|
||||||
return "navbar-actions--white-text";
|
return "navbar-actions--white-text";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 明亮主题<EFBFBD>?
|
// 明亮主题下
|
||||||
if (theme === ThemeMode.LIGHT) {
|
if (theme === ThemeMode.LIGHT) {
|
||||||
// 顶部布局和混合布局的顶部区域:
|
// 顶部布局和混合布局的顶部区域:
|
||||||
// - 如果侧边栏是经典蓝色,使用白色文<EFBFBD>?
|
// - 如果侧边栏是经典蓝色,使用白色文字
|
||||||
// - 如果侧边栏是极简白色,使用深色文<EFBFBD>?
|
// - 如果侧边栏是极简白色,使用深色文字
|
||||||
if (layout === LayoutMode.TOP || layout === LayoutMode.MIX) {
|
if (layout === LayoutMode.TOP || layout === LayoutMode.MIX) {
|
||||||
if (sidebarColorScheme === SidebarColor.CLASSIC_BLUE) {
|
if (sidebarColorScheme === SidebarColor.CLASSIC_BLUE) {
|
||||||
return "navbar-actions--white-text";
|
return "navbar-actions--white-text";
|
||||||
@@ -155,7 +155,7 @@ const navbarActionsClass = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登<EFBFBD>?
|
* 退出登录
|
||||||
*/
|
*/
|
||||||
function logout() {
|
function logout() {
|
||||||
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
|
||||||
@@ -189,14 +189,14 @@ function handleSettingsClick() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 44px; /* 增加最小点击区域到44px,符合人机交互标<EFBFBD>?*/
|
min-width: 44px; /* 增加最小点击区域到44px,符合人机交互标准 */
|
||||||
height: 44px;
|
height: 44px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
// 确保子元素居<EFBFBD>?
|
// 确保子元素居中
|
||||||
> * {
|
> * {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -262,7 +262,7 @@ function handleSettingsClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局<EFBFBD>?
|
// 白色文字样式(用于深色背景:暗黑主题、顶部布局、混合布局等)
|
||||||
.navbar-actions--white-text {
|
.navbar-actions--white-text {
|
||||||
.navbar-actions__item {
|
.navbar-actions__item {
|
||||||
:deep([class^="i-svg:"]) {
|
:deep([class^="i-svg:"]) {
|
||||||
@@ -298,7 +298,7 @@ function handleSettingsClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 深色文字样式(用于浅色背景:明亮主题下的左侧布局<EFBFBD>?
|
// 深色文字样式(用于浅色背景:明亮主题下的左侧布局等)
|
||||||
.navbar-actions--dark-text {
|
.navbar-actions--dark-text {
|
||||||
.navbar-actions__item {
|
.navbar-actions__item {
|
||||||
:deep([class^="i-svg:"]) {
|
:deep([class^="i-svg:"]) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const defaults = {
|
|||||||
showTagsView: true,
|
showTagsView: true,
|
||||||
showAppLogo: true,
|
showAppLogo: true,
|
||||||
showWatermark: false,
|
showWatermark: false,
|
||||||
|
pageSwitchingAnimation: "fade-slide",
|
||||||
showSettings: true,
|
showSettings: true,
|
||||||
watermarkContent: pkg.name,
|
watermarkContent: pkg.name,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export const useSettingsStore = defineStore("setting", () => {
|
|||||||
const showTagsView = useStorage(STORAGE_KEYS.SHOW_TAGS_VIEW, defaults.showTagsView);
|
const showTagsView = useStorage(STORAGE_KEYS.SHOW_TAGS_VIEW, defaults.showTagsView);
|
||||||
const showAppLogo = useStorage(STORAGE_KEYS.SHOW_APP_LOGO, defaults.showAppLogo);
|
const showAppLogo = useStorage(STORAGE_KEYS.SHOW_APP_LOGO, defaults.showAppLogo);
|
||||||
const showWatermark = useStorage(STORAGE_KEYS.SHOW_WATERMARK, defaults.showWatermark);
|
const showWatermark = useStorage(STORAGE_KEYS.SHOW_WATERMARK, defaults.showWatermark);
|
||||||
|
const pageSwitchingAnimation = useStorage(
|
||||||
|
STORAGE_KEYS.PAGE_SWITCHING_ANIMATION,
|
||||||
|
defaults.pageSwitchingAnimation
|
||||||
|
);
|
||||||
|
|
||||||
// 布局
|
// 布局
|
||||||
const layout = useStorage<LayoutMode>(STORAGE_KEYS.LAYOUT, defaults.layout as LayoutMode);
|
const layout = useStorage<LayoutMode>(STORAGE_KEYS.LAYOUT, defaults.layout as LayoutMode);
|
||||||
@@ -66,6 +70,7 @@ export const useSettingsStore = defineStore("setting", () => {
|
|||||||
showTagsView.value = defaults.showTagsView;
|
showTagsView.value = defaults.showTagsView;
|
||||||
showAppLogo.value = defaults.showAppLogo;
|
showAppLogo.value = defaults.showAppLogo;
|
||||||
showWatermark.value = defaults.showWatermark;
|
showWatermark.value = defaults.showWatermark;
|
||||||
|
pageSwitchingAnimation.value = defaults.pageSwitchingAnimation;
|
||||||
userEnableAi.value = false;
|
userEnableAi.value = false;
|
||||||
grayMode.value = false;
|
grayMode.value = false;
|
||||||
colorWeak.value = false;
|
colorWeak.value = false;
|
||||||
@@ -80,6 +85,7 @@ export const useSettingsStore = defineStore("setting", () => {
|
|||||||
showTagsView,
|
showTagsView,
|
||||||
showAppLogo,
|
showAppLogo,
|
||||||
showWatermark,
|
showWatermark,
|
||||||
|
pageSwitchingAnimation,
|
||||||
enableAiAssistant,
|
enableAiAssistant,
|
||||||
userEnableAi,
|
userEnableAi,
|
||||||
grayMode,
|
grayMode,
|
||||||
|
|||||||
@@ -108,20 +108,12 @@ export interface UserProfileDetail {
|
|||||||
|
|
||||||
/** 个人中心用户信息表单 */
|
/** 个人中心用户信息表单 */
|
||||||
export interface UserProfileForm {
|
export interface UserProfileForm {
|
||||||
/** 用户ID */
|
|
||||||
id?: string;
|
|
||||||
/** 用户名 */
|
|
||||||
username?: string;
|
|
||||||
/** 用户昵称 */
|
/** 用户昵称 */
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
/** 头像URL */
|
/** 头像URL */
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
/** 性别 */
|
/** 性别 */
|
||||||
gender?: number;
|
gender?: number;
|
||||||
/** 手机号 */
|
|
||||||
mobile?: string;
|
|
||||||
/** 邮箱 */
|
|
||||||
email?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改密码表单 */
|
/** 修改密码表单 */
|
||||||
@@ -134,12 +126,20 @@ export interface PasswordChangeForm {
|
|||||||
confirmPassword?: string;
|
confirmPassword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 密码校验表单 */
|
||||||
|
export interface PasswordVerifyForm {
|
||||||
|
/** 当前密码 */
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 修改手机表单 */
|
/** 修改手机表单 */
|
||||||
export interface MobileUpdateForm {
|
export interface MobileUpdateForm {
|
||||||
/** 手机号 */
|
/** 手机号 */
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
/** 验证码 */
|
/** 验证码 */
|
||||||
code?: string;
|
code?: string;
|
||||||
|
/** 当前密码 */
|
||||||
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 修改邮箱表单 */
|
/** 修改邮箱表单 */
|
||||||
@@ -148,4 +148,6 @@ export interface EmailUpdateForm {
|
|||||||
email?: string;
|
email?: string;
|
||||||
/** 验证码 */
|
/** 验证码 */
|
||||||
code?: string;
|
code?: string;
|
||||||
|
/** 当前密码 */
|
||||||
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ let notificationInstance: ReturnType<typeof ElNotification> | null = null;
|
|||||||
|
|
||||||
const showVoteNotification = () => {
|
const showVoteNotification = () => {
|
||||||
notificationInstance = ElNotification({
|
notificationInstance = ElNotification({
|
||||||
title: "Gitee 2025 开源评选 · 诚邀支持",
|
title: "2025 CSDN 博客之星评选,诚邀支持!",
|
||||||
message: `我正在参与 Gitee 2025 最受欢迎开源软件投票活动,欢迎支持!<br/><a href="https://gitee.com/activity/2025opensource?ident=I6VXEH" target="_blank" style="color: var(--el-color-primary); text-decoration: none; font-weight: 500;">点击投票</a>`,
|
message: `文章整理了 youlai 全栈开源矩阵,并同步了 2026 路线图与年度计划。欢迎<strong> 点赞 / 收藏 </strong>支持~<br/><a href="https://blog.csdn.net/u013737132/article/details/156545189" target="_blank" rel="noopener noreferrer" style="color: var(--el-color-primary); text-decoration: none; font-weight: 500;">点击查看评选文章</a>`,
|
||||||
type: "success",
|
type: "success",
|
||||||
position: "bottom-left",
|
position: "bottom-left",
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@@ -200,7 +200,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 应用内暗黑主题下顶部设置面板的深色样<EFBFBD>?*/
|
/* 应用内暗黑主题下顶部设置面板的深色样式 */
|
||||||
.dark .auth-view__toolbar {
|
.dark .auth-view__toolbar {
|
||||||
background-color: rgba(24, 28, 43, 0.9);
|
background-color: rgba(24, 28, 43, 0.9);
|
||||||
border-color: rgba(64, 128, 255, 0.35);
|
border-color: rgba(64, 128, 255, 0.35);
|
||||||
|
|||||||
@@ -69,41 +69,9 @@
|
|||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="手机号码">
|
<el-descriptions-item label="手机号码">
|
||||||
{{ userProfile.mobile || "未绑定" }}
|
{{ userProfile.mobile || "未绑定" }}
|
||||||
<el-button
|
|
||||||
v-if="userProfile.mobile"
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
@click="() => handleOpenDialog(DialogType.MOBILE)"
|
|
||||||
>
|
|
||||||
更换
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
@click="() => handleOpenDialog(DialogType.MOBILE)"
|
|
||||||
>
|
|
||||||
绑定
|
|
||||||
</el-button>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱">
|
<el-descriptions-item label="邮箱">
|
||||||
{{ userProfile.email || "未绑定" }}
|
{{ userProfile.email || "未绑定" }}
|
||||||
<el-button
|
|
||||||
v-if="userProfile.email"
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
@click="() => handleOpenDialog(DialogType.EMAIL)"
|
|
||||||
>
|
|
||||||
更换
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-else
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
@click="() => handleOpenDialog(DialogType.EMAIL)"
|
|
||||||
>
|
|
||||||
绑定
|
|
||||||
</el-button>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="部门">
|
<el-descriptions-item label="部门">
|
||||||
{{ userProfile.deptName }}
|
{{ userProfile.deptName }}
|
||||||
@@ -129,6 +97,66 @@
|
|||||||
修改
|
修改
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="security-item">
|
||||||
|
<div class="security-info">
|
||||||
|
<div class="security-title">手机号</div>
|
||||||
|
<div class="security-desc">
|
||||||
|
{{ mobileSecurityDesc }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-button
|
||||||
|
v-if="userProfile.mobile"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="() => handleOpenDialog(DialogType.MOBILE)"
|
||||||
|
>
|
||||||
|
更换
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="userProfile.mobile" type="danger" link @click="handleUnbindMobile">
|
||||||
|
解绑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="() => handleOpenDialog(DialogType.MOBILE)"
|
||||||
|
>
|
||||||
|
绑定
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="security-item">
|
||||||
|
<div class="security-info">
|
||||||
|
<div class="security-title">邮箱</div>
|
||||||
|
<div class="security-desc">
|
||||||
|
{{ emailSecurityDesc }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<el-button
|
||||||
|
v-if="userProfile.email"
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="() => handleOpenDialog(DialogType.EMAIL)"
|
||||||
|
>
|
||||||
|
更换
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="userProfile.email" type="danger" link @click="handleUnbindEmail">
|
||||||
|
解绑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
@click="() => handleOpenDialog(DialogType.EMAIL)"
|
||||||
|
>
|
||||||
|
绑定
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@@ -189,6 +217,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="当前密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="mobileUpdateForm.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
style="width: 250px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<!-- 绑定邮箱 -->
|
<!-- 绑定邮箱 -->
|
||||||
@@ -211,6 +247,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="当前密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="emailUpdateForm.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
style="width: 250px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -233,9 +277,10 @@ import type {
|
|||||||
UserProfileForm,
|
UserProfileForm,
|
||||||
} from "@/types/api";
|
} from "@/types/api";
|
||||||
|
|
||||||
import { ref, reactive } from "vue";
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||||
import FileAPI from "@/api/file";
|
import FileAPI from "@/api/file";
|
||||||
import { useUserStoreHook } from "@/store";
|
import { useUserStoreHook } from "@/store";
|
||||||
|
import { redirectToLogin } from "@/utils/auth";
|
||||||
|
|
||||||
import { Camera } from "@element-plus/icons-vue";
|
import { Camera } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
@@ -275,7 +320,19 @@ const emailTimer = ref();
|
|||||||
const passwordChangeRules = {
|
const passwordChangeRules = {
|
||||||
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
|
oldPassword: [{ required: true, message: "请输入原密码", trigger: "blur" }],
|
||||||
newPassword: [{ required: true, message: "请输入新密码", trigger: "blur" }],
|
newPassword: [{ required: true, message: "请输入新密码", trigger: "blur" }],
|
||||||
confirmPassword: [{ required: true, message: "请再次输入新密码", trigger: "blur" }],
|
confirmPassword: [
|
||||||
|
{ required: true, message: "请再次输入新密码", trigger: "blur" },
|
||||||
|
{
|
||||||
|
validator: (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||||
|
if (value !== passwordChangeForm.newPassword) {
|
||||||
|
callback(new Error("两次输入的密码不一致"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 手机号校验规则
|
// 手机号校验规则
|
||||||
@@ -289,6 +346,7 @@ const mobileBindingRules = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
|
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
|
||||||
|
password: [{ required: true, message: "请输入当前密码", trigger: "blur" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 邮箱校验规则
|
// 邮箱校验规则
|
||||||
@@ -302,8 +360,32 @@ const emailBindingRules = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
|
code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
|
||||||
|
password: [{ required: true, message: "请输入当前密码", trigger: "blur" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function maskMobile(mobile?: string) {
|
||||||
|
if (!mobile) return "";
|
||||||
|
return mobile.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskEmail(email?: string) {
|
||||||
|
if (!email) return "";
|
||||||
|
const [name, domain] = email.split("@");
|
||||||
|
if (!domain) return email;
|
||||||
|
if (name.length <= 2) return `${name[0] || ""}***@${domain}`;
|
||||||
|
return `${name.slice(0, 2)}***@${domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mobileSecurityDesc = computed(() => {
|
||||||
|
return userProfile.value.mobile
|
||||||
|
? `已绑定:${maskMobile(userProfile.value.mobile)}`
|
||||||
|
: "未绑定手机号";
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailSecurityDesc = computed(() => {
|
||||||
|
return userProfile.value.email ? `已绑定:${maskEmail(userProfile.value.email)}` : "未绑定邮箱";
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开弹窗
|
* 打开弹窗
|
||||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||||
@@ -315,22 +397,66 @@ const handleOpenDialog = (type: DialogType) => {
|
|||||||
case DialogType.ACCOUNT:
|
case DialogType.ACCOUNT:
|
||||||
dialog.title = "账号资料";
|
dialog.title = "账号资料";
|
||||||
// 初始化表单数据
|
// 初始化表单数据
|
||||||
userProfileForm.id = userProfile.value.id;
|
|
||||||
userProfileForm.nickname = userProfile.value.nickname;
|
userProfileForm.nickname = userProfile.value.nickname;
|
||||||
|
userProfileForm.avatar = userProfile.value.avatar;
|
||||||
userProfileForm.gender = userProfile.value.gender;
|
userProfileForm.gender = userProfile.value.gender;
|
||||||
break;
|
break;
|
||||||
case DialogType.PASSWORD:
|
case DialogType.PASSWORD:
|
||||||
dialog.title = "修改密码";
|
dialog.title = "修改密码";
|
||||||
break;
|
break;
|
||||||
case DialogType.MOBILE:
|
case DialogType.MOBILE:
|
||||||
dialog.title = "绑定手机";
|
dialog.title = userProfile.value.mobile ? "更换手机号" : "绑定手机号";
|
||||||
|
mobileUpdateForm.mobile = "";
|
||||||
|
mobileUpdateForm.code = "";
|
||||||
|
mobileUpdateForm.password = "";
|
||||||
break;
|
break;
|
||||||
case DialogType.EMAIL:
|
case DialogType.EMAIL:
|
||||||
dialog.title = "绑定邮箱";
|
dialog.title = userProfile.value.email ? "更换邮箱" : "绑定邮箱";
|
||||||
|
emailUpdateForm.email = "";
|
||||||
|
emailUpdateForm.code = "";
|
||||||
|
emailUpdateForm.password = "";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleUnbindMobile() {
|
||||||
|
if (!userProfile.value.mobile) return;
|
||||||
|
try {
|
||||||
|
const { value } = await ElMessageBox.prompt("请输入当前密码以解绑手机号", "解绑手机号", {
|
||||||
|
type: "warning",
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
inputType: "password",
|
||||||
|
inputPlaceholder: "当前密码",
|
||||||
|
inputValidator: (val) => !!val || "请输入当前密码",
|
||||||
|
});
|
||||||
|
await UserAPI.unbindMobile({ password: value });
|
||||||
|
ElMessage.success("手机号解绑成功");
|
||||||
|
await loadUserProfile();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUnbindEmail() {
|
||||||
|
if (!userProfile.value.email) return;
|
||||||
|
try {
|
||||||
|
const { value } = await ElMessageBox.prompt("请输入当前密码以解绑邮箱", "解绑邮箱", {
|
||||||
|
type: "warning",
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
inputType: "password",
|
||||||
|
inputPlaceholder: "当前密码",
|
||||||
|
inputValidator: (val) => !!val || "请输入当前密码",
|
||||||
|
});
|
||||||
|
await UserAPI.unbindEmail({ password: value });
|
||||||
|
ElMessage.success("邮箱解绑成功");
|
||||||
|
await loadUserProfile();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送手机验证码
|
* 发送手机验证码
|
||||||
*/
|
*/
|
||||||
@@ -395,33 +521,41 @@ function handleSendEmailCode() {
|
|||||||
* 提交表单
|
* 提交表单
|
||||||
*/
|
*/
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (dialog.type === DialogType.ACCOUNT) {
|
try {
|
||||||
UserAPI.updateProfile(userProfileForm).then(() => {
|
if (dialog.type === DialogType.ACCOUNT) {
|
||||||
|
const valid = await userProfileFormRef.value?.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
await UserAPI.updateProfile(userProfileForm);
|
||||||
ElMessage.success("账号资料修改成功");
|
ElMessage.success("账号资料修改成功");
|
||||||
dialog.visible = false;
|
dialog.visible = false;
|
||||||
loadUserProfile();
|
await loadUserProfile();
|
||||||
});
|
} else if (dialog.type === DialogType.PASSWORD) {
|
||||||
} else if (dialog.type === DialogType.PASSWORD) {
|
const valid = await passwordChangeFormRef.value?.validate();
|
||||||
if (passwordChangeForm.newPassword !== passwordChangeForm.confirmPassword) {
|
if (!valid) return;
|
||||||
ElMessage.error("两次输入的密码不一致");
|
|
||||||
return;
|
await UserAPI.changePassword(passwordChangeForm);
|
||||||
|
dialog.visible = false;
|
||||||
|
await redirectToLogin("密码已修改,请重新登录");
|
||||||
|
} else if (dialog.type === DialogType.MOBILE) {
|
||||||
|
const valid = await mobileBindingFormRef.value?.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
await UserAPI.bindOrChangeMobile(mobileUpdateForm);
|
||||||
|
ElMessage.success(userProfile.value.mobile ? "手机号更换成功" : "手机号绑定成功");
|
||||||
|
dialog.visible = false;
|
||||||
|
await loadUserProfile();
|
||||||
|
} else if (dialog.type === DialogType.EMAIL) {
|
||||||
|
const valid = await emailBindingFormRef.value?.validate();
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
await UserAPI.bindOrChangeEmail(emailUpdateForm);
|
||||||
|
ElMessage.success(userProfile.value.email ? "邮箱更换成功" : "邮箱绑定成功");
|
||||||
|
dialog.visible = false;
|
||||||
|
await loadUserProfile();
|
||||||
}
|
}
|
||||||
UserAPI.changePassword(passwordChangeForm).then(() => {
|
} catch {
|
||||||
ElMessage.success("密码修改成功");
|
// ignore
|
||||||
dialog.visible = false;
|
|
||||||
});
|
|
||||||
} else if (dialog.type === DialogType.MOBILE) {
|
|
||||||
UserAPI.bindOrChangeMobile(mobileUpdateForm).then(() => {
|
|
||||||
ElMessage.success("手机号绑定成功");
|
|
||||||
dialog.visible = false;
|
|
||||||
loadUserProfile();
|
|
||||||
});
|
|
||||||
} else if (dialog.type === DialogType.EMAIL) {
|
|
||||||
UserAPI.bindOrChangeEmail(emailUpdateForm).then(() => {
|
|
||||||
ElMessage.success("邮箱绑定成功");
|
|
||||||
dialog.visible = false;
|
|
||||||
loadUserProfile();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -482,6 +616,15 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
await loadUserProfile();
|
await loadUserProfile();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (mobileTimer.value) {
|
||||||
|
clearInterval(mobileTimer.value);
|
||||||
|
}
|
||||||
|
if (emailTimer.value) {
|
||||||
|
clearInterval(emailTimer.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "Dict",
|
name: "Dict",
|
||||||
inherititems: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
import { ref, reactive } from "vue";
|
import { ref, reactive } from "vue";
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ global.ResizeObserver = class ResizeObserver {
|
|||||||
// Mock Element.scrollIntoView
|
// Mock Element.scrollIntoView
|
||||||
Element.prototype.scrollIntoView = vi.fn();
|
Element.prototype.scrollIntoView = vi.fn();
|
||||||
|
|
||||||
|
// Mock __APP_INFO__
|
||||||
|
(globalThis as any).__APP_INFO__ = {
|
||||||
|
pkg: {
|
||||||
|
name: "vue3-element-admin",
|
||||||
|
version: "4.0.0",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Mock console methods to reduce noise in tests
|
// Mock console methods to reduce noise in tests
|
||||||
global.console = {
|
global.console = {
|
||||||
...console,
|
...console,
|
||||||
|
|||||||
@@ -160,6 +160,38 @@ describe("useSettingsStore", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("页面切换动画", () => {
|
||||||
|
it("应该修改页面切换动画", () => {
|
||||||
|
const store = useSettingsStore();
|
||||||
|
|
||||||
|
store.pageSwitchingAnimation = "fade";
|
||||||
|
expect(store.pageSwitchingAnimation).toBe("fade");
|
||||||
|
|
||||||
|
store.pageSwitchingAnimation = "fade-slide";
|
||||||
|
expect(store.pageSwitchingAnimation).toBe("fade-slide");
|
||||||
|
|
||||||
|
store.pageSwitchingAnimation = "fade-scale";
|
||||||
|
expect(store.pageSwitchingAnimation).toBe("fade-scale");
|
||||||
|
|
||||||
|
store.pageSwitchingAnimation = "none";
|
||||||
|
expect(store.pageSwitchingAnimation).toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("应该持久化页面切换动画设置", () => {
|
||||||
|
const store = useSettingsStore();
|
||||||
|
store.pageSwitchingAnimation = "fade-scale";
|
||||||
|
|
||||||
|
const newStore = useSettingsStore();
|
||||||
|
expect(newStore.pageSwitchingAnimation).toBe("fade-scale");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("应该使用默认的页面切换动画", () => {
|
||||||
|
const store = useSettingsStore();
|
||||||
|
// 默认值应该是 "fade-slide"
|
||||||
|
expect(store.pageSwitchingAnimation).toBe("fade-slide");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("重置设置", () => {
|
describe("重置设置", () => {
|
||||||
it("应该重置所有设置为默认值", () => {
|
it("应该重置所有设置为默认值", () => {
|
||||||
const store = useSettingsStore();
|
const store = useSettingsStore();
|
||||||
@@ -168,6 +200,7 @@ describe("useSettingsStore", () => {
|
|||||||
store.showTagsView = false;
|
store.showTagsView = false;
|
||||||
store.showAppLogo = false;
|
store.showAppLogo = false;
|
||||||
store.showWatermark = false;
|
store.showWatermark = false;
|
||||||
|
store.pageSwitchingAnimation = "fade-slide";
|
||||||
store.userEnableAi = true;
|
store.userEnableAi = true;
|
||||||
store.grayMode = true;
|
store.grayMode = true;
|
||||||
store.colorWeak = true;
|
store.colorWeak = true;
|
||||||
@@ -181,6 +214,7 @@ describe("useSettingsStore", () => {
|
|||||||
expect(store.userEnableAi).toBe(false);
|
expect(store.userEnableAi).toBe(false);
|
||||||
expect(store.grayMode).toBe(false);
|
expect(store.grayMode).toBe(false);
|
||||||
expect(store.colorWeak).toBe(false);
|
expect(store.colorWeak).toBe(false);
|
||||||
|
expect(store.pageSwitchingAnimation).toBe("fade-slide");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
|
import Components from "unplugin-vue-components/vite";
|
||||||
|
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
// API 自动导入
|
||||||
|
AutoImport({
|
||||||
|
imports: ["vue", "@vueuse/core", "pinia", "vue-router", "vue-i18n"],
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
|
// 组件自动导入
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
dts: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
test: {
|
test: {
|
||||||
// 使用 happy-dom 作为测试环境(比 jsdom 快)
|
// 使用 happy-dom 作为测试环境(比 jsdom 快)
|
||||||
environment: "happy-dom",
|
environment: "happy-dom",
|
||||||
|
|||||||
Reference in New Issue
Block a user