重构组件结构,优化导入路径并移除不必要的组件,新增多个模态框组件以支持部门、角色、权限和用户管理功能

This commit is contained in:
Chuck1sn
2025-06-16 15:09:52 +08:00
parent 28eed04823
commit ca42fbbda9
51 changed files with 2980 additions and 2881 deletions

View File

@@ -1,13 +1,11 @@
<script setup lang="ts">
import { RouterLink, RouterView } from "vue-router";
import Alert from "./components/Alert.vue";
import { RouterView } from "vue-router";
import Alert from "./components/ui/Alert.vue";
</script>
<template>
<RouterView />
<Alert/>
<Alert />
</template>
<style scoped>
</style>
<style scoped></style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,100 +0,0 @@
<template>
<!-- Main modal -->
<div :id="department?.id ? 'department-update-modal' : 'department-create-modal'" 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 max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
部门管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- 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>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
import type { DepartmentUpsertModel } from "../types/department";
const { department, availableDepartments, onSubmit } = defineProps<{
department?: components["schemas"]["Department"];
availableDepartments?: components["schemas"]["Department"][];
closeModal: () => void;
onSubmit: (department: DepartmentUpsertModel) => Promise<void>;
}>();
const formData = ref();
const updateFormData = (newDepartment: typeof department) => {
formData.value = {
id: newDepartment?.id,
name: newDepartment?.name,
parentId: newDepartment?.parentId,
};
};
watch(() => department, updateFormData, { immediate: true });
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
parentId: z.number().nullable().optional(),
name: z
.string({
message: "部门名称不能为空",
})
.min(2, "部门名称至少2个字符")
.max(15, "部门名称最多15个字符"),
});
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,141 +0,0 @@
<template>
<!-- Main modal -->
<div :id="llm?.id ? 'llm-update-modal' : 'llm-create-modal'" 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 max-w-sm sm:max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
大模型管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<form class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1 sm:grid-cols-2">
<div class="col-span-full sm:col-span-2">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">名称</label>
<input type="text" name="名称" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true">
</div>
<div class="col-span-full sm:col-span-2">
<label for="modelName" class="block mb-2 text-sm font-medium autocomplete text-gray-900">模型名称</label>
<input type="text" id="modelName" autocomplete="new-password" v-model="formData.modelName"
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 sm:col-span-1">
<label for="type" class="block mb-2 text-sm font-medium text-gray-900">类型</label>
<select id="type" v-model="formData.type"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
<option :value="'CHAT'">聊天</option>
<option :value="'EMBEDDING'">嵌入</option>
</select>
</div>
<div class="col-span-full sm:col-span-1">
<label for="status" class="block mb-2 text-sm font-medium text-gray-900">状态</label>
<select id="status" v-model="formData.enable"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
<option :value=true>启用</option>
<option :value=false>禁用</option>
</select>
</div>
<div class="col-span-full sm:col-span-2">
<label for="apiKey" class="block mb-2 text-sm font-medium autocomplete text-gray-900">apiKey</label>
<input type="text" id="apiKey" autocomplete="new-password" v-model="formData.apiKey"
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 sm:col-span-2">
<label for="url" class="block mb-2 text-sm font-medium text-gray-900">url</label>
<input type="text" id="url" autocomplete="new-password" v-model="formData.url"
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 sm:col-span-2">
<label for="priority" class="block mb-2 text-sm font-medium autocomplete text-gray-900">优先级</label>
<input type="number" id="priority" autocomplete="new-password" v-model="formData.priority"
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.prevent="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>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
const { llm, onSubmit } = defineProps<{
llm?: components["schemas"]["LlmVm"];
closeModal: () => void;
onSubmit: (data: components["schemas"]["LlmVm"]) => Promise<void>;
}>();
const formData = ref();
const updateFormData = (newLlm: typeof llm) => {
formData.value = {
...newLlm,
};
};
watch(() => llm, updateFormData, {
immediate: true,
});
const handleSubmit = async () => {
const llmSchema = z.object({
id: z.number({
message: "id不能为空",
}),
name: z.string({
message: "名称不能为空",
}),
modelName: z.string({
message: "模型名称不能为空",
}),
apiKey: z.string({
message: "apiKey不能为空",
}),
url: z.string({
message: "url不能为空",
}),
enable: z.boolean({
message: "状态不能为空",
}),
priority: z.number({
message: "优先级必须为数字",
}),
type: z.string({
message: "类型不能为空",
}),
});
const validatedData = llmSchema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,90 +0,0 @@
<template>
<div :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 max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow-sm">
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
权限管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<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" name="权限名称" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true">
</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.prevent="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>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { PermissionUpsertModel } from "@/types/permission";
import { ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
const { permission, onSubmit, closeModal } = defineProps<{
id: string;
permission?: components["schemas"]["PermissionRespDto"];
closeModal: () => void;
onSubmit: (data: PermissionUpsertModel) => Promise<void>;
}>();
const formData = ref();
const updateFormData = (newPermission: typeof permission) => {
formData.value = {
id: newPermission?.id,
name: newPermission?.name,
code: newPermission?.code,
};
};
watch(() => permission, updateFormData, { immediate: true });
const handleSubmit = async () => {
const permissionSchema = z.object({
id: z.number().optional(),
name: z
.string({
message: "权限名称不能为空",
})
.min(2, "权限名称至少2个字符")
.max(15, "权限名称最多15个字符"),
code: z
.string({
message: "权限代码不能为空",
})
.min(2, "权限代码至少2个字符")
.max(15, "权限代码最多15个字符"),
});
const validatedData = permissionSchema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
</script>

View File

@@ -1,52 +0,0 @@
<template>
<div :id tabindex="-1"
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 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow">
<button type="button" @click="closeModal"
class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 class="mb-4 text-base sm:text-lg font-medium text-gray-800">
{{ title }}
</h3>
<div class="flex justify-center items-center space-x-3 sm:space-x-4">
<button type="button" @click="onSubmit"
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center min-w-[80px]">
</button>
<button type="button" @click="closeModal"
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 min-w-[80px]"></button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { initFlowbite } from "flowbite";
import { onMounted } from "vue";
const { title, id, closeModal, onSubmit } = defineProps<{
title: string;
id: string;
closeModal: () => void;
onSubmit: () => Promise<void>;
}>();
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,89 +0,0 @@
<template>
<!-- Main modal -->
<div :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 max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
岗位管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- 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>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
import type { PositionUpsertModel } from "../types/position";
const alertStore = useAlertStore();
const { id, position, onSubmit } = defineProps<{
id: string;
position?: components["schemas"]["Position"];
closeModal: () => void;
onSubmit: (position: PositionUpsertModel) => Promise<void>;
}>();
const formData = ref();
const updateFormData = (newPosition: typeof position) => {
formData.value = {
id: newPosition?.id,
name: newPosition?.name,
};
};
watch(() => position, updateFormData, { immediate: true });
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
name: z
.string({
message: "岗位名称不能为空",
})
.min(2, "岗位名称至少2个字符")
.max(15, "岗位名称最多15个字符"),
});
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,97 +0,0 @@
<template>
<div id="role-upsert-modal" 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 max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow-sm">
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
角色管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<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" name="角色名称" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true">
</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.prevent="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>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import type { RoleUpsertModel } from "@/types/role";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
const alertStore = useAlertStore();
const { role, onSubmit } = defineProps<{
role?: components["schemas"]["RoleRespDto"];
closeModal: () => void;
onSubmit: (data: RoleUpsertModel) => Promise<void>;
}>();
const formData = ref();
const updateFormData = (newRole: typeof role) => {
formData.value = {
id: newRole?.id,
name: newRole?.name,
code: newRole?.code,
};
};
watch(() => role, updateFormData, { immediate: true });
const handleSubmit = async () => {
const roleSchema = z.object({
id: z.number().optional(),
name: z
.string({
message: "角色名称不能为空",
})
.min(2, "角色名称至少2个字符")
.max(15, "角色名称最多15个字符"),
code: z
.string({
message: "角色代码不能为空",
})
.min(2, "角色代码至少2个字符")
.max(15, "角色代码最多15个字符"),
});
const validatedData = roleSchema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -1,75 +0,0 @@
<template>
<div :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 max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow">
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
{{ '更新表达式' }}
</h3>
<button @click="closeModal" type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
</button>
</div>
<form @submit.prevent="handleSubmit" class="p-4 md:p-5">
<div class="mb-4">
<div>
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">Cron 表达式</label>
<input type="text" v-model="formData.cronExpression" name="name" id="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">
</div>
</div>
<button type="submit"
class="w-full sm:w-auto text-white inline-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-xs sm:text-sm px-4 py-2 sm:px-5 sm:py-2.5 text-center">
提交
</button>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import type { components } from "@/api/types/schema";
import { ref, watch } from "vue";
import { z } from "zod";
const { job, closeModal, onSubmit } = defineProps<{
id: string;
job?: components["schemas"]["JobTriggerDto"];
closeModal: () => void;
onSubmit: (cronExpression: string) => Promise<void>;
}>();
const alertStore = useAlertStore();
const formData = ref();
watch(
() => job,
(newJob) => {
formData.value = {
cronExpression: newJob?.cronExpression,
};
},
{ immediate: true },
);
const handleSubmit = async () => {
const jobSchema = z.object({
cronExpression: z
.string({
message: "表达式不可为空",
})
.min(5, "表达式的长度非法"),
});
const validatedData = jobSchema.parse(formData.value);
await onSubmit(validatedData.cronExpression);
};
</script>

View File

@@ -1,195 +0,0 @@
<template>
<!-- Main modal -->
<div 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 max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
用户管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<form class="p-4 md:p-5">
<div class="space-y-4">
<div class="w-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">用户名</label>
<input type="text" name="用户名" id="name" v-model="formData.username"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true">
</div>
<div class="w-full">
<label for="password" class="block mb-2 text-sm font-medium text-gray-900">密码</label>
<input type="password" id="password" autocomplete="new-password" v-model="formData.password"
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"
placeholder="编辑时非必填" required />
</div>
<div class="w-full">
<label for="confirm_password" class="block mb-2 text-sm font-medium text-gray-900">确认密码</label>
<input type="password" id="confirm_password" autocomplete="new-password"
v-model="formData.confirmPassword"
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 placeholder="编辑时非必填" />
</div>
<label class="block mb-2 text-sm font-medium text-gray-900" for="file_input">上传头像</label>
<input
class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none"
id="file_input" type="file" accept="image/*" @change="handleFileChange">
<div class="w-full">
<label for="status" class="block mb-2 text-sm font-medium text-gray-900">状态</label>
<select id="status" v-model="formData.enable"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
<option :value=true>启用</option>
<option :value=false>禁用</option>
</select>
</div>
</div>
<button type="submit" @click.prevent="handleSubmit" :disabled="uploadLoading"
class="mt-5 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center disabled:opacity-50 disabled:cursor-not-allowed">
保存
</button>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useUserUpsert } from "@/composables/user/useUserUpsert";
import type { UserUpsertSubmitModel } from "@/types/user";
import Compressor from "compressorjs";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
import { ValidationError } from "@/types/error";
import useAlertStore from "@/composables/store/useAlertStore";
const { user, onSubmit } = defineProps<{
user?: components["schemas"]["UserRolePermissionDto"];
closeModal: () => void;
onSubmit: (data: UserUpsertSubmitModel) => Promise<void>;
}>();
const formData = ref();
const { uploadUserAvatar } = useUserUpsert();
const { showAlert } = useAlertStore();
const uploadLoading = ref(false);
const updateFormData = (newUser: typeof user) => {
formData.value = {
id: newUser?.id,
username: newUser?.username,
password: undefined,
avatar: newUser?.avatar ?? undefined,
enable: newUser?.enable ?? true,
confirmPassword: undefined,
};
};
watch(() => user, updateFormData, {
immediate: true,
});
const validateFile = (file?: File) => {
if (!file) {
throw new ValidationError("您未选择文件");
}
const allowedTypes = ["image/jpeg", "image/png"];
if (!allowedTypes.includes(file.type)) {
throw new ValidationError("不支持的文件类型");
}
const maxSize = 200 * 1024; // 200KB
if (file.size > maxSize) {
throw new ValidationError("文件大小超过限制(200KB)");
}
};
const handleFileChange = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
uploadLoading.value = true;
try {
validateFile(file);
new Compressor(file!, {
quality: 0.8, // 压缩质量0-1之间
maxWidth: 800, // 最大宽度
maxHeight: 800, // 最大高度
mimeType: "auto", // 自动选择最佳格式
success: async (compressedFile: File) => {
formData.value.avatar = await uploadUserAvatar(compressedFile);
uploadLoading.value = false;
showAlert({
content: "上传成功",
level: "success",
});
},
error: (err: Error) => {
throw err;
},
});
} catch (error) {
(event.target as HTMLInputElement).value = "";
uploadLoading.value = false;
throw error;
}
};
const handleSubmit = async () => {
const userSchema = z
.object({
id: z.number().optional(),
avatar: z.string().optional(),
username: z
.string({
message: "用户名不能为空",
})
.min(1, "用户名至少1个字符")
.max(15, "用户名最多15个字符"),
enable: z.boolean({
message: "状态不能为空",
}),
password: z
.string({
message: "密码不能为空",
})
.min(5, "密码至少5个字符")
.max(20, "密码最多20个字符")
.optional(),
confirmPassword: z
.string({
message: "密码不能为空",
})
.min(5, "密码至少5个字符")
.max(20, "密码最多20个字符")
.optional(),
})
.refine(
(data) => {
if (!data.password) return true;
return data.password === data.confirmPassword;
},
{
message: "密码输入不一致。",
},
);
const validatedData = userSchema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -91,29 +91,29 @@
</template>
<script setup lang="ts">
import Avatar from "@/components/Avatar.vue";
import InputButton from "@/components/InputButton.vue";
import UserDeleteModal from "@/components/PopupModal.vue";
import DepartmentDeleteModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import LoadingIcon from "@/components/icons/LoadingIcon.vue";
import { LoadingIcon } from "@/components/icons";
import DepartmentUpsertModal from "@/components/modals/DepartmentUpsertModal.vue";
import UserDeleteModal from "@/components/modals/PopupModal.vue";
import DepartmentDeleteModal from "@/components/modals/PopupModal.vue";
import UserUpsertModal from "@/components/modals/UserUpsertModal.vue";
import TableButton from "@/components/tables/TableButton.vue";
import Avatar from "@/components/ui/Avatar.vue";
import InputButton from "@/components/ui/InputButton.vue";
import { useAiAction } from "@/composables/ai/useAiAction";
import { useAiChat } from "@/composables/ai/useAiChat";
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
import { useDepartmentUpsert } from "@/composables/department/useDepartmentUpsert";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import useUserStore from "@/composables/store/useUserStore";
import { useUserUpsert } from "@/composables/user/useUserUpsert";
import type { DepartmentUpsertModel } from "@/types/department";
import type { UserUpsertSubmitModel } from "@/types/user";
import DOMPurify from "dompurify";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { marked } from "marked";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { z } from "zod";
import DepartmentUpsertModal from "../components/DepartmentUpsertModal.vue";
import UserUpsertModal from "../components/UserUpsertModal.vue";
import { useAiChat } from "../composables/ai/useAiChat";
import useUserStore from "../composables/store/useUserStore";
import { useUserUpsert } from "../composables/user/useUserUpsert";
import type { UserUpsertSubmitModel } from "../types/user";
const {
messages,

View File

@@ -0,0 +1,42 @@
<template>
<div class="w-full">
<label :for="id" class="block mb-2 text-sm font-medium text-gray-900">{{ label }}</label>
<input :type="type" :id="id" :name="name" :value="modelValue"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)" :placeholder="placeholder"
:required="required" :autocomplete="autocomplete" :class="[
'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5',
error ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : 'focus:ring-blue-500 focus:border-blue-500'
]" />
<p v-if="error" class="mt-1 text-sm text-red-600">{{ error }}</p>
<p v-if="hint && !error" class="mt-1 text-sm text-gray-500">{{ hint }}</p>
</div>
</template>
<script setup lang="ts">
defineProps<{
/** 输入框标签 */
label: string;
/** 输入框ID */
id: string;
/** 输入框名称 */
name?: string;
/** 输入框类型 */
type?: string;
/** 输入框占位符 */
placeholder?: string;
/** 是否必填 */
required?: boolean;
/** 自动完成属性 */
autocomplete?: string;
/** 输入框值 */
modelValue: string;
/** 错误信息 */
error?: string;
/** 提示信息 */
hint?: string;
}>();
defineEmits<{
"update:modelValue": [value: string];
}>();
</script>

