重构错误处理逻辑,移除旧的错误处理工具,新增 useErrorHandler 组合式 API,优化模态框组件,添加 ID 属性以支持更灵活的 DOM 操作

This commit is contained in:
Chuck1sn
2025-06-16 15:41:09 +08:00
parent ca42fbbda9
commit 772ad547bf
13 changed files with 225 additions and 224 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div tabindex="-1" aria-hidden="true"
<div :id="id" tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full" :class="[maxWidthClass]">
<!-- Modal content -->
@@ -26,7 +26,8 @@
</template>
<script setup lang="ts">
import { computed } from "vue";
import { computed, onMounted } from "vue";
import { initFlowbite, Modal } from "flowbite";
export type ModalSize = "xs" | "sm" | "md" | "lg" | "xl";
@@ -37,6 +38,8 @@ const props = defineProps<{
size?: ModalSize;
/** 关闭模态框的回调函数 */
closeModal: () => void;
/** 模态框ID用于DOM选择 */
id?: string;
}>();
/** 根据size属性计算最大宽度类名 */
@@ -51,4 +54,9 @@ const maxWidthClass = computed(() => {
return sizes[props.size || "md"];
});
// 确保Flowbite初始化这对于PopupModal的正常工作至关重要
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,30 +1,30 @@
<template>
<BaseModal title="部门管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">部门名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
<div class="col-span-full">
<label for="category" class="block mb-2 text-sm font-medium text-gray-900">上级部门</label>
<select id="category" v-model="formData.parentId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option :value="null"></option>
<option v-for="dept in availableDepartments" :key="dept.id" :value="dept.id"
:selected="dept.id === formData.parentId">{{ dept.name }}</option>
</select>
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
<BaseModal :id="id" title="部门管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">部门名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
<div class="col-span-full">
<label for="category" class="block mb-2 text-sm font-medium text-gray-900">上级部门</label>
<select id="category" v-model="formData.parentId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option :value="null"></option>
<option v-for="dept in availableDepartments" :key="dept.id" :value="dept.id"
:selected="dept.id === formData.parentId">{{ dept.name }}</option>
</select>
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
</template>
<script setup lang="ts">
@@ -36,12 +36,14 @@ import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const { department, availableDepartments, onSubmit, closeModal } = defineProps<{
department?: components["schemas"]["Department"];
availableDepartments?: components["schemas"]["Department"][];
closeModal: () => void;
onSubmit: (department: DepartmentUpsertModel) => Promise<void>;
}>();
const { department, availableDepartments, onSubmit, closeModal, id } =
defineProps<{
department?: components["schemas"]["Department"];
availableDepartments?: components["schemas"]["Department"][];
closeModal: () => void;
onSubmit: (department: DepartmentUpsertModel) => Promise<void>;
id?: string;
}>();
const formData = ref<DepartmentUpsertModel>({
name: "",
@@ -92,8 +94,4 @@ const handleSubmit = async () => {
}
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,5 +1,5 @@
<template>
<BaseModal title="大模型配置" size="md" :closeModal="closeModal">
<BaseModal :id="id" title="大模型配置" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
@@ -63,10 +63,11 @@ import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const { llm, onSubmit } = defineProps<{
const { llm, onSubmit, id } = defineProps<{
llm?: components["schemas"]["LlmVm"];
closeModal: () => void;
onSubmit: (data: components["schemas"]["LlmVm"]) => Promise<void>;
id: string;
}>();
// 初始化默认值避免undefined错误
@@ -139,8 +140,4 @@ const handleSubmit = async () => {
await onSubmit(validatedData);
updateFormData(undefined);
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,43 +1,43 @@
<template>
<BaseModal title="权限管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">权限名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
<div class="col-span-full">
<label for="code" class="block mb-2 text-sm font-medium text-gray-900">权限代码</label>
<input type="text" id="code" v-model="formData.code"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
<BaseModal :id="id" title="权限管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">权限名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
<div class="col-span-full">
<label for="code" class="block mb-2 text-sm font-medium text-gray-900">权限代码</label>
<input type="text" id="code" v-model="formData.code"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import useAlertStore from "@/composables/store/useAlertStore";
import type { PermissionUpsertModel } from "@/types/permission";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const alertStore = useAlertStore();
const { permission, onSubmit, closeModal } = defineProps<{
const { permission, onSubmit, closeModal, id } = defineProps<{
permission?: components["schemas"]["PermissionRespDto"];
onSubmit: (data: PermissionUpsertModel) => Promise<void>;
closeModal: () => void;
id: string;
}>();
const formData = ref<PermissionUpsertModel>({

View File

@@ -1,5 +1,5 @@
<template>
<BaseModal :closeModal="closeModal" size="sm">
<BaseModal :id="id" :closeModal="closeModal" size="sm">
<div class="p-5 md:p-6 text-center">
<svg class="w-14 h-14 sm:w-16 sm:h-16 mx-auto text-red-600 mb-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,37 +1,37 @@
<template>
<BaseModal title="岗位管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">岗位名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
<BaseModal :id="id" title="岗位管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">岗位名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import useAlertStore from "@/composables/store/useAlertStore";
import type { PositionUpsertModel } from "@/types/position";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const alertStore = useAlertStore();
const { position, closeModal, onSubmit } = defineProps<{
const { position, closeModal, onSubmit, id } = defineProps<{
position?: components["schemas"]["Position"];
closeModal: () => void;
onSubmit: (data: PositionUpsertModel) => Promise<void>;
id: string;
}>();
const formData = ref<PositionUpsertModel>({
@@ -78,8 +78,4 @@ const handleSubmit = async () => {
}
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,43 +1,43 @@
<template>
<BaseModal title="角色管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">角色名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
<div class="col-span-full">
<label for="code" class="block mb-2 text-sm font-medium text-gray-900">角色代码</label>
<input type="text" id="code" v-model="formData.code"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
<BaseModal :id="id" title="角色管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">角色名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
<div class="col-span-full">
<label for="code" class="block mb-2 text-sm font-medium text-gray-900">角色代码</label>
<input type="text" id="code" v-model="formData.code"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import useAlertStore from "@/composables/store/useAlertStore";
import type { RoleUpsertModel } from "@/types/role";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const alertStore = useAlertStore();
const { role, closeModal, onSubmit } = defineProps<{
const { role, closeModal, onSubmit, id } = defineProps<{
role?: components["schemas"]["RoleRespDto"];
closeModal: () => void;
onSubmit: (data: RoleUpsertModel) => Promise<void>;
id: string;
}>();
const formData = ref<RoleUpsertModel>({
@@ -92,8 +92,4 @@ const handleSubmit = async () => {
}
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,34 +1,34 @@
<template>
<BaseModal title="定时任务配置" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="cronExpression" class="block mb-2 text-sm font-medium text-gray-900">CRON表达式</label>
<input type="text" id="cronExpression" v-model="formData.cronExpression"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
<BaseModal :id="id" title="定时任务配置" size="md" :closeModal="closeModal">
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1">
<div class="col-span-full">
<label for="cronExpression" class="block mb-2 text-sm font-medium text-gray-900">CRON表达式</label>
<input type="text" id="cronExpression" v-model="formData.cronExpression"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5">
保存
</button>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const { job, closeModal, onSubmit } = defineProps<{
const { job, closeModal, onSubmit, id } = defineProps<{
job?: components["schemas"]["JobTriggerDto"];
closeModal: () => void;
onSubmit: (cronExpression: string) => Promise<void>;
id: string;
}>();
const formData = ref({

View File

@@ -1,5 +1,5 @@
<template>
<BaseModal title="用户管理" size="md" :closeModal="closeModal">
<BaseModal :id="id" title="用户管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<form class="p-4 md:p-5">
<div class="space-y-4">
@@ -53,10 +53,11 @@ import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const { user, onSubmit } = defineProps<{
const { user, onSubmit, id } = defineProps<{
user?: components["schemas"]["UserRolePermissionDto"];
closeModal: () => void;
onSubmit: (data: UserUpsertSubmitModel) => Promise<void>;
id: string;
}>();
const formData = ref();

View File

@@ -0,0 +1,72 @@
import useUserAuth from "@/composables/auth/useUserAuth";
import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants";
import {
ForbiddenError,
InternalServerError,
RequestError,
UnAuthError,
ValidationError,
} from "@/types/error";
import { useRouter } from "vue-router";
import { z } from "zod";
/**
* 错误处理 Composable
*/
export function useErrorHandler() {
const router = useRouter();
const { signOut } = useUserAuth();
const alertStore = useAlertStore();
/**
* 处理各类错误,显示对应的提示信息
*/
const handleError = (err: unknown) => {
console.error(err);
try {
if (err instanceof ValidationError) {
alertStore.showAlert({
level: "error",
content: err.message,
});
} else if (err instanceof UnAuthError) {
signOut();
router.push(Routes.LOGIN.path);
alertStore.showAlert({
level: "error",
content: err.message,
});
} else if (err instanceof ForbiddenError || err instanceof RequestError) {
alertStore.showAlert({
level: "error",
content: err.message,
});
} else if (err instanceof InternalServerError) {
alertStore.showAlert({
level: "error",
content: err.detail ?? err.message,
});
} else if (err instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: err.errors[0].message,
});
} else {
alertStore.showAlert({
level: "error",
content: "发生异常,请稍候再试",
});
}
} catch (e) {
console.error("Error handler failed:", e);
}
};
return {
handleError,
};
}
export default useErrorHandler;

View File

@@ -3,12 +3,10 @@ import "./assets/main.css";
import { createPinia } from "pinia";
import { createApp } from "vue";
import App from "./App.vue";
import useUserAuth from "./composables/auth/useUserAuth";
import useAlertStore from "./composables/store/useAlertStore";
import router from "./router";
import makeErrorHandler from "./utils/errorHandler";
import VueDatePicker from "@vuepic/vue-datepicker";
import App from "./App.vue";
import useErrorHandler from "./composables/useErrorHandler";
import router from "./router";
import "@vuepic/vue-datepicker/dist/main.css";
import "./assets/datepicker.css";
@@ -27,11 +25,13 @@ async function enableMocking() {
enableMocking().then(() => {
const app = createApp(App);
app.use(createPinia());
const { signOut } = useUserAuth();
const { showAlert } = useAlertStore();
app.use(router);
const errorHandler = makeErrorHandler(router, signOut, showAlert);
app.config.errorHandler = errorHandler;
const { handleError } = useErrorHandler();
app.config.errorHandler = (err, instance, info) => {
handleError(err);
};
app.component("VueDatePicker", VueDatePicker);
app.mount("#app");
});

View File

@@ -1,67 +0,0 @@
import type { ComponentPublicInstance } from "vue";
import type { Router } from "vue-router";
import {
ForbiddenError,
InternalServerError,
RequestError,
ValidationError,
UnAuthError,
} from "../types/error";
import { z } from "zod";
import { Routes } from "../router/constants";
const makeErrorHandler =
(
router: Router,
signOut: () => void,
showAlert: ({
content,
level,
}: {
content: string;
level: "info" | "success" | "warning" | "error";
}) => void,
) =>
(err: unknown, instance: ComponentPublicInstance | null, info: string) => {
console.error(err);
if (err instanceof ValidationError) {
showAlert({
level: "error",
content: err.message,
});
} else if (err instanceof UnAuthError) {
signOut();
router.push(Routes.LOGIN.path);
showAlert({
level: "error",
content: err.message,
});
} else if (err instanceof ForbiddenError) {
showAlert({
level: "error",
content: err.message,
});
} else if (err instanceof RequestError) {
showAlert({
level: "error",
content: err.message,
});
} else if (err instanceof InternalServerError) {
showAlert({
level: "error",
content: err.detail ?? err.message,
});
} else if (err instanceof z.ZodError) {
showAlert({
level: "error",
content: err.errors[0].message,
});
} else {
showAlert({
level: "error",
content: "发生异常,请稍候再试",
});
}
};
export default makeErrorHandler;

View File

@@ -115,7 +115,7 @@
<RoleDeleteModal :id="'role-delete-modal'" :closeModal="() => {
roleDeleteModal!.hide();
}" :onSubmit="handleDeletedModalSubmit" title="确定删除该角色吗" content="删除角色"></RoleDeleteModal>
<RoleUpsertModal :onSubmit="handleUpsertModalSubmit" :closeModal="() => {
<RoleUpsertModal :id="'role-upsert-modal'" :onSubmit="handleUpsertModalSubmit" :closeModal="() => {
roleUpsertModal!.hide();
}" :role="selectedRole">
</RoleUpsertModal>