refactor: ♻️ 字典调整按需加载,api、store和枚举文件命名优化

This commit is contained in:
Ray.Hao
2025-03-24 08:17:31 +08:00
parent 6204deb7cb
commit 3c9cf67961
84 changed files with 989 additions and 1108 deletions

View File

@@ -15,18 +15,18 @@
<script setup lang="ts">
import { useAppStore, useSettingsStore } from "@/store";
import defaultSettings from "@/settings";
import { ThemeEnum } from "@/enums/ThemeEnum";
import { SizeEnum } from "@/enums/SizeEnum";
import { ThemeMode } from "@/enums/settings/theme.enum";
import { ComponentSize } from "@/enums/settings/layout.enum";
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const locale = computed(() => appStore.locale);
const size = computed(() => appStore.size as SizeEnum);
const size = computed(() => appStore.size as ComponentSize);
const watermarkEnabled = computed(() => settingsStore.watermarkEnabled);
// 明亮/暗黑主题水印字体颜色适配
const fontColor = computed(() => {
return settingsStore.theme === ThemeEnum.DARK ? "rgba(255, 255, 255, .15)" : "rgba(0, 0, 0, .15)";
return settingsStore.theme === ThemeMode.DARK ? "rgba(255, 255, 255, .15)" : "rgba(0, 0, 0, .15)";
});
</script>

View File

