mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-13 21:27:19 +08:00
重构组件结构,优化导入路径并移除不必要的组件,新增多个模态框组件以支持部门、角色、权限和用户管理功能
This commit is contained in:
@@ -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>
|
||||
|
||||
3752
frontend/src/api/types/schema.d.ts
vendored
3752
frontend/src/api/types/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
42
frontend/src/components/form/FormInput.vue
Normal file
42
frontend/src/components/form/FormInput.vue
Normal 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>
|
||||
40
frontend/src/components/form/FormSelect.vue
Normal file
40
frontend/src/components/form/FormSelect.vue
Normal 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>
|
||||
13
frontend/src/components/icons/index.ts
Normal file
13
frontend/src/components/icons/index.ts
Normal 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";
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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"]);
|
||||
54
frontend/src/components/modals/BaseModal.vue
Normal file
54
frontend/src/components/modals/BaseModal.vue
Normal 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>
|
||||
99
frontend/src/components/modals/DepartmentUpsertModal.vue
Normal file
99
frontend/src/components/modals/DepartmentUpsertModal.vue
Normal 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>
|
||||
146
frontend/src/components/modals/LlmUpdateModal.vue
Normal file
146
frontend/src/components/modals/LlmUpdateModal.vue
Normal 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>
|
||||
95
frontend/src/components/modals/PermissionUpsertModal.vue
Normal file
95
frontend/src/components/modals/PermissionUpsertModal.vue
Normal 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>
|
||||
33
frontend/src/components/modals/PopupModal.vue
Normal file
33
frontend/src/components/modals/PopupModal.vue
Normal 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>
|
||||
85
frontend/src/components/modals/PositionUpsertModal.vue
Normal file
85
frontend/src/components/modals/PositionUpsertModal.vue
Normal 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>
|
||||
99
frontend/src/components/modals/RoleUpsertModal.vue
Normal file
99
frontend/src/components/modals/RoleUpsertModal.vue
Normal 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>
|
||||
65
frontend/src/components/modals/SchedulerUpdateModal.vue
Normal file
65
frontend/src/components/modals/SchedulerUpdateModal.vue
Normal 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>
|
||||
168
frontend/src/components/modals/UserUpsertModal.vue
Normal file
168
frontend/src/components/modals/UserUpsertModal.vue
Normal 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>
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Routes } from "../router/constants";
|
||||
import { Routes } from "@/router/constants";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user