mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-22 02:43:42 +08:00
重构组件结构,优化导入路径并移除不必要的组件,新增多个模态框组件以支持部门、角色、权限和用户管理功能
This commit is contained in:
@@ -1,13 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink, RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import Alert from "./components/Alert.vue";
|
import Alert from "./components/ui/Alert.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<Alert/>
|
<Alert />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import { LoadingIcon } from "@/components/icons";
|
||||||
import InputButton from "@/components/InputButton.vue";
|
import DepartmentUpsertModal from "@/components/modals/DepartmentUpsertModal.vue";
|
||||||
import UserDeleteModal from "@/components/PopupModal.vue";
|
import UserDeleteModal from "@/components/modals/PopupModal.vue";
|
||||||
import DepartmentDeleteModal from "@/components/PopupModal.vue";
|
import DepartmentDeleteModal from "@/components/modals/PopupModal.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import UserUpsertModal from "@/components/modals/UserUpsertModal.vue";
|
||||||
import LoadingIcon from "@/components/icons/LoadingIcon.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 { useAiAction } from "@/composables/ai/useAiAction";
|
||||||
|
import { useAiChat } from "@/composables/ai/useAiChat";
|
||||||
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
|
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
|
||||||
import { useDepartmentUpsert } from "@/composables/department/useDepartmentUpsert";
|
import { useDepartmentUpsert } from "@/composables/department/useDepartmentUpsert";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
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 { DepartmentUpsertModel } from "@/types/department";
|
||||||
|
import type { UserUpsertSubmitModel } from "@/types/user";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
import { z } from "zod";
|
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 {
|
const {
|
||||||
messages,
|
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">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import Assistant from "./Assistant.vue";
|
import Assistant from "@/components/common/Assistant.vue";
|
||||||
import Headbar from "./Headbar.vue";
|
import Headbar from "./Headbar.vue";
|
||||||
import Sidebar from "./Sidebar.vue";
|
import Sidebar from "./Sidebar.vue";
|
||||||
|
|
||||||
@@ -88,14 +88,14 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<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 useUserStore from "@/composables/store/useUserStore";
|
||||||
|
import { Routes } from "@/router/constants";
|
||||||
import { Dropdown, type DropdownInterface, initFlowbite } from "flowbite";
|
import { Dropdown, type DropdownInterface, initFlowbite } from "flowbite";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
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<{
|
const props = defineProps<{
|
||||||
changeAssistantVisible: () => void;
|
changeAssistantVisible: () => void;
|
||||||
@@ -33,14 +33,16 @@ import { initFlowbite } from "flowbite";
|
|||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { RouterLink, useRoute } from "vue-router";
|
import { RouterLink, useRoute } from "vue-router";
|
||||||
|
|
||||||
import DepartmentIcon from "./icons/DepartmentIcon.vue";
|
import {
|
||||||
import LlmConfigIcon from "./icons/LlmConfigIcon.vue";
|
DepartmentIcon,
|
||||||
import PermissionIcon from "./icons/PermissionIcon.vue";
|
LlmConfigIcon,
|
||||||
import PositionIcon from "./icons/PositionIcon.vue";
|
PermissionIcon,
|
||||||
import RoleIcon from "./icons/RoleIcon.vue";
|
PositionIcon,
|
||||||
import SchedulerIcon from "./icons/SchedulerIcon.vue";
|
RoleIcon,
|
||||||
import SettingsIcon from "./icons/SettingsIcon.vue";
|
SchedulerIcon,
|
||||||
import UsersIcon from "./icons/UsersIcon.vue";
|
SettingsIcon,
|
||||||
|
UsersIcon,
|
||||||
|
} from "@/components/icons";
|
||||||
|
|
||||||
const isDrawerVisible = ref(false);
|
const isDrawerVisible = ref(false);
|
||||||
const emit = defineEmits(["menu-click"]);
|
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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { StopIcon } from "@/components/icons";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import StopIcon from "./icons/StopIcon.vue";
|
|
||||||
|
|
||||||
export type ButtonVariant =
|
export type ButtonVariant =
|
||||||
| "primary"
|
| "primary"
|
||||||
@@ -35,6 +35,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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();
|
const alertStore = useAlertStore();
|
||||||
</script>
|
</script>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
import Dashboard from "../../components/Dashboard.vue";
|
import { EPermission, Routes } from "../constants";
|
||||||
import { EPermission, ERole, Routes } from "../constants";
|
|
||||||
import aiRoutes from "./ai";
|
import aiRoutes from "./ai";
|
||||||
import userManagementRoutes from "./user";
|
import userManagementRoutes from "./user";
|
||||||
|
import Dashboard from "../../components/layout/Dashboard.vue";
|
||||||
|
|
||||||
const dashboardRoutes: RouteRecordRaw = {
|
const dashboardRoutes: RouteRecordRaw = {
|
||||||
path: Routes.DASHBOARD.path,
|
path: Routes.DASHBOARD.path,
|
||||||
|
|||||||
@@ -88,23 +88,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
|
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
|
||||||
import BindModal from "@/components/PopupModal.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import UnModal from "@/components/PopupModal.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import BindModal from "@/components/modals/PopupModal.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
import UnModal from "@/components/modals/PopupModal.vue";
|
||||||
|
import { useDepartmentBind } from "@/composables/department/useDepartmentBind";
|
||||||
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
|
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { onMounted, reactive, ref, watch } from "vue";
|
import { onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { useDepartmentBind } from "../composables/department/useDepartmentBind";
|
|
||||||
import useAlertStore from "../composables/store/useAlertStore";
|
|
||||||
|
|
||||||
// 定义筛选配置
|
// 定义筛选配置
|
||||||
const filterConfig: FilterItem[] = [
|
const filterConfig: FilterItem[] = [
|
||||||
|
|||||||
@@ -89,23 +89,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import type { components } from "@/api/types/schema";
|
||||||
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import BindModal from "@/components/PopupModal.vue";
|
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
|
||||||
import UnModal from "@/components/PopupModal.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import TablePagination from "@/components/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 { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { onMounted, reactive, ref, watch } from "vue";
|
import { onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { usePermissionBind } from "../composables/permission/usePermissionBind";
|
import usePermissionsQuery from "@/composables/permission/usePermissionQuery";
|
||||||
import usePermissionsQuery from "../composables/permission/usePermissionQuery";
|
|
||||||
import useAlertStore from "../composables/store/useAlertStore";
|
|
||||||
|
|
||||||
// 定义筛选配置
|
// 定义筛选配置
|
||||||
const filterConfig: FilterItem[] = [
|
const filterConfig: FilterItem[] = [
|
||||||
|
|||||||
@@ -80,24 +80,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
|
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
|
||||||
import BindModal from "@/components/PopupModal.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import UnModal from "@/components/PopupModal.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import BindModal from "@/components/modals/PopupModal.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
import UnModal from "@/components/modals/PopupModal.vue";
|
||||||
import { usePositionBind } from "@/composables/position/usePositionBind";
|
import { usePositionBind } from "@/composables/position/usePositionBind";
|
||||||
import { usePositionQuery } from "@/composables/position/usePositionQuery";
|
import { usePositionQuery } from "@/composables/position/usePositionQuery";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
import { useMobileStyles } from "@/composables/useMobileStyles";
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { onMounted, reactive, ref, watch } from "vue";
|
import { onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import useAlertStore from "../composables/store/useAlertStore";
|
|
||||||
|
|
||||||
// 定义筛选配置
|
// 定义筛选配置
|
||||||
const filterConfig: FilterItem[] = [
|
const filterConfig: FilterItem[] = [
|
||||||
|
|||||||
@@ -90,23 +90,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import type { components } from "@/api/types/schema";
|
||||||
import MobileCardListWithCheckbox from "@/components/MobileCardListWithCheckbox.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import BindModal from "@/components/PopupModal.vue";
|
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
|
||||||
import UnModal from "@/components/PopupModal.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
import BindModal from "@/components/modals/PopupModal.vue";
|
||||||
import { useRolesQuery } from "@/composables/role/useRolesQuery";
|
import UnModal from "@/components/modals/PopupModal.vue";
|
||||||
|
import { useRoleBind } from "@/composables/role/useRoleBind";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
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 { useRoute } from "vue-router";
|
||||||
import { useRoleBind } from "../composables/role/useRoleBind";
|
import { useRolesQuery } from "@/composables/role/useRolesQuery";
|
||||||
import useAlertStore from "../composables/store/useAlertStore";
|
|
||||||
|
|
||||||
const filterConfig: FilterItem[] = [
|
const filterConfig: FilterItem[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -108,26 +108,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import type { components } from "@/api/types/schema";
|
||||||
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import DepartmentUpsertModal from "@/components/DepartmentUpsertModal.vue";
|
import MobileCardList from "@/components/tables/MobileCardList.vue";
|
||||||
import MobileCardList from "@/components/MobileCardList.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import DepartmentDeleteModal from "@/components/PopupModal.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
|
||||||
import PlusIcon from "@/components/icons/PlusIcon.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 { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import type { DepartmentUpsertModel } from "@/types/department";
|
import type { DepartmentUpsertModel } from "@/types/department";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
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 = [
|
const filterConfig = [
|
||||||
|
|||||||
@@ -115,19 +115,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import type { components } from "@/api/types/schema";
|
||||||
import LlmUpdateModal from "@/components/LlmUpdateModal.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import MobileCardList from "@/components/MobileCardList.vue";
|
import MobileCardList from "@/components/tables/MobileCardList.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
|
import LlmUpdateModal from "@/components/modals/LlmUpdateModal.vue";
|
||||||
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
|
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
|
||||||
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
|
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||||
import type { components } from "../api/types/schema";
|
|
||||||
|
|
||||||
// 定义筛选配置
|
// 定义筛选配置
|
||||||
const filterConfig: FilterItem[] = [
|
const filterConfig: FilterItem[] = [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Routes } from "../router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -108,17 +108,16 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { components } from "@/api/types/schema";
|
import type { components } from "@/api/types/schema";
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
|
import MobileCardList from "@/components/tables/MobileCardList.vue";
|
||||||
import MobileCardList from "@/components/MobileCardList.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import PermissionUpsertModal from "@/components/PermissionUpsertModal.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import PermissionDeleteModal from "@/components/PopupModal.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
|
||||||
import PlusIcon from "@/components/icons/PlusIcon.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 usePermissionDelete from "@/composables/permission/usePermissionDelete";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
|
|||||||
@@ -98,24 +98,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import type { components } from "@/api/types/schema";
|
||||||
import MobileCardList from "@/components/MobileCardList.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import PositionDeleteModal from "@/components/PopupModal.vue";
|
import MobileCardList from "@/components/tables/MobileCardList.vue";
|
||||||
import PositionUpsertModal from "@/components/PositionUpsertModal.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
|
||||||
import PlusIcon from "@/components/icons/PlusIcon.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 usePositionDelete from "@/composables/position/usePositionDelete";
|
||||||
import { usePositionQuery } from "@/composables/position/usePositionQuery";
|
import { usePositionQuery } from "@/composables/position/usePositionQuery";
|
||||||
import { usePositionUpsert } from "@/composables/position/usePositionUpsert";
|
import { usePositionUpsert } from "@/composables/position/usePositionUpsert";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||||
import type { components } from "../api/types/schema";
|
|
||||||
import useAlertStore from "../composables/store/useAlertStore";
|
|
||||||
|
|
||||||
// 定义筛选配置
|
// 定义筛选配置
|
||||||
const filterConfig = [
|
const filterConfig = [
|
||||||
|
|||||||
@@ -122,27 +122,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import type { components } from "@/api/types/schema";
|
||||||
import MobileCardList from "@/components/MobileCardList.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import RoleDeleteModal from "@/components/PopupModal.vue";
|
import MobileCardList from "@/components/tables/MobileCardList.vue";
|
||||||
import RoleUpsertModal from "@/components/RoleUpsertModal.vue";
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
|
||||||
import PlusIcon from "@/components/icons/PlusIcon.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 useRoleDelete from "@/composables/role/useRoleDelete";
|
||||||
import { useRolesQuery } from "@/composables/role/useRolesQuery";
|
import { useRolesQuery } from "@/composables/role/useRolesQuery";
|
||||||
|
import { useRoleUpsert } from "@/composables/role/useRoleUpsert";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
import type { RoleUpsertModel } from "@/types/role";
|
import type { RoleUpsertModel } from "@/types/role";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
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 = [
|
const filterConfig = [
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<!-- 移动端卡片布局 -->
|
<!-- 移动端卡片布局 -->
|
||||||
<div class="md:hidden">
|
<div class="md:hidden">
|
||||||
<MobileCardList :items="jobs as Array<components['schemas']['JobTriggerDto']>">
|
<MobileCardList :items="jobs">
|
||||||
<template #title="{ item }">
|
<template #title="{ item }">
|
||||||
{{ `${item.name}:${item.group}` }}
|
{{ `${item.name}:${item.group}` }}
|
||||||
</template>
|
</template>
|
||||||
@@ -134,7 +134,6 @@
|
|||||||
|
|
||||||
<TablePagination :pageChange="handlePageChange" :total="total" />
|
<TablePagination :pageChange="handlePageChange" :total="total" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PopupModal :id="'job-resume-modal'" :closeModal="() => {
|
<PopupModal :id="'job-resume-modal'" :closeModal="() => {
|
||||||
jobResumeModal!.hide();
|
jobResumeModal!.hide();
|
||||||
}" :onSubmit="handleResumeModalSubmit" title="确定恢复该任务吗?" content="恢复任务"></PopupModal>
|
}" :onSubmit="handleResumeModalSubmit" title="确定恢复该任务吗?" content="恢复任务"></PopupModal>
|
||||||
@@ -147,19 +146,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import MobileCardList from "@/components/MobileCardList.vue";
|
import PopupModal from "@/components/modals/PopupModal.vue";
|
||||||
import PopupModal from "@/components/PopupModal.vue";
|
import SchedulerUpdateModal from "@/components/modals/SchedulerUpdateModal.vue";
|
||||||
import SchedulerUpdateModal from "@/components/SchedulerUpdateModal.vue";
|
import MobileCardList from "@/components/tables/MobileCardList.vue";
|
||||||
import TableFilterForm from "@/components/TableFilterForm.vue";
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
import type { FilterItem } from "@/components/TableFilterForm.vue";
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import { useJobControl } from "@/composables/job/useJobControl";
|
import { useJobControl } from "@/composables/job/useJobControl";
|
||||||
import { useJobsPaginationQuery } from "@/composables/job/useJobQuery";
|
import { useJobsPaginationQuery } from "@/composables/job/useJobQuery";
|
||||||
import { useJobUpdate } from "@/composables/job/useJobUpdate";
|
import { useJobUpdate } from "@/composables/job/useJobUpdate";
|
||||||
import useAlertStore from "@/composables/store/useAlertStore";
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import { dayjs } from "@/utils/dateUtil";
|
import dayjs from "dayjs";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||||
import type { components } from "../api/types/schema";
|
import type { components } from "../api/types/schema";
|
||||||
|
|||||||
@@ -52,14 +52,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import useUserAuth from "@/composables/auth/useUserAuth";
|
import useUserAuth from "@/composables/auth/useUserAuth";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import useUserStore from "@/composables/store/useUserStore";
|
import useUserStore from "@/composables/store/useUserStore";
|
||||||
import { initFlowbite } from "flowbite";
|
import { initFlowbite } from "flowbite";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import useAlertStore from "../composables/store/useAlertStore";
|
|
||||||
import { Routes } from "../router/constants";
|
|
||||||
|
|
||||||
const { user } = useUserStore();
|
const { user } = useUserStore();
|
||||||
const { upsertCurrentUser } = useUserAuth();
|
const { upsertCurrentUser } = useUserAuth();
|
||||||
|
|||||||
@@ -158,31 +158,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import type { components } from "@/api/types/schema";
|
||||||
import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
import { PlusIcon } from "@/components/icons";
|
||||||
import UserDeleteModal from "@/components/PopupModal.vue";
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
import SortIcon from "@/components/SortIcon.vue";
|
import UserDeleteModal from "@/components/modals/PopupModal.vue";
|
||||||
import TableButton from "@/components/TableButton.vue";
|
import UserUpsertModal from "@/components/modals/UserUpsertModal.vue";
|
||||||
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
import TableFilterForm, {
|
import TableFilterForm, {
|
||||||
type FilterItem,
|
type FilterItem,
|
||||||
} from "@/components/TableFilterForm.vue";
|
} from "@/components/tables/TableFilterForm.vue";
|
||||||
import TableFormLayout from "@/components/TableFormLayout.vue";
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
import TablePagination from "@/components/TablePagination.vue";
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
import UserUpsertModal from "@/components/UserUpsertModal.vue";
|
import Avatar from "@/components/ui/Avatar.vue";
|
||||||
import PlusIcon from "@/components/icons/PlusIcon.vue";
|
import SortIcon from "@/components/ui/SortIcon.vue";
|
||||||
import { useSort } from "@/composables/sort";
|
import { useSort } from "@/composables/sort";
|
||||||
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
import useUserDelete from "@/composables/user/useUserDelete";
|
import useUserDelete from "@/composables/user/useUserDelete";
|
||||||
import { useUserQuery } from "@/composables/user/useUserQuery";
|
import { useUserQuery } from "@/composables/user/useUserQuery";
|
||||||
|
import { useUserUpsert } from "@/composables/user/useUserUpsert";
|
||||||
import { Routes } from "@/router/constants";
|
import { Routes } from "@/router/constants";
|
||||||
import type { UserUpsertSubmitModel } from "@/types/user";
|
import type { UserUpsertSubmitModel } from "@/types/user";
|
||||||
import { dayjs, formatDate } from "@/utils/dateUtil";
|
import { dayjs, formatDate } from "@/utils/dateUtil";
|
||||||
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
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[] = [
|
const filterConfig: FilterItem[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user