View File

@@ -0,0 +1,40 @@
<template>
<div class="w-full">
<label :for="id" class="block mb-2 text-sm font-medium text-gray-900">{{ label }}</label>
<select :id="id" :name="name" :value="modelValue"
@change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)" :required="required" :class="[
'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5',
error ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : 'focus:ring-blue-500 focus:border-blue-500'
]">
<option v-if="placeholder" value="" disabled>{{ placeholder }}</option>
<slot></slot>
</select>
<p v-if="error" class="mt-1 text-sm text-red-600">{{ error }}</p>
<p v-if="hint && !error" class="mt-1 text-sm text-gray-500">{{ hint }}</p>
</div>
</template>
<script setup lang="ts">
defineProps<{
/** 选择框标签 */
label: string;
/** 选择框ID */
id: string;
/** 选择框名称 */
name?: string;
/** 选择框占位符 */
placeholder?: string;
/** 是否必填 */
required?: boolean;
/** 选择框值 */
modelValue: string | number | boolean;
/** 错误信息 */
error?: string;
/** 提示信息 */
hint?: string;
}>();
defineEmits<{
"update:modelValue": [value: string | number | boolean];
}>();
</script>

View File

@@ -0,0 +1,13 @@
// 统一导出所有图标组件
export { default as StopIcon } from "./StopIcon.vue";
export { default as PlusIcon } from "./PlusIcon.vue";
export { default as AiChatIcon } from "./AiChatIcon.vue";
export { default as PositionIcon } from "./PositionIcon.vue";
export { default as SchedulerIcon } from "./SchedulerIcon.vue";
export { default as DepartmentIcon } from "./DepartmentIcon.vue";
export { default as LlmConfigIcon } from "./LlmConfigIcon.vue";
export { default as LoadingIcon } from "./LoadingIcon.vue";
export { default as RoleIcon } from "./RoleIcon.vue";
export { default as SettingsIcon } from "./SettingsIcon.vue";
export { default as UsersIcon } from "./UsersIcon.vue";
export { default as PermissionIcon } from "./PermissionIcon.vue";