@@ -3,7 +3,7 @@ import request from "@/utils/request";
const CONFIG_BASE_URL = "/api/v1/config";
const ConfigAPI = {
/** 获取系统配置分页数据 */
/** 系统配置分页 */
getPage(queryParams?: ConfigPageQuery) {
return request<any, PageResult<ConfigPageVO[]>>({
url: `${CONFIG_BASE_URL}/page`,
@@ -11,12 +11,8 @@ const ConfigAPI = {
params: queryParams,
});
},
/**
*
*
* @param id ConfigID
* @returns Config表单数据
*/
/** 系统配置表单数据 */
getFormData(id: number) {
return request<any, ConfigForm>({
url: `${CONFIG_BASE_URL}/${id}/form`,
@@ -24,8 +20,8 @@ const ConfigAPI = {
});
},
/** 添加系统配置*/
add(data: ConfigForm) {
/** 新增系统配置 */
create(data: ConfigForm) {
return request({
url: `${CONFIG_BASE_URL}`,
method: "post",
@@ -33,12 +29,7 @@ const ConfigAPI = {
});
},
/**
*
*
* @param id ConfigID
* @param data Config表单数据
*/
/** 更新系统配置 */
update(id: number, data: ConfigForm) {
return request({
url: `${CONFIG_BASE_URL}/${id}`,

View File

@@ -44,7 +44,7 @@ const DeptAPI = {
* @param data
* @returns
*/
add(data: DeptForm) {
create(data: DeptForm) {
return request({
url: `${DEPT_BASE_URL}`,
method: "post",

View File

@@ -1,162 +0,0 @@
import request from "@/utils/request";
const DICT_DATA_BASE_URL = "/api/v1/dict-data";
const DictDataAPI = {
/**
* 获取字典分页列表
*
* @param queryParams 查询参数
* @returns 字典分页结果
*/
getPage(queryParams: DictDataPageQuery) {
return request<any, PageResult<DictDataPageVO[]>>({
url: `${DICT_DATA_BASE_URL}/page`,
method: "get",
params: queryParams,
});
},
/**
* 获取字典数据表单
*
* @param id 字典ID
* @returns 字典数据表单
*/
getFormData(id: number) {
return request<any, ResponseData<DictDataForm>>({
url: `${DICT_DATA_BASE_URL}/${id}/form`,
method: "get",
});
},
/**
* 新增字典数据
*
* @param data 字典数据
*/
add(data: DictDataForm) {
return request({
url: `${DICT_DATA_BASE_URL}`,
method: "post",
data: data,
});
},
/**
* 修改字典数据
*
* @param id 字典ID
* @param data 字典数据
*/
update(id: number, data: DictDataForm) {
return request({
url: `${DICT_DATA_BASE_URL}/${id}`,
method: "put",
data: data,
});
},
/**
* 删除字典
*
* @param ids 字典ID多个以英文逗号(,)分隔
*/
deleteByIds(ids: string) {
return request({
url: `${DICT_DATA_BASE_URL}/${ids}`,
method: "delete",
});
},
/**
* 获取字典的数据项
*
* @param dictCode 字典编码
* @returns 字典数据项
*/
getOptions(dictCode: string) {
return request<any, OptionType[]>({
url: `${DICT_DATA_BASE_URL}/${dictCode}/options`,
method: "get",
});
},
};
export default DictDataAPI;
/**
* 字典查询参数
*/
export interface DictDataPageQuery extends PageQuery {
/** 关键字(字典数据值/标签) */
keywords?: string;
/** 字典编码 */
dictCode?: string;
}
/**
* 字典分页对象
*/
export interface DictDataPageVO {
/**
* 字典ID
*/
id: number;
/**
* 字典编码
*/
dictCode: string;
/**
* 字典数据值
*/
value: string;
/**
* 字典数据标签
*/
label: string;
/**
* 状态1:启用0:禁用)
*/
status: number;
/**
* 字典排序
*/
sort?: number;
}
/**
* 字典
*/
export interface DictDataForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典编码
*/
dictCode?: string;
/**
* 字典数据值
*/
value?: string;
/**
* 字典数据标签
*/
label?: string;
/**
* 状态1:启用0:禁用)
*/
status?: number;
/**
* 字典排序
*/
sort?: number;
/**
* 标签类型
*/
tagType?: "success" | "warning" | "info" | "primary" | "danger" | undefined;
}

302
src/api/system/dict.api.ts Normal file
View File

@@ -0,0 +1,302 @@
import request from "@/utils/request";
const DICT_BASE_URL = "/api/v1/dicts";
const DictAPI = {
//---------------------------------------------------
// 字典相关接口
//---------------------------------------------------
/**
* 字典分页列表
*
* @param queryParams 查询参数
* @returns 字典分页结果
*/
getPage(queryParams: DictPageQuery) {
return request<any, PageResult<DictPageVO[]>>({
url: `${DICT_BASE_URL}/page`,
method: "get",
params: queryParams,
});
},
/**
* 字典表单数据
*
* @param id 字典ID
* @returns 字典表单数据
*/
getFormData(id: number) {
return request<any, ResponseData<DictForm>>({
url: `${DICT_BASE_URL}/${id}/form`,
method: "get",
});
},
/**
* 新增字典
*
* @param data 字典表单数据
*/
create(data: DictForm) {
return request({
url: `${DICT_BASE_URL}`,
method: "post",
data: data,
});
},
/**
* 修改字典
*
* @param id 字典ID
* @param data 字典表单数据
*/
update(id: number, data: DictForm) {
return request({
url: `${DICT_BASE_URL}/${id}`,
method: "put",
data: data,
});
},
/**
* 删除字典
*
* @param ids 字典ID多个以英文逗号(,)分隔
*/
deleteByIds(ids: string) {
return request({
url: `${DICT_BASE_URL}/${ids}`,
method: "delete",
});
},
//---------------------------------------------------
// 字典项相关接口
//---------------------------------------------------
/**
* 获取字典分页列表
*
* @param queryParams 查询参数
* @returns 字典分页结果
*/
getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) {
return request<any, PageResult<DictItemPageVO[]>>({
url: `${DICT_BASE_URL}/${dictCode}/items/page`,
method: "get",
params: queryParams,
});
},
/**
* 获取字典项列表
*/
getDictItems(dictCode: string) {
return request<any, DictItemOption[]>({
url: `${DICT_BASE_URL}/${dictCode}/items`,
method: "get",
});
},
/**
* 新增字典项
*/
createDictItem(dictCode: string, data: DictItemForm) {
return request({
url: `${DICT_BASE_URL}/${dictCode}/items`,
method: "post",
data: data,
});
},
/**
* 获取字典项表单数据
*
* @param id 字典项ID
* @returns 字典项表单数据
*/
getDictItemFormData(dictCode: string, id: number) {
return request<any, ResponseData<DictItemForm>>({
url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`,
method: "get",
});
},
/**
* 修改字典项
*/
updateDictItem(dictCode: string, id: number, data: DictItemForm) {
return request({
url: `${DICT_BASE_URL}/${dictCode}/items/${id}`,
method: "put",
data: data,
});
},
/**
* 删除字典项
*/
deleteDictItems(dictCode: string, ids: string) {
return request({
url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`,
method: "delete",
});
},
};
export default DictAPI;
/**
* 字典查询参数
*/
export interface DictPageQuery extends PageQuery {
/**
* 关键字(字典名称/编码)
*/
keywords?: string;
/**
* 字典状态1:启用0:禁用)
*/
status?: number;
}
/**
* 字典分页对象
*/
export interface DictPageVO {
/**
* 字典ID
*/
id: number;
/**
* 字典名称
*/
name: string;
/**
* 字典编码
*/
dictCode: string;
/**
* 字典状态1:启用0:禁用)
*/
status: number;
}
/**
* 字典
*/
export interface DictForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典名称
*/
name?: string;
/**
* 字典编码
*/
dictCode?: string;
/**
* 字典状态1-启用0-禁用)
*/
status?: number;
/**
* 备注
*/
remark?: string;
}
/**
* 字典查询参数
*/
export interface DictItemPageQuery extends PageQuery {
/** 关键字(字典数据值/标签) */
keywords?: string;
/** 字典编码 */
dictCode?: string;
}
/**
* 字典分页对象
*/
export interface DictItemPageVO {
/**
* 字典ID
*/
id: number;
/**
* 字典编码
*/
dictCode: string;
/**
* 字典数据值
*/
value: string;
/**
* 字典数据标签
*/
label: string;
/**
* 状态1:启用0:禁用)
*/
status: number;
/**
* 字典排序
*/
sort?: number;
}
/**
* 字典
*/
export interface DictItemForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典编码
*/
dictCode?: string;
/**
* 字典数据值
*/
value?: string;
/**
* 字典数据标签
*/
label?: string;
/**
* 状态1:启用0:禁用)
*/
status?: number;
/**
* 字典排序
*/
sort?: number;
/**
* 标签类型
*/
tagType?: "success" | "warning" | "info" | "primary" | "danger" | undefined;
}
/**
* 字典项下拉选项
*/
export interface DictItemOption {
/** 字典数据值 */
value: string;
/** 字典数据标签 */
label: string;
/** 标签类型 */
tagType: string;
}

View File

@@ -1,180 +0,0 @@
import request from "@/utils/request";
const DICT_BASE_URL = "/api/v1/dict";
const DictAPI = {
/**
* 获取字典分页列表
*
* @param queryParams 查询参数
* @returns 字典分页结果
*/
getPage(queryParams: DictPageQuery) {
return request<any, PageResult<DictPageVO[]>>({
url: `${DICT_BASE_URL}/page`,
method: "get",
params: queryParams,
});
},
/**
* 获取字典表单数据
*
* @param id 字典ID
* @returns 字典表单数据
*/
getFormData(id: number) {
return request<any, ResponseData<DictForm>>({
url: `${DICT_BASE_URL}/${id}/form`,
method: "get",
});
},
/**
* 新增字典
*
* @param data 字典表单数据
*/
add(data: DictForm) {
return request({
url: `${DICT_BASE_URL}`,
method: "post",
data: data,
});
},
/**
* 修改字典
*
* @param id 字典ID
* @param data 字典表单数据
*/
update(id: number, data: DictForm) {
return request({
url: `${DICT_BASE_URL}/${id}`,
method: "put",
data: data,
});
},
/**
* 删除字典
*
* @param ids 字典ID多个以英文逗号(,)分隔
*/
deleteByIds(ids: string) {
return request({
url: `${DICT_BASE_URL}/${ids}`,
method: "delete",
});
},
/**
* 获取字典列表
*
* @returns 字典列表
*/
getList() {
return request<any, DictVO[]>({
url: `${DICT_BASE_URL}/list`,
method: "get",
});
},
};
export default DictAPI;
/**
* 字典查询参数
*/
export interface DictPageQuery extends PageQuery {
/**
* 关键字(字典名称/编码)
*/
keywords?: string;
/**
* 字典状态1:启用0:禁用)
*/
status?: number;
}
/**
* 字典分页对象
*/
export interface DictPageVO {
/**
* 字典ID
*/
id: number;
/**
* 字典名称
*/
name: string;
/**
* 字典编码
*/
dictCode: string;
/**
* 字典状态1:启用0:禁用)
*/
status: number;
}
/**
* 字典
*/
export interface DictForm {
/**
* 字典ID
*/
id?: number;
/**
* 字典名称
*/
name?: string;
/**
* 字典编码
*/
dictCode?: string;
/**
* 字典状态1-启用0-禁用)
*/
status?: number;
/**
* 备注
*/
remark?: string;
}
/**
* 字典数据项分页VO
*
* @description 字典数据分页对象
*/
export interface DictVO {
/** 字典名称 */
name: string;
/** 字典编码 */
dictCode: string;
/** 字典数据集合 */
dictDataList: DictData[];
}
/**
* 字典数据
*
* @description 字典数据
*/
export interface DictData {
/** 字典数据值 */
value: string;
/** 字典数据标签 */
label: string;
/** 标签类型 */
tagType: string;
}

View File

@@ -62,7 +62,7 @@ const MenuAPI = {
* @param data
* @returns
*/
add(data: MenuForm) {
create(data: MenuForm) {
return request({
url: `${MENU_BASE_URL}`,
method: "post",
@@ -101,7 +101,7 @@ const MenuAPI = {
export default MenuAPI;
import type { MenuTypeEnum } from "@/enums/MenuTypeEnum";
import type { MenuTypeEnum } from "@/enums/system/menu.enum";
/** 菜单查询参数 */
export interface MenuQuery {

View File

@@ -31,7 +31,7 @@ const NoticeAPI = {
* @param data Notice表单数据
* @returns
*/
add(data: NoticeForm) {
create(data: NoticeForm) {
return request({
url: `${NOTICE_BASE_URL}`,
method: "post",

View File

@@ -60,7 +60,7 @@ const RoleAPI = {
},
/** 添加角色 */
add(data: RoleForm) {
create(data: RoleForm) {
return request({
url: `${ROLE_BASE_URL}`,
method: "post",

View File

@@ -46,7 +46,7 @@ const UserAPI = {
*
* @param data
*/
add(data: UserForm) {
create(data: UserForm) {
return request({
url: `${USER_BASE_URL}`,
method: "post",

View File

@@ -6,47 +6,55 @@
<span>{{ label }}</span>
</template>
</template>
<script setup lang="ts">
import { useDictStore } from "@/store";
const dictStore = useDictStore();
const props = defineProps({
code: String,
modelValue: [String, Number],
code: String, // 字典编码
modelValue: [String, Number], // 字典项的值
size: {
type: String,
default: "default",
default: "default", // 标签大小
},
});
const label = ref("");
const tagType = ref<"success" | "warning" | "info" | "primary" | "danger" | undefined>();
const tagSize = ref(props.size as "default" | "large" | "small");
const tagType = ref<"success" | "warning" | "info" | "primary" | "danger" | undefined>(); // 标签类型
const tagSize = ref<"default" | "large" | "small">(props.size as "default" | "large" | "small"); // 标签大小
const dictStore = useDictStore();
/**
* 根据字典项的值获取对应的 label 和 tagType
* @param dictCode 字典编码
* @param value 字典项的值
* @returns 包含 label 和 tagType 的对象
*/
const getLabelAndTagByValue = async (dictCode: string, value: any) => {
// 先从本地缓存中获取字典数据
const dictData = dictStore.getDictionary(dictCode);
// 按需加载字典数据
await dictStore.loadDictItems(dictCode);
// 从缓存中获取字典数据
const dictItems = dictStore.getDictItems(dictCode);
// 查找对应的字典项
const dictEntry = dictData.find((item: any) => item.value == value);
const dictItem = dictItems.find((item) => item.value == value);
return {
label: dictEntry ? dictEntry.label : "",
tag: dictEntry ? dictEntry.tagType : undefined,
label: dictItem?.label || "",
tagType: dictItem?.tagType,
};
};
// 监听 props 的变化,获取并更新 label 和 tag
const fetchLabelAndTag = async () => {
const result = await getLabelAndTagByValue(props.code as string, props.modelValue);
label.value = result.label;
tagType.value = result.tag as "success" | "warning" | "info" | "primary" | "danger" | undefined;
/**
* 更新 label 和 tagType
*/
const updateLabelAndTag = async () => {
console.log("updateLabelAndTag", props.code, props.modelValue);
if (!props.code || props.modelValue === undefined) return;
const { label: newLabel, tagType: newTagType } = await getLabelAndTagByValue(
props.code,
props.modelValue
);
label.value = newLabel;
tagType.value = newTagType as typeof tagType.value;
};
// 首次挂载时获取字典数据
onMounted(fetchLabelAndTag);
watch([() => props.code, () => props.modelValue], updateLabelAndTag);
// 当 modelValue 发生变化时重新获取
watch(() => props.modelValue, fetchLabelAndTag);
onMounted(updateLabelAndTag);
</script>

View File

@@ -100,41 +100,34 @@ const selectedValue = ref<any>(
: undefined
);
// 监听 modelValue 变化
// 监听 modelValue 和 options 的变化
watch(
() => props.modelValue,
(newValue) => {
if (props.type === "checkbox") {
selectedValue.value = Array.isArray(newValue) ? newValue : [];
[() => props.modelValue, () => options.value],
([newValue, newOptions]) => {
if (newOptions.length > 0 && newValue !== undefined) {
if (props.type === "checkbox") {
selectedValue.value = Array.isArray(newValue) ? newValue : [];
} else {
const matchedOption = newOptions.find(
(option) => String(option.value) === String(newValue)
);
selectedValue.value = matchedOption?.value;
}
} else {
selectedValue.value = newValue?.toString() || "";
selectedValue.value = undefined;
}
},
{ immediate: true }
);
// 监听 options 变化并重新匹配 selectedValue
watch(
() => options.value,
(newOptions) => {
// options 加载后,确保 selectedValue 可以正确匹配到 options
if (newOptions.length > 0 && selectedValue.value !== undefined) {
const matchedOption = newOptions.find((option) => option.value === selectedValue.value);
if (!matchedOption && props.type !== "checkbox") {
// 如果找不到匹配项,清空选中
selectedValue.value = "";
}
}
}
);
// 监听 selectedValue 的变化并触发 update:modelValue
function handleChange(val: any) {
emit("update:modelValue", val);
}
// 获取字典数据
onMounted(() => {
options.value = dictStore.getDictionary(props.code);
onMounted(async () => {
await dictStore.loadDictItems(props.code);
options.value = dictStore.getDictItems(props.code);
});
</script>

View File

@@ -6,8 +6,8 @@
<script setup lang="ts">
import { useSettingsStore } from "@/store";
import { ThemeEnum, SidebarColorEnum } from "@/enums/ThemeEnum";
import { LayoutEnum } from "@/enums/LayoutEnum";
import { ThemeMode, SidebarColor } from "@/enums/settings/theme.enum";
import { LayoutMode } from "@/enums/settings/layout.enum";
defineProps({
isActive: { type: Boolean, required: true },
@@ -20,14 +20,14 @@ const layout = computed(() => settingsStore.layout);
const hamburgerClass = computed(() => {
// 如果暗黑主题
if (settingsStore.theme === ThemeEnum.DARK) {
if (settingsStore.theme === ThemeMode.DARK) {
return "hamburger--white";
}
// 如果是混合布局 && 侧边栏配色方案是经典蓝
if (
layout.value === LayoutEnum.MIX &&
settingsStore.sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE
layout.value === LayoutMode.MIX &&
settingsStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE
) {
return "hamburger--white";
}

View File

@@ -17,8 +17,8 @@
</template>
<script setup lang="ts">
import { useAppStore } from "@/store/modules/app";
import { LanguageEnum } from "@/enums/LanguageEnum";
import { useAppStore } from "@/store/modules/app.store";
import { LanguageEnum } from "@/enums/settings/locale.enum";
defineProps({
size: {

View File

@@ -87,7 +87,7 @@
</template>
<script setup lang="ts">
import NoticeAPI, { NoticePageVO, NoticeDetailVO } from "@/api/system/notice";
import NoticeAPI, { NoticePageVO, NoticeDetailVO } from "@/api/system/notice.api";
import router from "@/router";
const noticeList = ref<NoticePageVO[]>([]);

View File

@@ -22,15 +22,15 @@
</template>
<script setup lang="ts">
import { SizeEnum } from "@/enums/SizeEnum";
import { useAppStore } from "@/store/modules/app";
import { ComponentSize } from "@/enums/settings/layout.enum";
import { useAppStore } from "@/store/modules/app.store";
const { t } = useI18n();
const sizeOptions = computed(() => {
return [
{ label: t("sizeSelect.default"), value: SizeEnum.DEFAULT },
{ label: t("sizeSelect.large"), value: SizeEnum.LARGE },
{ label: t("sizeSelect.small"), value: SizeEnum.SMALL },
{ label: t("sizeSelect.default"), value: ComponentSize.DEFAULT },
{ label: t("sizeSelect.large"), value: ComponentSize.LARGE },
{ label: t("sizeSelect.small"), value: ComponentSize.SMALL },
];
});

View File

@@ -50,7 +50,7 @@ import {
UploadRequestOptions,
} from "element-plus";
import FileAPI, { FileInfo } from "@/api/file";
import FileAPI, { FileInfo } from "@/api/file.api";
const props = defineProps({
/**

View File

@@ -40,7 +40,7 @@
</template>
<script setup lang="ts">
import { UploadRawFile, UploadRequestOptions, UploadUserFile } from "element-plus";
import FileAPI, { FileInfo } from "@/api/file";
import FileAPI, { FileInfo } from "@/api/file.api";
const props = defineProps({
/**

View File

@@ -26,7 +26,7 @@
<script setup lang="ts">
import { UploadRawFile, UploadRequestOptions } from "element-plus";
import FileAPI, { FileInfo } from "@/api/file";
import FileAPI, { FileInfo } from "@/api/file.api";
const props = defineProps({
/**

View File

@@ -34,7 +34,7 @@ import { Toolbar, Editor } from "@wangeditor-next/editor-for-vue";
import { IToolbarConfig, IEditorConfig } from "@wangeditor-next/editor";
// 文件上传 API
import FileAPI from "@/api/file";
import FileAPI from "@/api/file.api";
// 上传图片回调函数类型
type InsertFnType = (_url: string, _alt: string, _href: string) => void;

View File

@@ -1,18 +0,0 @@
/**
* 菜单布局枚举
*/
export const enum LayoutEnum {
/**
* 左侧菜单布局
*/
LEFT = "left",
/**
* 顶部菜单布局
*/
TOP = "top",
/**
* 混合菜单布局
*/
MIX = "mix",
}

View File

@@ -1,14 +0,0 @@
/**
* 侧边栏状态枚举
*/
export const enum SidebarStatusEnum {
/**
* 展开
*/
OPENED = "opened",
/**
* 关闭
*/
CLOSED = "closed",
}

View File

@@ -1,19 +0,0 @@
/**
* 布局大小枚举
*/
export const enum SizeEnum {
/**
* 默认
*/
DEFAULT = "default",
/**
* 大型
*/
LARGE = "large",
/**
* 小型
*/
SMALL = "small",
}

11
src/enums/index.ts Normal file
View File

@@ -0,0 +1,11 @@
export * from "./api/result.enum";
export * from "./codegen/form.enum";
export * from "./codegen/query.enum";
export * from "./settings/layout.enum";
export * from "./settings/theme.enum";
export * from "./settings/locale.enum";
export * from "./settings/device.enum";
export * from "./system/menu.enum";

View File

@@ -0,0 +1,53 @@
/**
* 菜单布局枚举
*/
export const enum LayoutMode {
/**
* 左侧菜单布局
*/
LEFT = "left",
/**
* 顶部菜单布局
*/
TOP = "top",
/**
* 混合菜单布局
*/
MIX = "mix",
}
/**
* 侧边栏状态枚举
*/
export const enum SidebarStatus {
/**
* 展开
*/
OPENED = "opened",
/**
* 关闭
*/
CLOSED = "closed",
}
/**
* 组件尺寸枚举
*/
export const enum ComponentSize {
/**
* 默认
*/
DEFAULT = "default",
/**
* 大型
*/
LARGE = "large",
/**
* 小型
*/
SMALL = "small",
}

View File

@@ -1,7 +1,7 @@
/**
*
*/
export const enum ThemeEnum {
export const enum ThemeMode {
/**
*
*/
@@ -20,7 +20,7 @@ export const enum ThemeEnum {
/**
*
*/
export const enum SidebarColorEnum {
export const enum SidebarColor {
/**
*
*/

View File

@@ -1,6 +1,6 @@
import type { App } from "vue";
import { createI18n } from "vue-i18n";
import { useAppStoreHook } from "@/store/modules/app";
import { useAppStoreHook } from "@/store/modules/app.store";
// 本地语言包
import enLocale from "./package/en";
import zhCnLocale from "./package/zh-cn";

View File

@@ -30,7 +30,7 @@ export default {
// 导航栏国际化
navbar: {
dashboard: "首页",
logout: "注销登",
logout: "注销登",
document: "项目文档",
gitee: "项目地址",
profile: "个人中心",

View File

@@ -44,10 +44,10 @@
</template>
<script setup lang="ts">
import defaultSettings from "@/settings";
import { DeviceEnum } from "@/enums/DeviceEnum";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { useAppStore, useSettingsStore, useUserStore, useTagsViewStore } from "@/store";
import { SidebarColorEnum, ThemeEnum } from "@/enums/ThemeEnum";
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
const appStore = useAppStore();
const settingStore = useSettingsStore();
@@ -68,12 +68,12 @@ function handleProfileClick() {
// 根据主题和侧边栏配色方案选择 navbar 右侧的样式类
const navbarRightClass = computed(() => {
// 如果暗黑主题
if (settingStore.theme === ThemeEnum.DARK) {
if (settingStore.theme === ThemeMode.DARK) {
return "navbar__right--white";
}
// 如果侧边栏是经典蓝
if (settingStore.sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE) {
if (settingStore.sidebarColorScheme === SidebarColor.CLASSIC_BLUE) {
return "navbar__right--white";
}
});

View File

@@ -21,26 +21,26 @@
</template>
<script lang="ts" setup>
import { LayoutEnum } from "@/enums/LayoutEnum";
import { LayoutMode } from "@/enums/settings/layout.enum";
interface LayoutOption {
value: LayoutEnum;
value: LayoutMode;
label: string;
className: string;
}
const layoutOptions: LayoutOption[] = [
{ value: LayoutEnum.LEFT, label: "左侧模式", className: "left" },
{ value: LayoutEnum.TOP, label: "顶部模式", className: "top" },
{ value: LayoutEnum.MIX, label: "混合模式", className: "mix" },
{ value: LayoutMode.LEFT, label: "左侧模式", className: "left" },
{ value: LayoutMode.TOP, label: "顶部模式", className: "top" },
{ value: LayoutMode.MIX, label: "混合模式", className: "mix" },
];
const modelValue = defineModel<LayoutEnum>("modelValue", {
const modelValue = defineModel<LayoutMode>("modelValue", {
required: true,
default: () => LayoutEnum.LEFT,
default: () => LayoutMode.LEFT,
});
function handleLayoutChange(layout: LayoutEnum) {
function handleLayoutChange(layout: LayoutMode) {
modelValue.value = layout;
}
</script>

View File

@@ -48,10 +48,10 @@
<div v-if="!isDark" class="config-item flex-x-between">
<span class="text-xs">{{ $t("settings.sidebarColorScheme") }}</span>
<el-radio-group v-model="sidebarColor" @change="changeSidebarColor">
<el-radio :value="SidebarColorEnum.CLASSIC_BLUE">
<el-radio :value="SidebarColor.CLASSIC_BLUE">
{{ $t("settings.classicBlue") }}
</el-radio>
<el-radio :value="SidebarColorEnum.MINIMAL_WHITE">
<el-radio :value="SidebarColor.MINIMAL_WHITE">
{{ $t("settings.minimalWhite") }}
</el-radio>
</el-radio-group>
@@ -67,9 +67,9 @@
</template>
<script setup lang="ts">
import { LayoutEnum } from "@/enums/LayoutEnum";
import { ThemeEnum } from "@/enums/ThemeEnum";
import { SidebarColorEnum } from "@/enums/ThemeEnum";
import { LayoutMode } from "@/enums/settings/layout.enum";
import { ThemeMode } from "@/enums/settings/theme.enum";
import { SidebarColor } from "@/enums/settings/theme.enum";
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
// 颜色预设
const colorPresets = [
@@ -89,7 +89,7 @@ const appStore = useAppStore();
const settingsStore = useSettingsStore();
const permissionStore = usePermissionStore();
const isDark = ref<boolean>(settingsStore.theme === ThemeEnum.DARK);
const isDark = ref<boolean>(settingsStore.theme === ThemeMode.DARK);
const sidebarColor = ref(settingsStore.sidebarColorScheme);
const selectedThemeColor = computed({
@@ -108,7 +108,7 @@ const drawerVisible = computed({
* @param isDark 是否启用暗黑模式
*/
const handleThemeChange = (isDark: string | number | boolean) => {
settingsStore.changeTheme(isDark ? ThemeEnum.DARK : ThemeEnum.LIGHT);
settingsStore.changeTheme(isDark ? ThemeMode.DARK : ThemeMode.LIGHT);
};
/**
@@ -125,9 +125,9 @@ const changeSidebarColor = (val: any) => {
*
* @param layout - 布局模式
*/
const handleLayoutChange = (layout: LayoutEnum) => {
const handleLayoutChange = (layout: LayoutMode) => {
settingsStore.changeLayout(layout);
if (layout === LayoutEnum.MIX && route.name) {
if (layout === LayoutMode.MIX && route.name) {
const topLevelRoute = findTopLevelRoute(permissionStore.routes, route.name as string);
if (appStore.activeTopMenuPath !== topLevelRoute.path) {
appStore.activeTopMenu(topLevelRoute.path);

View File

@@ -5,17 +5,17 @@
:default-active="currentRoute.path"
:collapse="!appStore.sidebar.opened"
:background-color="
theme === 'dark' || sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-background']
: undefined
"
:text-color="
theme === 'dark' || sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-text']
: undefined
"
:active-text-color="
theme === 'dark' || sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-active-text']
: undefined
"
@@ -41,8 +41,8 @@ import path from "path-browserify";
import type { MenuInstance } from "element-plus";
import type { RouteRecordRaw } from "vue-router";
import { LayoutEnum } from "@/enums/LayoutEnum";
import { SidebarColorEnum } from "@/enums/ThemeEnum";
import { LayoutMode } from "@/enums/settings/layout.enum";
import { SidebarColor } from "@/enums/settings/theme.enum";
import { useSettingsStore, useAppStore } from "@/store";
import { isExternal } from "@/utils/index";
@@ -70,7 +70,7 @@ const expandedMenuIndexes = ref<string[]>([]);
// 根据布局模式设置菜单的显示方式:顶部布局使用水平模式,其他使用垂直模式
const menuMode = computed(() => {
return settingsStore.layout === LayoutEnum.TOP ? "horizontal" : "vertical";
return settingsStore.layout === LayoutMode.TOP ? "horizontal" : "vertical";
});
// 获取主题

View File

@@ -5,17 +5,17 @@
mode="horizontal"
:default-active="activePath"
:background-color="
theme === 'dark' || sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-background']
: undefined
"
:text-color="
theme === 'dark' || sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-text']
: undefined
"
:active-text-color="
theme === 'dark' || sidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE
theme === 'dark' || sidebarColorScheme === SidebarColor.CLASSIC_BLUE
? variables['menu-active-text']
: undefined
"
@@ -44,7 +44,7 @@ import { LocationQueryRaw, RouteRecordRaw } from "vue-router";
import { usePermissionStore, useAppStore, useSettingsStore } from "@/store";
import { translateRouteTitle } from "@/utils/i18n";
import variables from "@/styles/variables.module.scss";
import { SidebarColorEnum } from "@/enums/ThemeEnum";
import { SidebarColor } from "@/enums/settings/theme.enum";
const router = useRouter();
const appStore = useAppStore();

View File

@@ -1,7 +1,7 @@
<template>
<div :class="{ 'has-logo': sidebarLogo }">
<!-- 混合布局 -->
<div v-if="layout == LayoutEnum.MIX" class="flex w-full">
<div v-if="layout == LayoutMode.MIX" class="flex w-full">
<SidebarLogo v-if="sidebarLogo" :collapse="isSidebarCollapsed" />
<SidebarMixTopMenu class="flex-1" />
<NavbarRight />
@@ -15,13 +15,13 @@
</el-scrollbar>
<!-- 顶部导航 -->
<NavbarRight v-if="layout == LayoutEnum.TOP" />
<NavbarRight v-if="layout == LayoutMode.TOP" />
</template>
</div>
</template>
<script setup lang="ts">
import { LayoutEnum } from "@/enums/LayoutEnum";
import { LayoutMode } from "@/enums/settings/layout.enum";
import { useSettingsStore, usePermissionStore, useAppStore } from "@/store";
import NavbarRight from "../NavBar/components/NavbarRight.vue";

View File

@@ -7,7 +7,7 @@
<Sidebar class="layout__sidebar" />
<!-- 混合布局 -->
<div v-if="layout === LayoutEnum.MIX" class="layout__container">
<div v-if="layout === LayoutMode.MIX" class="layout__container">
<!-- 左侧菜单栏 -->
<div class="layout__sidebar--left">
<el-scrollbar>
@@ -32,7 +32,7 @@
<!-- 左侧或顶部布局的主内容区 -->
<div v-else :class="{ hasTagsView: isShowTagsView }" class="layout__main">
<NavBar v-if="layout === LayoutEnum.LEFT" />
<NavBar v-if="layout === LayoutMode.LEFT" />
<TagsView v-if="isShowTagsView" />
<AppMain />
<Settings v-if="defaultSettings.showSettings" />
@@ -52,8 +52,8 @@ import { useAppStore, useSettingsStore, usePermissionStore } from "@/store";
import defaultSettings from "@/settings";
// 枚举
import { DeviceEnum } from "@/enums/DeviceEnum";
import { LayoutEnum } from "@/enums/LayoutEnum";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { LayoutMode } from "@/enums/settings/layout.enum";
// 组件
import NavBar from "./components/NavBar/index.vue";

View File

@@ -40,7 +40,7 @@ export function setupPermission() {
} catch (error) {
console.error(error);
// 路由加载失败,重置 token 并重定向到登录页
await useUserStore().clearUserData();
await useUserStore().clearSessionAndCache();
redirectToLogin(to, next);
NProgress.done();
}

View File

@@ -1,7 +1,4 @@
import { SizeEnum } from "./enums/SizeEnum";
import { LayoutEnum } from "./enums/LayoutEnum";
import { ThemeEnum, SidebarColorEnum } from "./enums/ThemeEnum";
import { LanguageEnum } from "./enums/LanguageEnum";
import { LayoutMode, ComponentSize, SidebarColor, ThemeMode, LanguageEnum } from "./enums";
const { pkg } = __APP_INFO__;
@@ -20,11 +17,11 @@ const defaultSettings: AppSettings = {
// 是否显示侧边栏Logo
sidebarLogo: true,
// 布局方式,默认为左侧布局
layout: LayoutEnum.LEFT,
layout: LayoutMode.LEFT,
// 主题,根据操作系统的色彩方案自动选择
theme: mediaQueryList.matches ? ThemeEnum.DARK : ThemeEnum.LIGHT,
theme: mediaQueryList.matches ? ThemeMode.DARK : ThemeMode.LIGHT,
// 组件大小 default | medium | small | large
size: SizeEnum.DEFAULT,
size: ComponentSize.DEFAULT,
// 语言
language: LanguageEnum.ZH_CN,
// 主题颜色
@@ -34,7 +31,7 @@ const defaultSettings: AppSettings = {
// 水印内容
watermarkContent: pkg.name,
// 侧边栏配色方案
sidebarColorScheme: SidebarColorEnum.CLASSIC_BLUE,
sidebarColorScheme: SidebarColor.CLASSIC_BLUE,
};
export default defaultSettings;

View File

@@ -8,10 +8,10 @@ export function setupStore(app: App<Element>) {
app.use(store);
}
export * from "./modules/app";
export * from "./modules/permission";
export * from "./modules/settings";
export * from "./modules/tags-view";
export * from "./modules/user";
export * from "./modules/dict";
export * from "./modules/app.store";
export * from "./modules/permission.store";
export * from "./modules/settings.store";
export * from "./modules/tags-view.store";
export * from "./modules/user.store";
export * from "./modules/dict.store";
export { store };

View File

@@ -4,8 +4,8 @@ import defaultSettings from "@/settings";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import en from "element-plus/es/locale/lang/en";
import { store } from "@/store";
import { DeviceEnum } from "@/enums/DeviceEnum";
import { SidebarStatusEnum } from "@/enums/SidebarStatusEnum";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { SidebarStatus } from "@/enums/settings/layout.enum";
export const useAppStore = defineStore("app", () => {
// 设备类型
@@ -15,9 +15,9 @@ export const useAppStore = defineStore("app", () => {
// 语言
const language = useStorage("language", defaultSettings.language);
// 侧边栏状态
const sidebarStatus = useStorage("sidebarStatus", SidebarStatusEnum.CLOSED);
const sidebarStatus = useStorage("sidebarStatus", SidebarStatus.CLOSED);
const sidebar = reactive({
opened: sidebarStatus.value === SidebarStatusEnum.OPENED,
opened: sidebarStatus.value === SidebarStatus.OPENED,
withoutAnimation: false,
});
@@ -38,19 +38,19 @@ export const useAppStore = defineStore("app", () => {
// 切换侧边栏
function toggleSidebar() {
sidebar.opened = !sidebar.opened;
sidebarStatus.value = sidebar.opened ? SidebarStatusEnum.OPENED : SidebarStatusEnum.CLOSED;
sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED;
}
// 关闭侧边栏
function closeSideBar() {
sidebar.opened = false;
sidebarStatus.value = SidebarStatusEnum.CLOSED;
sidebarStatus.value = SidebarStatus.CLOSED;
}
// 打开侧边栏
function openSideBar() {
sidebar.opened = true;
sidebarStatus.value = SidebarStatusEnum.OPENED;
sidebarStatus.value = SidebarStatus.OPENED;
}
// 切换设备

View File

@@ -0,0 +1,55 @@
import { store } from "@/store";
import DictAPI, { type DictItemOption } from "@/api/system/dict.api";
export const useDictStore = defineStore("dict", () => {
// 字典数据缓存
const dictCache = useStorage<Record<string, DictItemOption[]>>("dict_cache", {});
// 请求队列(防止重复请求)
const requestQueue: Record<string, Promise<void>> = {};
/**
* 缓存字典数据
* @param dictCode 字典编码
* @param data 字典项列表
*/
const cacheDictItems = (dictCode: string, data: DictItemOption[]) => {
dictCache.value[dictCode] = data;
};
/**
* 加载字典数据(如果缓存中没有则请求)
* @param dictCode 字典编码
*/
const loadDictItems = async (dictCode: string) => {
if (dictCache.value[dictCode]) return;
// 防止重复请求
if (!requestQueue[dictCode]) {
requestQueue[dictCode] = DictAPI.getDictItems(dictCode).then((data) => {
cacheDictItems(dictCode, data);
Reflect.deleteProperty(requestQueue, dictCode);
});
}
await requestQueue[dictCode];
};
/**
* 获取字典项列表
* @param dictCode 字典编码
* @returns 字典项列表
*/
const getDictItems = (dictCode: string): DictItemOption[] => {
return dictCache.value[dictCode] || [];
};
/**
* 清空字典缓存
*/
const clearDictCache = () => {
dictCache.value = {};
};
return {
loadDictItems,
getDictItems,
clearDictCache,
};
});
export function useDictStoreHook() {
return useDictStore(store);
}

View File

@@ -1,41 +0,0 @@
import { store } from "@/store";
import DictionaryAPI, { type DictVO, type DictData } from "@/api/system/dict";
export const useDictStore = defineStore("dict", () => {
const dictionary = useStorage<Record<string, DictData[]>>("dictionary", {});
const setDictionary = (dict: DictVO) => {
dictionary.value[dict.dictCode] = dict.dictDataList;
};
const loadDictionaries = async () => {
const dictList = await DictionaryAPI.getList();
dictList.forEach(setDictionary);
};
const getDictionary = (dictCode: string): DictData[] => {
return dictionary.value[dictCode] || [];
};
const clearDictionaryCache = () => {
dictionary.value = {};
};
const updateDictionaryCache = async () => {
clearDictionaryCache(); // 先清除旧缓存
await loadDictionaries(); // 重新加载最新字典数据
};
return {
dictionary,
setDictionary,
loadDictionaries,
getDictionary,
clearDictionaryCache,
updateDictionaryCache,
};
});
export function useDictStoreHook() {
return useDictStore(store);
}

View File

@@ -3,7 +3,7 @@ import { constantRoutes } from "@/router";
import { store } from "@/store";
import router from "@/router";
import MenuAPI, { type RouteVO } from "@/api/system/menu";
import MenuAPI, { type RouteVO } from "@/api/system/menu.api";
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");

View File

@@ -1,6 +1,6 @@
import defaultSettings from "@/settings";
import { SidebarColorEnum, ThemeEnum } from "@/enums/ThemeEnum";
import { LayoutEnum } from "@/enums/LayoutEnum";
import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum";
import { LayoutMode } from "@/enums/settings/layout.enum";
import { generateThemeColors, applyTheme, toggleDarkMode, toggleSidebarColor } from "@/utils/theme";
type SettingsValue = boolean | string;
@@ -18,7 +18,7 @@ export const useSettingsStore = defineStore("setting", () => {
defaultSettings.sidebarColorScheme
);
// 布局
const layout = useStorage<LayoutEnum>("layout", defaultSettings.layout as LayoutEnum);
const layout = useStorage<LayoutMode>("layout", defaultSettings.layout as LayoutMode);
// 水印
const watermarkEnabled = useStorage<boolean>(
"watermarkEnabled",
@@ -33,7 +33,7 @@ export const useSettingsStore = defineStore("setting", () => {
watch(
[theme, themeColor],
([newTheme, newThemeColor]) => {
toggleDarkMode(newTheme === ThemeEnum.DARK);
toggleDarkMode(newTheme === ThemeMode.DARK);
const colors = generateThemeColors(newThemeColor);
applyTheme(colors);
},
@@ -44,7 +44,7 @@ export const useSettingsStore = defineStore("setting", () => {
watch(
[sidebarColorScheme],
([newSidebarColorScheme]) => {
toggleSidebarColor(newSidebarColorScheme === SidebarColorEnum.CLASSIC_BLUE);
toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE);
},
{ immediate: true }
);
@@ -75,7 +75,7 @@ export const useSettingsStore = defineStore("setting", () => {
themeColor.value = color;
}
function changeLayout(val: LayoutEnum) {
function changeLayout(val: LayoutMode) {
layout.value = val;
}

View File

@@ -1,9 +1,9 @@
import { store } from "@/store";
import { usePermissionStoreHook } from "@/store/modules/permission";
import { useDictStoreHook } from "@/store/modules/dict";
import { usePermissionStoreHook } from "@/store/modules/permission.store";
import { useDictStoreHook } from "@/store/modules/dict.store";
import AuthAPI, { type LoginFormData } from "@/api/auth";
import UserAPI, { type UserInfo } from "@/api/system/user";
import AuthAPI, { type LoginFormData } from "@/api/auth.api";
import UserAPI, { type UserInfo } from "@/api/system/user.api";
import { setAccessToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth";
@@ -60,7 +60,7 @@ export const useUserStore = defineStore("user", () => {
return new Promise<void>((resolve, reject) => {
AuthAPI.logout()
.then(() => {
clearUserData();
clearSessionAndCache();
resolve();
})
.catch((error) => {
@@ -90,15 +90,13 @@ export const useUserStore = defineStore("user", () => {
}
/**
*
*
* @returns
*
*/
function clearUserData() {
function clearSessionAndCache() {
return new Promise<void>((resolve) => {
clearToken();
usePermissionStoreHook().resetRouter();
useDictStoreHook().clearDictionaryCache();
useDictStoreHook().clearDictCache();
resolve();
});
}
@@ -108,7 +106,7 @@ export const useUserStore = defineStore("user", () => {
getUserInfo,
login,
logout,
clearUserData,
clearSessionAndCache,
refreshToken,
};
});

View File

@@ -1,7 +1,7 @@
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios";
import qs from "qs";
import { useUserStoreHook } from "@/store/modules/user";
import { ResultEnum } from "@/enums/ResultEnum";
import { useUserStoreHook } from "@/store/modules/user.store";
import { ResultEnum } from "@/enums/api/result.enum";
import { getAccessToken } from "@/utils/auth";
import router from "@/router";
@@ -12,12 +12,11 @@ const service = axios.create({
headers: { "Content-Type": "application/json;charset=utf-8" },
paramsSerializer: (params) => qs.stringify(params),
});
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = getAccessToken();
// 如果 Authorization 设置为 no-auth则不携带 Token,用于登录、刷新 Token 等接口
// 如果 Authorization 设置为 no-auth则不携带 Token
if (config.headers.Authorization !== "no-auth" && accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
} else {
@@ -27,7 +26,6 @@ service.interceptors.request.use(
},
(error) => Promise.reject(error)
);
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
@@ -35,18 +33,15 @@ service.interceptors.response.use(
if (response.config.responseType === "blob") {
return response;
}
const { code, data, msg } = response.data;
if (code === ResultEnum.SUCCESS) {
return data;
}
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error"));
},
async (error) => {
console.error("request error", error); // for debug
// 非 2xx 状态码处理 401、403、500 等
const { config, response } = error;
if (response) {
const { code, msg } = response.data;
@@ -54,6 +49,8 @@ service.interceptors.response.use(
// Token 过期,刷新 Token
return handleTokenRefresh(config);
} else if (code === ResultEnum.REFRESH_TOKEN_INVALID) {
// 刷新 Token 过期,跳转登录页
await handleSessionExpired();
return Promise.reject(new Error(msg || "Error"));
} else {
ElMessage.error(msg || "系统出错");
@@ -62,29 +59,22 @@ service.interceptors.response.use(
return Promise.reject(error.message);
}
);
export default service;
// 是否正在刷新标识,避免重复刷新
let isRefreshing = false;
// 因 Token 过期导致的请求等待队列
const waitingQueue: Array<() => void> = [];
// 刷新 Token 处理
async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
return new Promise((resolve) => {
// 封装需要重试的请求
const retryRequest = () => {
config.headers.Authorization = getAccessToken();
config.headers.Authorization = `Bearer ${getAccessToken()}`;
resolve(service(config));
};
waitingQueue.push(retryRequest);
if (!isRefreshing) {
isRefreshing = true;
// 刷新 Token
useUserStoreHook()
.refreshToken()
.then(() => {
@@ -92,19 +82,10 @@ async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
waitingQueue.forEach((callback) => callback());
waitingQueue.length = 0;
})
.catch((error: any) => {
console.log("handleTokenRefresh error", error);
.catch(async (error) => {
console.error("handleTokenRefresh error", error);
// 刷新 Token 失败,跳转登录页
ElNotification({
title: "提示",
message: "您的会话已过期,请重新登录",
type: "info",
});
useUserStoreHook()
.clearUserData()
.then(() => {
router.push("/login");
});
await handleSessionExpired();
})
.finally(() => {
isRefreshing = false;
@@ -112,3 +93,13 @@ async function handleTokenRefresh(config: InternalAxiosRequestConfig) {
}
});
}
// 处理会话过期
async function handleSessionExpired() {
ElNotification({
title: "提示",
message: "您的会话已过期,请重新登录",
type: "info",
});
await useUserStoreHook().clearSessionAndCache();
router.push("/login");
}

View File

@@ -417,18 +417,18 @@ import Codemirror from "codemirror-editor-vue3";
import type { CmComponentRef } from "codemirror-editor-vue3";
import type { EditorConfiguration } from "codemirror";
import { FormTypeEnum } from "@/enums/FormTypeEnum";
import { QueryTypeEnum } from "@/enums/QueryTypeEnum";
import { FormTypeEnum } from "@/enums/codegen/form.enum";
import { QueryTypeEnum } from "@/enums/codegen/query.enum";
import GeneratorAPI, {
TablePageVO,
GenConfigForm,
TablePageQuery,
FieldConfig,
} from "@/api/codegen";
} from "@/api/codegen.api";
import DictAPI from "@/api/system/dict";
import MenuAPI from "@/api/system/menu";
import DictAPI from "@/api/system/dict.api";
import MenuAPI from "@/api/system/menu.api";
interface TreeNode {
label: string;

View File

@@ -285,8 +285,8 @@ defineOptions({
});
import { dayjs } from "element-plus";
import LogAPI, { VisitStatsVO, VisitTrendVO } from "@/api/system/log";
import { useUserStore } from "@/store/modules/user";
import LogAPI, { VisitStatsVO, VisitTrendVO } from "@/api/system/log.api";
import { useUserStore } from "@/store/modules/user.store";
import { formatGrowthRate } from "@/utils";
interface VersionItem {

View File

@@ -1,4 +1,4 @@
import UserAPI, { type UserForm } from "@/api/system/user";
import UserAPI, { type UserForm } from "@/api/system/user.api";
import type { IModalConfig } from "@/components/CURD/types";
const modalConfig: IModalConfig<UserForm> = {
@@ -11,7 +11,7 @@ const modalConfig: IModalConfig<UserForm> = {
form: {
labelWidth: 100,
},
formAction: UserAPI.add,
formAction: UserAPI.create,
beforeSubmit(data) {
console.log("提交之前处理", data);
},

View File

@@ -1,6 +1,6 @@
import UserAPI from "@/api/system/user";
import RoleAPI from "@/api/system/role";
import type { UserPageQuery } from "@/api/system/user";
import UserAPI from "@/api/system/user.api";
import RoleAPI from "@/api/system/role.api";
import type { UserPageQuery } from "@/api/system/user.api";
import type { IContentConfig } from "@/components/CURD/types";
const contentConfig: IContentConfig<UserPageQuery> = {

View File

@@ -1,6 +1,6 @@
import UserAPI, { type UserForm } from "@/api/system/user";
import UserAPI, { type UserForm } from "@/api/system/user.api";
import type { IModalConfig } from "@/components/CURD/types";
import { DeviceEnum } from "@/enums/DeviceEnum";
import { DeviceEnum } from "@/enums/settings/device.enum";
import { useAppStore } from "@/store";
const modalConfig: IModalConfig<UserForm> = {

View File

@@ -1,4 +1,4 @@
import DeptAPI from "@/api/system/dept";
import DeptAPI from "@/api/system/dept.api";
import type { ISearchConfig } from "@/components/CURD/types";
const searchConfig: ISearchConfig = {

View File

@@ -90,9 +90,9 @@
</template>
<script setup lang="ts">
import UserAPI from "@/api/system/user";
import DeptAPI from "@/api/system/dept";
import RoleAPI from "@/api/system/role";
import UserAPI from "@/api/system/user.api";
import DeptAPI from "@/api/system/dept.api";
import RoleAPI from "@/api/system/role.api";
import type { IObject, IOperatData } from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage";
import addModalConfig from "./config/add";

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import FileAPI from "@/api/file";
import FileAPI from "@/api/file.api";
const imgUrl = ref("");
const canvas = ref();

View File

@@ -1,4 +1,4 @@
import UserAPI from "@/api/system/user";
import UserAPI from "@/api/system/user.api";
import type { ISelectConfig } from "@/components/TableSelect/index.vue";
const selectConfig: ISelectConfig = {

View File

@@ -98,7 +98,7 @@
<script setup lang="ts">
import { useStomp } from "@/hooks/useStomp";
import { useUserStoreHook } from "@/store/modules/user";
import { useUserStoreHook } from "@/store/modules/user.store";
const userStore = useUserStoreHook();
// 用于手动调整 WebSocket 地址

View File

@@ -146,28 +146,27 @@
</template>
<script setup lang="ts">
import { LocationQuery, useRoute } from "vue-router";
import { LocationQuery, RouteLocationRaw, useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import AuthAPI, { type LoginFormData } from "@/api/auth";
import AuthAPI, { type LoginFormData } from "@/api/auth.api";
import router from "@/router";
import type { FormInstance } from "element-plus";
import defaultSettings from "@/settings";
import { ThemeEnum } from "@/enums/ThemeEnum";
import { ThemeMode } from "@/enums/settings/theme.enum";
import { useSettingsStore, useUserStore, useDictStore } from "@/store";
import { useSettingsStore, useUserStore } from "@/store";
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const dictStore = useDictStore();
const route = useRoute();
const { t } = useI18n();
const loginFormRef = ref<FormInstance>();
const isDark = ref(settingsStore.theme === ThemeEnum.DARK); // 是否暗黑模式
const isDark = ref(settingsStore.theme === ThemeMode.DARK); // 是否暗黑模式
const loading = ref(false); // 按钮 loading 状态
const isCapslock = ref(false); // 是否大写锁定
const captchaBase64 = ref(); // 验证码图片Base64字符串
@@ -218,57 +217,60 @@ function getCaptcha() {
});
}
// 登录
// 登录提交处理
async function handleLoginSubmit() {
loginFormRef.value?.validate((valid: boolean) => {
if (valid) {
loading.value = true;
userStore
.login(loginFormData.value)
.then(async () => {
await userStore.getUserInfo();
// 需要在路由跳转前加载字典数据,否则会出现字典数据未加载完成导致页面渲染异常
await dictStore.loadDictionaries();
// 跳转到登录前的页面
const { path, queryParams } = parseRedirect();
router.push({ path: path, query: queryParams });
})
.catch(() => {
getCaptcha();
})
.finally(() => {
loading.value = false;
});
}
});
// 1. 表单验证
const valid = await loginFormRef.value?.validate();
if (!valid) return;
loading.value = true;
try {
// 2. 执行登录
await userStore.login(loginFormData.value);
// 3. 获取用户信息
await userStore.getUserInfo();
// 4. 解析并跳转目标地址
const redirect = resolveRedirectTarget(route.query);
await router.push(redirect);
} catch (error) {
// 5. 统一错误处理
getCaptcha(); // 刷新验证码
console.error("登录失败:", error);
} finally {
loading.value = false;
}
}
/**
* 解析 redirect 字符串 为 path 和 queryParams
*
* @returns { path: string, queryParams: Record<string, string> } 解析后的 path 和 queryParams
* 解析重定向目标
* @param query 路由查询参数
* @returns 标准化后的路由地址对象
*/
function parseRedirect(): {
path: string;
queryParams: Record<string, string>;
} {
const query: LocationQuery = route.query;
const redirect = (query.redirect as string) ?? "/";
function resolveRedirectTarget(query: LocationQuery): RouteLocationRaw {
// 默认跳转路径
const defaultPath = "/";
const url = new URL(redirect, window.location.origin);
const path = url.pathname;
const queryParams: Record<string, string> = {};
// 获取原始重定向路径
const rawRedirect = (query.redirect as string) || defaultPath;
url.searchParams.forEach((value, key) => {
queryParams[key] = value;
});
return { path, queryParams };
try {
// 6. 使用Vue Router解析路径
const resolved = router.resolve(rawRedirect);
return {
path: resolved.path,
query: resolved.query,
};
} catch {
// 7. 异常处理:返回安全路径
return { path: defaultPath };
}
}
// 主题切换
const toggleTheme = () => {
const newTheme = settingsStore.theme === ThemeEnum.DARK ? ThemeEnum.LIGHT : ThemeEnum.DARK;
const newTheme = settingsStore.theme === ThemeMode.DARK ? ThemeMode.LIGHT : ThemeMode.DARK;
settingsStore.changeTheme(newTheme);
};

View File

@@ -260,9 +260,9 @@ import UserAPI, {
MobileUpdateForm,
EmailUpdateForm,
UserProfileForm,
} from "@/api/system/user";
} from "@/api/system/user.api";
import FileAPI from "@/api/file";
import FileAPI from "@/api/file.api";
import { Camera } from "@element-plus/icons-vue";

View File

@@ -135,7 +135,7 @@ defineOptions({
inheritAttrs: false,
});
import ConfigAPI, { ConfigPageVO, ConfigForm, ConfigPageQuery } from "@/api/system/config";
import ConfigAPI, { ConfigPageVO, ConfigForm, ConfigPageQuery } from "@/api/system/config.api";
const queryFormRef = ref();
const dataFormRef = ref();
@@ -233,7 +233,7 @@ function handleSubmit() {
})
.finally(() => (loading.value = false));
} else {
ConfigAPI.add(formData)
ConfigAPI.create(formData)
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();

View File

@@ -158,7 +158,7 @@ defineOptions({
inheritAttrs: false,
});
import DeptAPI, { DeptVO, DeptForm, DeptQuery } from "@/api/system/dept";
import DeptAPI, { DeptVO, DeptForm, DeptQuery } from "@/api/system/dept.api";
const queryFormRef = ref();
const deptFormRef = ref();
@@ -251,7 +251,7 @@ function handleSubmit() {
})
.finally(() => (loading.value = false));
} else {
DeptAPI.add(formData)
DeptAPI.create(formData)
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();

View File

@@ -1,4 +1,4 @@
<!-- 字典数据 -->
<!-- 字典 -->
<template>
<div class="app-container">
<div class="search-bar mt-5">
@@ -34,8 +34,8 @@
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="字典标签" prop="label" />
<el-table-column label="字典值" prop="value" />
<el-table-column label="字典标签" prop="label" />
<el-table-column label="字典值" prop="value" />
<el-table-column label="排序" prop="sort" />
<el-table-column label="状态">
<template #default="scope">
@@ -78,7 +78,7 @@
/>
</el-card>
<!--字典弹窗-->
<!--字典弹窗-->
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
@@ -87,10 +87,10 @@
>
<el-form ref="dataFormRef" :model="formData" :rules="computedRules" label-width="100px">
<el-card shadow="never">
<el-form-item label="字典标签" prop="label">
<el-form-item label="字典标签" prop="label">
<el-input v-model="formData.label" placeholder="请输入字典标签" />
</el-form-item>
<el-form-item label="字典值" prop="value">
<el-form-item label="字典值" prop="value">
<el-input v-model="formData.value" placeholder="请输入字典值" />
</el-form-item>
<el-form-item label="状态">
@@ -130,15 +130,11 @@
<script setup lang="ts">
defineOptions({
name: "DictData",
name: "DictItem",
inherititems: false,
});
import DictDataAPI, {
DictDataPageQuery,
DictDataPageVO,
DictDataForm,
} from "@/api/system/dict-data";
import DictAPI, { DictItemPageQuery, DictItemPageVO, DictItemForm } from "@/api/system/dict.api";
const route = useRoute();
@@ -151,20 +147,19 @@ const loading = ref(false);
const ids = ref<number[]>([]);
const total = ref(0);
const queryParams = reactive<DictDataPageQuery>({
const queryParams = reactive<DictItemPageQuery>({
pageNum: 1,
pageSize: 10,
dictCode: dictCode.value,
});
const tableData = ref<DictDataPageVO[]>();
const tableData = ref<DictItemPageVO[]>();
const dialog = reactive({
title: "",
visible: false,
});
const formData = reactive<DictDataForm>({});
const formData = reactive<DictItemForm>({});
const computedRules = computed(() => {
const rules: Partial<Record<string, any>> = {
@@ -177,7 +172,7 @@ const computedRules = computed(() => {
//
function handleQuery() {
loading.value = true;
DictDataAPI.getPage(queryParams)
DictAPI.getDictItemPage(dictCode.value, queryParams)
.then((data) => {
tableData.value = data.list;
total.value = data.total;
@@ -200,12 +195,12 @@ function handleSelectionChange(selection: any) {
}
//
function handleOpenDialog(row?: DictDataPageVO) {
function handleOpenDialog(row?: DictItemPageVO) {
dialog.visible = true;
dialog.title = row ? "编辑字典数据" : "新增字典数据";
dialog.title = row ? "编辑字典" : "新增字典";
if (row?.id) {
DictDataAPI.getFormData(row.id).then((data) => {
DictAPI.getDictItemFormData(dictCode.value, row.id).then((data) => {
Object.assign(formData, data);
});
}
@@ -219,7 +214,7 @@ function handleSubmitClick() {
const id = formData.id;
formData.dictCode = dictCode.value;
if (id) {
DictDataAPI.update(id, formData)
DictAPI.updateDictItem(dictCode.value, id, formData)
.then(() => {
ElMessage.success("修改成功");
handleCloseDialog();
@@ -227,7 +222,7 @@ function handleSubmitClick() {
})
.finally(() => (loading.value = false));
} else {
DictDataAPI.add(formData)
DictAPI.createDictItem(dictCode.value, formData)
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
@@ -256,8 +251,8 @@ function handleCloseDialog() {
* @param id 字典ID
*/
function handleDelete(id?: number) {
const attrGroupIds = [id || ids.value].join(",");
if (!attrGroupIds) {
const itemIds = [id || ids.value].join(",");
if (!itemIds) {
ElMessage.warning("请勾选删除项");
return;
}
@@ -267,7 +262,7 @@ function handleDelete(id?: number) {
type: "warning",
}).then(
() => {
DictDataAPI.deleteByIds(attrGroupIds).then(() => {
DictAPI.deleteDictItems(dictCode.value, itemIds).then(() => {
ElMessage.success("删除成功");
handleResetQuery();
});

View File

@@ -129,7 +129,7 @@ defineOptions({
inherititems: false,
});
import DictAPI, { DictPageQuery, DictPageVO, DictForm } from "@/api/system/dict";
import DictAPI, { DictPageQuery, DictPageVO, DictForm } from "@/api/system/dict.api";
import router from "@/router";
@@ -221,7 +221,7 @@ function handleSubmitClick() {
})
.finally(() => (loading.value = false));
} else {
DictAPI.add(formData)
DictAPI.create(formData)
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
@@ -270,17 +270,12 @@ function handleDelete(id?: number) {
);
}
// 打开字典数据
// 打开字典
function handleOpenDictData(row: DictPageVO) {
router.push({
path: "/system/dict-data",
path: "/system/dict-item",
query: { dictCode: row.dictCode, title: "【" + row.name + "】字典数据" },
});
/* router.push({
name: "DictData",
params: { dictCode: row.dictCode, title: "【" + row.name + "】字典数据" },
}); */
}
onMounted(() => {

View File

@@ -60,7 +60,7 @@ defineOptions({
inheritAttrs: false,
});
import LogAPI, { LogPageVO, LogPageQuery } from "@/api/system/log";
import LogAPI, { LogPageVO, LogPageQuery } from "@/api/system/log.api";
const queryFormRef = ref();

View File

@@ -335,8 +335,8 @@ defineOptions({
inheritAttrs: false,
});
import MenuAPI, { MenuQuery, MenuForm, MenuVO } from "@/api/system/menu";
import { MenuTypeEnum } from "@/enums/MenuTypeEnum";
import MenuAPI, { MenuQuery, MenuForm, MenuVO } from "@/api/system/menu.api";
import { MenuTypeEnum } from "@/enums/system/menu.enum";
const queryFormRef = ref();
const menuFormRef = ref();
@@ -465,7 +465,7 @@ function handleSubmit() {
handleQuery();
});
} else {
MenuAPI.add(formData.value).then(() => {
MenuAPI.create(formData.value).then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
handleQuery();

View File

@@ -107,7 +107,7 @@ defineOptions({
inheritAttrs: false,
});
import NoticeAPI, { NoticePageVO, NoticePageQuery, NoticeDetailVO } from "@/api/system/notice";
import NoticeAPI, { NoticePageVO, NoticePageQuery, NoticeDetailVO } from "@/api/system/notice.api";
const queryFormRef = ref();
const pageData = ref<NoticePageVO[]>([]);

View File

@@ -259,8 +259,8 @@ import NoticeAPI, {
NoticeForm,
NoticePageQuery,
NoticeDetailVO,
} from "@/api/system/notice";
import UserAPI from "@/api/system/user";
} from "@/api/system/notice.api";
import UserAPI from "@/api/system/user.api";
const queryFormRef = ref();
const dataFormRef = ref();
@@ -389,7 +389,7 @@ function handleSubmit() {
})
.finally(() => (loading.value = false));
} else {
NoticeAPI.add(formData)
NoticeAPI.create(formData)
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();

View File

@@ -208,8 +208,8 @@ defineOptions({
inheritAttrs: false,
});
import RoleAPI, { RolePageVO, RoleForm, RolePageQuery } from "@/api/system/role";
import MenuAPI from "@/api/system/menu";
import RoleAPI, { RolePageVO, RoleForm, RolePageQuery } from "@/api/system/role.api";
import MenuAPI from "@/api/system/menu.api";
const queryFormRef = ref();
const roleFormRef = ref();
@@ -313,7 +313,7 @@ function handleSubmit() {
})
.finally(() => (loading.value = false));
} else {
RoleAPI.add(formData)
RoleAPI.create(formData)
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();

View File

@@ -21,7 +21,7 @@
</template>
<script setup lang="ts">
import DeptAPI from "@/api/system/dept";
import DeptAPI from "@/api/system/dept.api";
const props = defineProps({
modelValue: {
type: [Number],

View File

@@ -90,8 +90,8 @@
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user";
import { ResultEnum } from "@/enums/ResultEnum";
import UserAPI from "@/api/system/user.api";
import { ResultEnum } from "@/enums/api/result.enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {

View File

@@ -237,10 +237,10 @@
</template>
<script setup lang="ts">
import UserAPI, { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user";
import UserAPI, { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user.api";
import DeptAPI from "@/api/system/dept";
import RoleAPI from "@/api/system/role";
import DeptAPI from "@/api/system/dept.api";
import RoleAPI from "@/api/system/role.api";
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
@@ -301,7 +301,7 @@ const roleOptions = ref<OptionType[]>();
const importDialogVisible = ref(false);
// 查询
function handleQuery() {
async function handleQuery() {
loading.value = true;
UserAPI.getPage(queryParams)
.then((data) => {
@@ -395,7 +395,7 @@ const handleSubmit = useDebounceFn(() => {
})
.finally(() => (loading.value = false));
} else {
UserAPI.add(formData)
UserAPI.create(formData)
.then(() => {
ElMessage.success("新增用户成功");
handleCloseDialog();