View File

@@ -13,7 +13,7 @@
<script setup lang="ts">
import { ref } from "vue";
import { RouterView } from "vue-router";
import Assistant from "./Assistant.vue";
import Assistant from "@/components/common/Assistant.vue";
import Headbar from "./Headbar.vue";
import Sidebar from "./Sidebar.vue";

View File

@@ -88,14 +88,14 @@
</nav>
</template>
<script setup lang="ts">
import { AiChatIcon } from "@/components/icons";
import Avatar from "@/components/ui/Avatar.vue";
import useUserAuth from "@/composables/auth/useUserAuth";
import useUserStore from "@/composables/store/useUserStore";
import { Routes } from "@/router/constants";
import { Dropdown, type DropdownInterface, initFlowbite } from "flowbite";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import useUserAuth from "../composables/auth/useUserAuth";
import { Routes } from "../router/constants";
import Avatar from "./Avatar.vue";
import AiChatIcon from "./icons/AiChatIcon.vue";
const props = defineProps<{
changeAssistantVisible: () => void;

View File

@@ -33,14 +33,16 @@ import { initFlowbite } from "flowbite";
import { onMounted, ref } from "vue";
import { RouterLink, useRoute } from "vue-router";
import DepartmentIcon from "./icons/DepartmentIcon.vue";
import LlmConfigIcon from "./icons/LlmConfigIcon.vue";
import PermissionIcon from "./icons/PermissionIcon.vue";
import PositionIcon from "./icons/PositionIcon.vue";
import RoleIcon from "./icons/RoleIcon.vue";
import SchedulerIcon from "./icons/SchedulerIcon.vue";
import SettingsIcon from "./icons/SettingsIcon.vue";
import UsersIcon from "./icons/UsersIcon.vue";
import {
DepartmentIcon,
LlmConfigIcon,
PermissionIcon,
PositionIcon,
RoleIcon,
SchedulerIcon,
SettingsIcon,
UsersIcon,
} from "@/components/icons";
const isDrawerVisible = ref(false);
const emit = defineEmits(["menu-click"]);

View File

@@ -0,0 +1,54 @@
<template>
<div 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 -->
<div class="relative bg-white rounded-lg shadow">
<!-- Modal header -->
<div v-if="title" class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900">
{{ title }}
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">关闭</span>
</button>
</div>
<!-- Modal body -->
<slot></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
export type ModalSize = "xs" | "sm" | "md" | "lg" | "xl";
const props = defineProps<{
/** 模态框标题 */
title?: string;
/** 模态框大小 */
size?: ModalSize;
/** 关闭模态框的回调函数 */
closeModal: () => void;
}>();
/** 根据size属性计算最大宽度类名 */
const maxWidthClass = computed(() => {
const sizes: Record<ModalSize, string> = {
xs: "max-w-xs",
sm: "max-w-sm",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
};
return sizes[props.size || "md"];
});
</script>

View File

@@ -0,0 +1,99 @@
<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>
</template>
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import useAlertStore from "@/composables/store/useAlertStore";
import type { DepartmentUpsertModel } from "@/types/department";
import { initFlowbite } from "flowbite";
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 formData = ref<DepartmentUpsertModel>({
name: "",
parentId: null,
});
const updateFormData = (newDepartment: typeof department) => {
if (!newDepartment) {
formData.value = {
name: "",
parentId: null,
};
return;
}
formData.value = {
id: newDepartment.id,
name: newDepartment.name ?? "",
parentId: newDepartment.parentId,
};
};
watch(() => department, updateFormData, { immediate: true });
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
name: z
.string({
message: "部门名称不能为空",
})
.min(2, "部门名称至少2个字符")
.max(15, "部门名称最多15个字符"),
parentId: z.number().nullable().optional(),
});
try {
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
} catch (error) {
if (error instanceof z.ZodError) {
const alertStore = useAlertStore();
alertStore.showAlert({
content: error.errors[0].message,
level: "error",
});
}
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,146 @@
<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="modelName" class="block mb-2 text-sm font-medium text-gray-900">模型名称</label>
<input type="text" id="modelName" v-model="formData.modelName"
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="url" class="block mb-2 text-sm font-medium text-gray-900">API地址</label>
<input type="text" id="url" v-model="formData.url"
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="apiKey" class="block mb-2 text-sm font-medium text-gray-900">秘钥</label>
<input type="text" id="apiKey" v-model="formData.apiKey"
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" />
</div>
<div class="col-span-full">
<label for="type" class="block mb-2 text-sm font-medium text-gray-900">类型</label>
<select id="type" v-model="formData.type"
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="CHAT">聊天</option>
<option value="EMBEDDING">嵌入</option>
</select>
</div>
<div class="col-span-full">
<label for="priority" class="block mb-2 text-sm font-medium text-gray-900">优先级</label>
<input type="number" id="priority" v-model="formData.priority"
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" />
</div>
<div class="col-span-full">
<label for="enable" class="block mb-2 text-sm font-medium text-gray-900">状态</label>
<select id="enable" v-model="formData.enable"
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="true">启用</option>
<option :value="false">禁用</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">
import type { components } from "@/api/types/schema";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const { llm, onSubmit } = defineProps<{
llm?: components["schemas"]["LlmVm"];
closeModal: () => void;
onSubmit: (data: components["schemas"]["LlmVm"]) => Promise<void>;
}>();
// 初始化默认值避免undefined错误
const formData = ref<components["schemas"]["LlmVm"]>({
id: 0,
name: "",
modelName: "",
apiKey: "",
url: "",
enable: true,
priority: 0,
type: "CHAT",
});
const updateFormData = (newLlm: typeof llm) => {
if (!newLlm) {
formData.value = {
id: 0,
name: "",
modelName: "",
apiKey: "",
url: "",
enable: true,
priority: 0,
type: "CHAT",
};
return;
}
formData.value = {
id: newLlm.id ?? 0,
name: newLlm.name ?? "",
modelName: newLlm.modelName ?? "",
apiKey: newLlm.apiKey ?? "",
url: newLlm.url ?? "",
enable: newLlm.enable ?? true,
priority: newLlm.priority ?? 0,
type: newLlm.type ?? "CHAT",
};
};
watch(() => llm, updateFormData, { immediate: true });
const handleSubmit = async () => {
const configSchema = z.object({
id: z.number(),
name: z
.string({
message: "名称不能为空",
})
.min(1, "名称至少1个字符")
.max(15, "名称最多15个字符"),
modelName: z
.string({
message: "模型名称不能为空",
})
.min(1, "模型名称至少1个字符"),
apiKey: z.string(),
url: z
.string({
message: "API地址不能为空",
})
.min(1, "API地址至少1个字符"),
enable: z.boolean(),
priority: z.number(),
type: z.string(),
});
const validatedData = configSchema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,95 @@
<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>
</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 { z } from "zod";
import BaseModal from "./BaseModal.vue";
const alertStore = useAlertStore();
const { permission, onSubmit, closeModal } = defineProps<{
permission?: components["schemas"]["PermissionRespDto"];
onSubmit: (data: PermissionUpsertModel) => Promise<void>;
closeModal: () => void;
}>();
const formData = ref<PermissionUpsertModel>({
name: "",
code: "",
});
const updateFormData = (newPermission: typeof permission) => {
if (!newPermission) {
formData.value = {
name: "",
code: "",
};
return;
}
formData.value = {
id: newPermission.id,
name: newPermission.name ?? "",
code: newPermission.code ?? "",
};
};
watch(() => permission, updateFormData, { immediate: true });
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
name: z
.string({
message: "权限名称不能为空",
})
.min(2, "权限名称至少2个字符")
.max(15, "权限名称最多15个字符"),
code: z
.string({
message: "权限代码不能为空",
})
.min(2, "权限代码至少2个字符")
.max(50, "权限代码最多50个字符"),
});
try {
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
content: error.errors[0].message,
level: "error",
});
}
}
};
</script>

View File

@@ -0,0 +1,33 @@
<template>
<BaseModal :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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<h3 class="mb-4 text-base sm:text-lg font-medium text-gray-800">
{{ title }}
</h3>
<div class="flex justify-center items-center space-x-3 sm:space-x-4">
<button type="button" @click="onSubmit"
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center min-w-[80px]">
</button>
<button type="button" @click="closeModal"
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 min-w-[80px]"></button>
</div>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import BaseModal from "./BaseModal.vue";
const { title, id, closeModal, onSubmit } = defineProps<{
title: string;
id: string;
closeModal: () => void;
onSubmit: () => Promise<void>;
}>();
</script>

View File

@@ -0,0 +1,85 @@
<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>
</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 { z } from "zod";
import BaseModal from "./BaseModal.vue";
const alertStore = useAlertStore();
const { position, closeModal, onSubmit } = defineProps<{
position?: components["schemas"]["Position"];
closeModal: () => void;
onSubmit: (data: PositionUpsertModel) => Promise<void>;
}>();
const formData = ref<PositionUpsertModel>({
name: "",
});
const updateFormData = (newPosition: typeof position) => {
if (!newPosition) {
formData.value = {
name: "",
};
return;
}
formData.value = {
id: newPosition.id,
name: newPosition.name ?? "",
};
};
watch(() => position, updateFormData, { immediate: true });
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
name: z
.string({
message: "岗位名称不能为空",
})
.min(2, "岗位名称至少2个字符")
.max(15, "岗位名称最多15个字符"),
});
try {
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
content: error.errors[0].message,
level: "error",
});
}
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,99 @@
<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>
</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 { z } from "zod";
import BaseModal from "./BaseModal.vue";
const alertStore = useAlertStore();
const { role, closeModal, onSubmit } = defineProps<{
role?: components["schemas"]["RoleRespDto"];
closeModal: () => void;
onSubmit: (data: RoleUpsertModel) => Promise<void>;
}>();
const formData = ref<RoleUpsertModel>({
name: "",
code: "",
});
const updateFormData = (newRole: typeof role) => {
if (!newRole) {
formData.value = {
name: "",
code: "",
};
return;
}
formData.value = {
id: newRole.id,
name: newRole.name ?? "",
code: newRole.code ?? "",
};
};
watch(() => role, updateFormData, { immediate: true });
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
name: z
.string({
message: "角色名称不能为空",
})
.min(2, "角色名称至少2个字符")
.max(15, "角色名称最多15个字符"),
code: z
.string({
message: "角色代码不能为空",
})
.min(2, "角色代码至少2个字符")
.max(15, "角色代码最多15个字符"),
});
try {
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
content: error.errors[0].message,
level: "error",
});
}
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,65 @@
<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>
</template>
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const { job, closeModal, onSubmit } = defineProps<{
job?: components["schemas"]["JobTriggerDto"];
closeModal: () => void;
onSubmit: (cronExpression: string) => Promise<void>;
}>();
const formData = ref({
cronExpression: "",
});
const updateFormData = (newJob: typeof job) => {
if (!newJob) {
formData.value = {
cronExpression: "",
};
return;
}
formData.value = {
cronExpression: newJob.cronExpression || "",
};
};
watch(() => job, updateFormData, { immediate: true });
const handleSubmit = async () => {
const schema = z.object({
cronExpression: z
.string({
message: "CRON表达式不能为空",
})
.min(1, "CRON表达式不能为空"),
});
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData.cronExpression);
};
</script>

View File

@@ -0,0 +1,168 @@
<template>
<BaseModal title="用户管理" size="md" :closeModal="closeModal">
<!-- Modal body -->
<form class="p-4 md:p-5">
<div class="space-y-4">
<div class="w-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">用户名</label>
<input type="text" name="用户名" id="name" v-model="formData.username"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true">
</div>
<div class="w-full">
<label for="password" class="block mb-2 text-sm font-medium text-gray-900">密码</label>
<input type="password" id="password" autocomplete="new-password" v-model="formData.password"
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"
placeholder="编辑时非必填" required />
</div>
<div class="w-full">
<label for="confirm_password" class="block mb-2 text-sm font-medium text-gray-900">确认密码</label>
<input type="password" id="confirm_password" autocomplete="new-password" v-model="formData.confirmPassword"
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 placeholder="编辑时非必填" />
</div>
<label class="block mb-2 text-sm font-medium text-gray-900" for="file_input">上传头像</label>
<input
class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none"
id="file_input" type="file" accept="image/*" @change="handleFileChange">
<div class="w-full">
<label for="status" class="block mb-2 text-sm font-medium text-gray-900">状态</label>
<select id="status" v-model="formData.enable"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
<option :value=true>启用</option>
<option :value=false>禁用</option>
</select>
</div>
</div>
<button type="submit" @click.prevent="handleSubmit" :disabled="uploadLoading"
class="mt-5 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center disabled:opacity-50 disabled:cursor-not-allowed">
保存
</button>
</form>
</BaseModal>
</template>
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import useAlertStore from "@/composables/store/useAlertStore";
import { useUserUpsert } from "@/composables/user/useUserUpsert";
import { ValidationError } from "@/types/error";
import type { UserUpsertSubmitModel } from "@/types/user";
import Compressor from "compressorjs";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import BaseModal from "./BaseModal.vue";
const { user, onSubmit } = defineProps<{
user?: components["schemas"]["UserRolePermissionDto"];
closeModal: () => void;
onSubmit: (data: UserUpsertSubmitModel) => Promise<void>;
}>();
const formData = ref();
const { uploadUserAvatar } = useUserUpsert();
const { showAlert } = useAlertStore();
const uploadLoading = ref(false);
const updateFormData = (newUser: typeof user) => {
formData.value = {
id: newUser?.id,
username: newUser?.username,
password: undefined,
avatar: newUser?.avatar ?? undefined,
enable: newUser?.enable ?? true,
confirmPassword: undefined,
};
};
watch(() => user, updateFormData, {
immediate: true,
});
const validateFile = (file?: File) => {
if (!file) {
throw new ValidationError("您未选择文件");
}
const allowedTypes = ["image/jpeg", "image/png"];
if (!allowedTypes.includes(file.type)) {
throw new ValidationError("不支持的文件类型");
}
const maxSize = 200 * 1024; // 200KB
if (file.size > maxSize) {
throw new ValidationError("文件大小超过限制(200KB)");
}
};
const handleFileChange = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0];
uploadLoading.value = true;
try {
validateFile(file);
new Compressor(file!, {
quality: 0.8, // 压缩质量0-1之间
maxWidth: 800, // 最大宽度
maxHeight: 800, // 最大高度
mimeType: "auto", // 自动选择最佳格式
success: async (compressedFile: File) => {
formData.value.avatar = await uploadUserAvatar(compressedFile);
uploadLoading.value = false;
showAlert({
content: "上传成功",
level: "success",
});
},
error: (err: Error) => {
throw err;
},
});
} catch (error) {
(event.target as HTMLInputElement).value = "";
uploadLoading.value = false;
throw error;
}
};
const handleSubmit = async () => {
const userSchema = z
.object({
id: z.number().optional(),
avatar: z.string().optional(),
username: z
.string({
message: "用户名不能为空",
})
.min(1, "用户名至少1个字符")
.max(15, "用户名最多15个字符"),
enable: z.boolean({
message: "状态不能为空",
}),
password: z
.string({
message: "密码不能为空",
})
.min(5, "密码至少5个字符")
.max(20, "密码最多20个字符")
.optional(),
confirmPassword: z
.string({
message: "密码不能为空",
})
.min(5, "密码至少5个字符")
.max(20, "密码最多20个字符")
.optional(),
})
.refine(
(data) => {
if (!data.password) return true;
return data.password === data.confirmPassword;
},
{
message: "密码输入不一致。",
},
);
const validatedData = userSchema.parse(formData.value);
await onSubmit(validatedData);
updateFormData(undefined);
};
</script>

View File

@@ -15,8 +15,8 @@
</template>
<script setup lang="ts">
import { StopIcon } from "@/components/icons";
import { computed } from "vue";
import StopIcon from "./icons/StopIcon.vue";
export type ButtonVariant =
| "primary"

View File

@@ -35,6 +35,8 @@
<script setup lang="ts">
import useAlertStore from "../composables/store/useAlertStore";
import { onMounted, onUnmounted, ref } from "vue";
import useAlertStore from "@/composables/store/useAlertStore";
import type { AlertLevel } from "@/types/alert";
const alertStore = useAlertStore();
</script>

View File

@@ -1,8 +1,8 @@
import type { RouteRecordRaw } from "vue-router";
import Dashboard from "../../components/Dashboard.vue";
import { EPermission, ERole, Routes } from "../constants";
import { EPermission, Routes } from "../constants";
import aiRoutes from "./ai";
import userManagementRoutes from "./user";
import Dashboard from "../../components/layout/Dashboard.vue";
const dashboardRoutes: RouteRecordRaw = {
path: Routes.DASHBOARD.path,

View File

@@ -88,23 +88,23 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import BindModal from "@/components/modals/PopupModal.vue";
import UnModal from "@/components/modals/PopupModal.vue";
import { useDepartmentBind } from "@/composables/department/useDepartmentBind";
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useDepartmentBind } from "../composables/department/useDepartmentBind";
import useAlertStore from "../composables/store/useAlertStore";
// 定义筛选配置
const filterConfig: FilterItem[] = [

View File

@@ -89,23 +89,24 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import BindModal from "@/components/modals/PopupModal.vue";
import UnModal from "@/components/modals/PopupModal.vue";
import { usePermissionBind } from "@/composables/permission/usePermissionBind";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { usePermissionBind } from "../composables/permission/usePermissionBind";
import usePermissionsQuery from "../composables/permission/usePermissionQuery";
import useAlertStore from "../composables/store/useAlertStore";
import usePermissionsQuery from "@/composables/permission/usePermissionQuery";
// 定义筛选配置
const filterConfig: FilterItem[] = [

View File

@@ -80,24 +80,23 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import BindModal from "@/components/modals/PopupModal.vue";
import UnModal from "@/components/modals/PopupModal.vue";
import { usePositionBind } from "@/composables/position/usePositionBind";
import { usePositionQuery } from "@/composables/position/usePositionQuery";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import { useMobileStyles } from "@/composables/useMobileStyles";
import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import useAlertStore from "../composables/store/useAlertStore";
// 定义筛选配置
const filterConfig: FilterItem[] = [

View File

@@ -90,23 +90,24 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useRolesQuery } from "@/composables/role/useRolesQuery";
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import BindModal from "@/components/modals/PopupModal.vue";
import UnModal from "@/components/modals/PopupModal.vue";
import { useRoleBind } from "@/composables/role/useRoleBind";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, reactive, ref } from "vue";
import { onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useRoleBind } from "../composables/role/useRoleBind";
import useAlertStore from "../composables/store/useAlertStore";
import { useRolesQuery } from "@/composables/role/useRolesQuery";
const filterConfig: FilterItem[] = [
{

View File

@@ -108,26 +108,25 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import DepartmentUpsertModal from "@/components/DepartmentUpsertModal.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import DepartmentDeleteModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardList from "@/components/tables/MobileCardList.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import PlusIcon from "@/components/icons/PlusIcon.vue";
import DepartmentUpsertModal from "@/components/modals/DepartmentUpsertModal.vue";
import DepartmentDeleteModal from "@/components/modals/PopupModal.vue";
import useDepartmentDelete from "@/composables/department/useDepartmentDelete";
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
import { useDepartmentUpsert } from "@/composables/department/useDepartmentUpsert";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import type { DepartmentUpsertModel } from "@/types/department";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, reactive, ref } from "vue";
import type { components } from "../api/types/schema";
import useDepartmentDelete from "../composables/department/useDepartmentDelete";
import { useDepartmentQuery } from "../composables/department/useDepartmentQuery";
import { useDepartmentUpsert } from "../composables/department/useDepartmentUpsert";
import useAlertStore from "../composables/store/useAlertStore";
// 定义筛选配置
const filterConfig = [

View File

@@ -115,19 +115,19 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import LlmUpdateModal from "@/components/LlmUpdateModal.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardList from "@/components/tables/MobileCardList.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import LlmUpdateModal from "@/components/modals/LlmUpdateModal.vue";
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
import useAlertStore from "@/composables/store/useAlertStore";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, reactive, ref } from "vue";
import type { components } from "../api/types/schema";
// 定义筛选配置
const filterConfig: FilterItem[] = [

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Routes } from "../router/constants";
import { Routes } from "@/router/constants";
</script>
<template>

View File

@@ -108,17 +108,16 @@
<script setup lang="ts">
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import PermissionUpsertModal from "@/components/PermissionUpsertModal.vue";
import PermissionDeleteModal from "@/components/PopupModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardList from "@/components/tables/MobileCardList.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import PlusIcon from "@/components/icons/PlusIcon.vue";
import PermissionUpsertModal from "@/components/modals/PermissionUpsertModal.vue";
import PermissionDeleteModal from "@/components/modals/PopupModal.vue";
import usePermissionDelete from "@/composables/permission/usePermissionDelete";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";

View File

@@ -98,24 +98,24 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import PositionDeleteModal from "@/components/PopupModal.vue";
import PositionUpsertModal from "@/components/PositionUpsertModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardList from "@/components/tables/MobileCardList.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import PlusIcon from "@/components/icons/PlusIcon.vue";
import PositionDeleteModal from "@/components/modals/PopupModal.vue";
import PositionUpsertModal from "@/components/modals/PositionUpsertModal.vue";
import usePositionDelete from "@/composables/position/usePositionDelete";
import { usePositionQuery } from "@/composables/position/usePositionQuery";
import { usePositionUpsert } from "@/composables/position/usePositionUpsert";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, reactive, ref } from "vue";
import type { components } from "../api/types/schema";
import useAlertStore from "../composables/store/useAlertStore";
// 定义筛选配置
const filterConfig = [

View File

@@ -122,27 +122,27 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import RoleDeleteModal from "@/components/PopupModal.vue";
import RoleUpsertModal from "@/components/RoleUpsertModal.vue";
import TableButton from "@/components/TableButton.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import MobileCardList from "@/components/tables/MobileCardList.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import PlusIcon from "@/components/icons/PlusIcon.vue";
import RoleDeleteModal from "@/components/modals/PopupModal.vue";
import RoleUpsertModal from "@/components/modals/RoleUpsertModal.vue";
import useRoleDelete from "@/composables/role/useRoleDelete";
import { useRolesQuery } from "@/composables/role/useRolesQuery";
import { useRoleUpsert } from "@/composables/role/useRoleUpsert";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import { Routes } from "@/router/constants";
import type { RoleUpsertModel } from "@/types/role";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import type { components } from "../api/types/schema";
import { useRoleUpsert } from "../composables/role/useRoleUpsert";
import useAlertStore from "../composables/store/useAlertStore";
// 定义筛选配置
const filterConfig = [

View File

@@ -11,7 +11,7 @@
<!-- 移动端卡片布局 -->
<div class="md:hidden">
<MobileCardList :items="jobs as Array<components['schemas']['JobTriggerDto']>">
<MobileCardList :items="jobs">
<template #title="{ item }">
{{ `${item.name}:${item.group}` }}
</template>
@@ -134,7 +134,6 @@
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<PopupModal :id="'job-resume-modal'" :closeModal="() => {
jobResumeModal!.hide();
}" :onSubmit="handleResumeModalSubmit" title="确定恢复该任务吗?" content="恢复任务"></PopupModal>
@@ -147,19 +146,19 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import PopupModal from "@/components/PopupModal.vue";
import SchedulerUpdateModal from "@/components/SchedulerUpdateModal.vue";
import TableFilterForm from "@/components/TableFilterForm.vue";
import type { FilterItem } from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import PopupModal from "@/components/modals/PopupModal.vue";
import SchedulerUpdateModal from "@/components/modals/SchedulerUpdateModal.vue";
import MobileCardList from "@/components/tables/MobileCardList.vue";
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import { useJobControl } from "@/composables/job/useJobControl";
import { useJobsPaginationQuery } from "@/composables/job/useJobQuery";
import { useJobUpdate } from "@/composables/job/useJobUpdate";
import useAlertStore from "@/composables/store/useAlertStore";
import { dayjs } from "@/utils/dateUtil";
import dayjs from "dayjs";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, reactive, ref } from "vue";
import type { components } from "../api/types/schema";

View File

@@ -52,14 +52,13 @@
</template>
<script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import useUserAuth from "@/composables/auth/useUserAuth";
import useAlertStore from "@/composables/store/useAlertStore";
import useUserStore from "@/composables/store/useUserStore";
import { initFlowbite } from "flowbite";
import { onMounted, ref } from "vue";
import { z } from "zod";
import useAlertStore from "../composables/store/useAlertStore";
import { Routes } from "../router/constants";
const { user } = useUserStore();
const { upsertCurrentUser } = useUserAuth();

View File

@@ -158,31 +158,31 @@
</template>
<script setup lang="ts">
import Avatar from "@/components/Avatar.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
import UserDeleteModal from "@/components/PopupModal.vue";
import SortIcon from "@/components/SortIcon.vue";
import TableButton from "@/components/TableButton.vue";
import type { components } from "@/api/types/schema";
import { PlusIcon } from "@/components/icons";
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
import UserDeleteModal from "@/components/modals/PopupModal.vue";
import UserUpsertModal from "@/components/modals/UserUpsertModal.vue";
import TableButton from "@/components/tables/TableButton.vue";
import TableFilterForm, {
type FilterItem,
} from "@/components/TableFilterForm.vue";
import TableFormLayout from "@/components/TableFormLayout.vue";
import TablePagination from "@/components/TablePagination.vue";
import UserUpsertModal from "@/components/UserUpsertModal.vue";
import PlusIcon from "@/components/icons/PlusIcon.vue";
} from "@/components/tables/TableFilterForm.vue";
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
import TablePagination from "@/components/tables/TablePagination.vue";
import Avatar from "@/components/ui/Avatar.vue";
import SortIcon from "@/components/ui/SortIcon.vue";
import { useSort } from "@/composables/sort";
import { useActionExcStore } from "@/composables/store/useActionExcStore";
import useAlertStore from "@/composables/store/useAlertStore";
import useUserDelete from "@/composables/user/useUserDelete";
import { useUserQuery } from "@/composables/user/useUserQuery";
import { useUserUpsert } from "@/composables/user/useUserUpsert";
import { Routes } from "@/router/constants";
import type { UserUpsertSubmitModel } from "@/types/user";
import { dayjs, formatDate } from "@/utils/dateUtil";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import type { components } from "../api/types/schema";
import useAlertStore from "../composables/store/useAlertStore";
import { useUserUpsert } from "../composables/user/useUserUpsert";
const filterConfig: FilterItem[] = [
{