This commit is contained in:
Chuck1sn
2025-05-14 10:16:48 +08:00
commit 3cd59337e7
220 changed files with 23768 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['部门分配']" />
<h1 class="text-xl mb-2 font-semibold text-gray-900 sm:text-2xl dark:text-white">部门分配</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4 grid grid-cols-5 gap-y-4">
<div class="col-span-3">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="departmentName"
class="block p-3 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="部门名称" required />
</div>
</div>
<select id="countries" v-model="bindState"
class="col-span-2 block bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="BIND">已绑定</option>
<option value="UNBIND">未绑定</option>
<option value="ALL">全部</option>
</select>
<button type="submit"
class="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-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</form>
<div class="flex items-center justify-end gap-2 absolute right-5 bottom-2">
<button @click="() => {
if (checkedDepartmentIds.length === 0) {
alertStore.showAlert({
content: '没有选择部门',
level: 'error',
});
} else {
departmentBindModal?.show();
}
}"
class="flex items-center block 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"
type="button">
绑定
</button>
<button @click="() => {
if (checkedDepartmentIds.length === 0) {
alertStore.showAlert({
content: '没有选择部门',
level: 'error',
});
} else {
departmentUnbindModal?.show();
}
}"
class="flex items-center block text-white bg-red-700 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"
type="button">
解绑
</button>
</div>
</div>
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">上级部门</th>
<th scope="col" class="px-6 py-3">部门名称</th>
<th scope="col" class="px-6 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="department in departments" :key="department.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + department.id" :value="department.id" type="checkbox"
v-model="checkedDepartmentIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + department.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4whitespace-nowrap dark:text-white">
{{ !department.parentName ? '无' : department.parentName }}
</td>
<td scope="row" class="px-6 py-4whitespace-nowrap font-medium text-gray-900 dark:text-white">
{{ department.name }}
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="department.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
department.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<BindModal :id="'department-bind-modal'" :closeModal="() => {
departmentBindModal!.hide();
}" :onSubmit="handleBindDepartmentSubmit" title="绑定选中的部门吗"></BindModal>
<UnModal :id="'department-unbind-modal'" :closeModal="() => {
departmentUnbindModal!.hide();
}" :onSubmit="handleUnbindDepartmentSubmit" title="解绑选中的部门吗"></UnModal>
</template>
<script setup lang="ts">
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useDepartmentQuery } from "@/composables/department/useDepartmentQuery";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useDepartmentBind } from "../composables/department/useDepartmentBind";
import useAlertStore from "../composables/store/useAlertStore";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const departmentName = ref<string>("");
const checkedDepartmentIds = ref<number[]>([]);
const departmentBindModal = ref<ModalInterface>();
const departmentUnbindModal = ref<ModalInterface>();
const allChecked = ref<boolean>(false);
const $route = useRoute();
const bindState = ref<"BIND" | "ALL" | "UNBIND">("BIND");
const alertStore = useAlertStore();
const { total, departments, fetchDepartmentWith } = useDepartmentQuery();
const { bindDepartment, unbindDepartment } = useDepartmentBind();
const handleBindDepartmentSubmit = async () => {
await bindDepartment(
Number($route.params.userId),
checkedDepartmentIds.value,
);
clearCheckedDepartment();
departmentBindModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchDepartmentWith({
name: departmentName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
};
const handleUnbindDepartmentSubmit = async () => {
await unbindDepartment(
Number($route.params.userId),
checkedDepartmentIds.value,
);
clearCheckedDepartment();
departmentUnbindModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchDepartmentWith({
name: departmentName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
};
onMounted(async () => {
await fetchDepartmentWith({
name: departmentName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
initFlowbite();
const $bindModalElement: HTMLElement | null = document.querySelector(
"#department-bind-modal",
);
departmentBindModal.value = new Modal(
$bindModalElement,
{},
{ id: "department-bind-modal" },
);
const $unbindModalElement: HTMLElement | null = document.querySelector(
"#department-unbind-modal",
);
departmentUnbindModal.value = new Modal(
$unbindModalElement,
{},
{ id: "department-unbind-modal" },
);
});
const handleSearch = async () => {
await fetchDepartmentWith({
name: departmentName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchDepartmentWith(
{
name: departmentName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
},
page,
pageSize,
);
};
watch(allChecked, async () => {
if (allChecked.value) {
checkedDepartmentIds.value = departments.value?.map((d) => d.id!) ?? [];
} else {
checkedDepartmentIds.value = [];
}
});
const clearCheckedDepartment = () => {
checkedDepartmentIds.value = [];
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,241 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['绑定权限']" />
<h1 class="text-xl mb-2 font-semibold text-gray-900 sm:text-2xl dark:text-white">
绑定权限</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4 grid grid-cols-5 gap-y-4">
<div class="col-span-3">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="permissionName"
class="block p-3 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="权限名" required />
</div>
</div>
<select id="countries" v-model="bindState"
class="col-span-2 block bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="BIND">已绑定</option>
<option value="UNBIND">未绑定</option>
<option value="ALL">全部</option>
</select>
<button type="submit"
class="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-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</form>
<div class="flex items-center justify-end gap-2 absolute right-5 bottom-2">
<button @click="() => {
if (checkedPermissionIds.length === 0) {
alertStore.showAlert({
content: '没有选择权限',
level: 'error',
});
} else {
permissionBindModal?.show();
}
}"
class="flex items-center block 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"
type="button">
绑定
</button>
<button @click="() => {
if (checkedPermissionIds.length === 0) {
alertStore.showAlert({
content: '没有选择权限',
level: 'error',
});
} else {
permissionUnbindModal?.show();
}
}"
class="flex items-center block text-white bg-red-700 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"
type="button">
解绑
</button>
</div>
</div>
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">权限编码</th>
<th scope="col" class="px-6 py-3">权限名称</th>
<th scope="col" class="px-6 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="permission in permissions" :key="permission.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + permission.id" :value="permission.id" type="checkbox"
v-model="checkedPermissionIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + permission.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ permission.code }}
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ permission.name }}
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="permission.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
permission.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<BindModal :id="'permission-bind-modal'" :closeModal="() => {
permissionBindModal!.hide();
}" :onSubmit="handleBindPermissionSubmit" title="确定绑定选中的权限吗"></BindModal>
<UnModal :id="'permission-unbind-modal'" :closeModal="() => {
permissionUnbindModal!.hide();
}" :onSubmit="handleUnbindPermissionSubmit" title="确定解绑选中的权限吗"></UnModal>
</template>
<script setup lang="ts">
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { usePermissionBind } from "../composables/permission/usePermissionBind";
import usePermissionsQuery from "../composables/permission/usePermissionQuery";
import useAlertStore from "../composables/store/useAlertStore";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const permissionName = ref<string>("");
const checkedPermissionIds = ref<number[]>([]);
const permissionBindModal = ref<ModalInterface>();
const permissionUnbindModal = ref<ModalInterface>();
const allChecked = ref<boolean>(false);
const $route = useRoute();
const bindState = ref<"BIND" | "ALL" | "UNBIND">("BIND");
const alertStore = useAlertStore();
const { total, permissions, fetchPermissionsWith } = usePermissionsQuery();
const { bindPermission, unbindPermission } = usePermissionBind();
const handleBindPermissionSubmit = async () => {
await bindPermission({
roleId: Number($route.params.roleId),
permissionIds: checkedPermissionIds.value,
});
permissionBindModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
clearCheckedRoleIds();
await fetchPermissionsWith({
name: permissionName.value,
roleId: Number($route.params.roleId),
bindState: bindState.value,
});
};
const handleUnbindPermissionSubmit = async () => {
await unbindPermission({
roleId: Number($route.params.roleId),
permissionIds: checkedPermissionIds.value,
});
permissionUnbindModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
clearCheckedRoleIds();
await fetchPermissionsWith({
name: permissionName.value,
roleId: Number($route.params.roleId),
bindState: bindState.value,
});
};
onMounted(async () => {
await fetchPermissionsWith({
name: permissionName.value,
roleId: Number($route.params.roleId),
bindState: bindState.value,
});
initFlowbite();
const $bindModalElement: HTMLElement | null = document.querySelector(
"#permission-bind-modal",
);
permissionBindModal.value = new Modal(
$bindModalElement,
{},
{ id: "permission-bind-modal" },
);
const $unbindModalElement: HTMLElement | null = document.querySelector(
"#permission-unbind-modal",
);
permissionUnbindModal.value = new Modal(
$unbindModalElement,
{},
{ id: "permission-unbind-modal" },
);
});
const handleSearch = async () => {
await fetchPermissionsWith({
name: permissionName.value,
roleId: Number($route.params.roleId),
bindState: bindState.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchPermissionsWith(
{
name: permissionName.value,
roleId: Number($route.params.roleId),
bindState: bindState.value,
},
page,
pageSize,
);
};
watch(allChecked, async () => {
if (allChecked.value) {
checkedPermissionIds.value = permissions.value?.map((p) => p.id!) ?? [];
} else {
checkedPermissionIds.value = [];
}
});
const clearCheckedRoleIds = () => {
checkedPermissionIds.value = [];
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,233 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['岗位分配']" />
<h1 class="text-xl mb-2 font-semibold text-gray-900 sm:text-2xl dark:text-white">岗位分配</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4 grid grid-cols-5 gap-y-4">
<div class="col-span-3">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="positionName"
class="block p-3 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="岗位名称" required />
</div>
</div>
<select id="countries" v-model="bindState"
class="col-span-2 block bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="BIND">已绑定</option>
<option value="UNBIND">未绑定</option>
<option value="ALL">全部</option>
</select>
<button type="submit"
class="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-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</form>
<div class="flex items-center justify-end gap-2 absolute right-5 bottom-2">
<button @click="() => {
if (checkedPositionIds.length === 0) {
alertStore.showAlert({
content: '没有选择岗位',
level: 'error',
});
} else {
positionBindModal?.show();
}
}"
class="flex items-center block 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"
type="button">
绑定
</button>
<button @click="() => {
if (checkedPositionIds.length === 0) {
alertStore.showAlert({
content: '没有选择岗位',
level: 'error',
});
} else {
positionUnbindModal?.show();
}
}"
class="flex items-center block text-white bg-red-700 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"
type="button">
解绑
</button>
</div>
</div>
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">岗位名称</th>
<th scope="col" class="px-6 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="position in positions" :key="position.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + position.id" :value="position.id" type="checkbox"
v-model="checkedPositionIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + position.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ position.name }}
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="position.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
position.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<BindModal :id="'position-bind-modal'" :closeModal="() => {
positionBindModal!.hide();
}" :onSubmit="handleBindPositionSubmit" title="绑定选中的岗位吗"></BindModal>
<UnModal :id="'position-unbind-modal'" :closeModal="() => {
positionUnbindModal!.hide();
}" :onSubmit="handleUnbindPositionSubmit" title="解绑选中的岗位吗"></UnModal>
</template>
<script setup lang="ts">
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import { usePositionBind } from "@/composables/position/usePositionBind";
import { usePositionQuery } from "@/composables/position/usePositionQuery";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import useAlertStore from "../composables/store/useAlertStore";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const positionName = ref<string>("");
const checkedPositionIds = ref<number[]>([]);
const positionBindModal = ref<ModalInterface>();
const positionUnbindModal = ref<ModalInterface>();
const allChecked = ref<boolean>(false);
const $route = useRoute();
const bindState = ref<"BIND" | "ALL" | "UNBIND">("BIND");
const alertStore = useAlertStore();
const { total, positions, fetchPositionWith } = usePositionQuery();
const { bindPosition, unbindPosition } = usePositionBind();
const handleBindPositionSubmit = async () => {
await bindPosition(Number($route.params.userId), checkedPositionIds.value);
positionBindModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchPositionWith({
name: positionName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
clearCheckedPositionIds();
};
const handleUnbindPositionSubmit = async () => {
await unbindPosition(Number($route.params.userId), checkedPositionIds.value);
positionUnbindModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchPositionWith({
name: positionName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
clearCheckedPositionIds();
};
onMounted(async () => {
await fetchPositionWith({
name: positionName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
initFlowbite();
const $bindModalElement: HTMLElement | null = document.querySelector(
"#position-bind-modal",
);
positionBindModal.value = new Modal(
$bindModalElement,
{},
{ id: "position-bind-modal" },
);
const $unbindModalElement: HTMLElement | null = document.querySelector(
"#position-unbind-modal",
);
positionUnbindModal.value = new Modal(
$unbindModalElement,
{},
{ id: "position-unbind-modal" },
);
});
const handleSearch = async () => {
await fetchPositionWith({
name: positionName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchPositionWith(
{
name: positionName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
},
page,
pageSize,
);
};
watch(allChecked, async () => {
if (allChecked.value) {
checkedPositionIds.value = positions.value?.map((d) => d.id!) ?? [];
} else {
checkedPositionIds.value = [];
}
});
const clearCheckedPositionIds = () => {
checkedPositionIds.value = [];
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,235 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['角色分配']" />
<h1 class="text-xl mb-2 font-semibold text-gray-900 sm:text-2xl dark:text-white">角色分配</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4 grid grid-cols-5 gap-y-4">
<div class="col-span-3">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="roleName"
class="block p-3 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="角色名" required />
</div>
</div>
<select id="countries" v-model="bindState"
class="col-span-2 block bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="BIND">已绑定</option>
<option value="UNBIND">未绑定</option>
<option value="ALL">全部</option>
</select>
<button type="submit"
class="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-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</form>
<div class="flex items-center justify-end gap-2 absolute right-5 bottom-2">
<button @click="() => {
if (checkedRoleIds.length === 0) {
alertStore.showAlert({
content: '没有选择角色',
level: 'error',
});
} else {
roleBindModal?.show();
}
}"
class="flex items-center block 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"
type="button">
绑定
</button>
<button @click="() => {
if (checkedRoleIds.length === 0) {
alertStore.showAlert({
content: '没有选择角色',
level: 'error',
});
} else {
roleUnbindModal?.show();
}
}"
class="flex items-center block text-white bg-red-700 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"
type="button">
解绑
</button>
</div>
</div>
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">角色编码</th>
<th scope="col" class="px-6 py-3">角色名称</th>
<th scope="col" class="px-6 py-3">绑定状态</th>
</tr>
</thead>
<tbody>
<tr v-for="role in roles" :key="role.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + role.id" :value="role.id" type="checkbox" v-model="checkedRoleIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + role.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ role.code }}
</td>
<td scope="row" class="px-6 py-4 whitespace-nowrap dark:text-white">
{{ role.name }}
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="role.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{
role.isBound === true ? "已绑定" : "未绑定" }}
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<BindModal :id="'role-bind-modal'" :closeModal="() => {
roleBindModal!.hide();
}" :onSubmit="handleBindRoleSubmit" title="确定绑定选中的角色吗"></BindModal>
<UnModal :id="'role-unbind-modal'" :closeModal="() => {
roleUnbindModal!.hide();
}" :onSubmit="handleUnbindRoleSubmit" title="确定解绑选中的角色吗"></UnModal>
</template>
<script setup lang="ts">
import BindModal from "@/components/PopupModal.vue";
import UnModal from "@/components/PopupModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useRolesQuery } from "@/composables/role/useRolesQuery";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { useRoleBind } from "../composables/role/useRoleBind";
import useAlertStore from "../composables/store/useAlertStore";
import { tr } from "@faker-js/faker";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const roleName = ref<string>("");
const checkedRoleIds = ref<number[]>([]);
const roleBindModal = ref<ModalInterface>();
const roleUnbindModal = ref<ModalInterface>();
const allChecked = ref<boolean>(false);
const $route = useRoute();
const bindState = ref<"BIND" | "ALL" | "UNBIND">("BIND");
const alertStore = useAlertStore();
const { total, roles, fetchRolesWith } = useRolesQuery();
const { bindRole, unbindRole } = useRoleBind();
const handleBindRoleSubmit = async () => {
await bindRole({
userId: Number($route.params.userId),
roleIds: checkedRoleIds.value,
});
roleBindModal.value?.hide();
clearCheckedRoleIds();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchRolesWith({
name: roleName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
};
const handleUnbindRoleSubmit = async () => {
await unbindRole(Number($route.params.userId), checkedRoleIds.value);
clearCheckedRoleIds();
roleUnbindModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchRolesWith({
name: roleName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
};
onMounted(async () => {
await fetchRolesWith({
name: roleName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
initFlowbite();
const $bindModalElement: HTMLElement | null =
document.querySelector("#role-bind-modal");
roleBindModal.value = new Modal(
$bindModalElement,
{},
{ id: "role-bind-modal" },
);
const $unbindModalElement: HTMLElement | null =
document.querySelector("#role-unbind-modal");
roleUnbindModal.value = new Modal(
$unbindModalElement,
{},
{ id: "role-unbind-modal" },
);
});
const handleSearch = async () => {
await fetchRolesWith({
name: roleName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchRolesWith(
{
name: roleName.value,
userId: Number($route.params.userId),
bindState: bindState.value,
},
page,
pageSize,
);
};
watch(allChecked, () => {
if (allChecked.value) {
checkedRoleIds.value = roles.value?.map((r) => r.id!) ?? [];
} else {
checkedRoleIds.value = [];
}
});
const clearCheckedRoleIds = () => {
checkedRoleIds.value = [];
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,233 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['部门管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">部门管理</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="name"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="部门名称" required />
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</div>
</form>
<!-- Create Modal toggle -->
<button @click="handleUpsertDepartmentClick()"
class="flex items-center block 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 absolute right-5 bottom-2"
type="button">
新增部门
</button>
</div>
<table class="w-full text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">上级部门</th>
<th scope="col" class="px-6 py-3">部门名称</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="department in departments" :key="department.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + department.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + department.id" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-6 py-4">
{{ !department.parentName ? '无' : department.parentName }}
</td>
<td class="px-6 py-4 font-medium text-gray-900 ">
{{ department.name }}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertDepartmentClick(department)"
class="flex items-center gap-x-1 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 "
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
编辑
</button>
<button
class="flex items-center block gap-x-1
bg-red-700 hover:bg-red-800 focus:outline-none dark:bg-red-600 dark:hover:bg-red-700
focus:ring-red-500 block text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
@click="handleDeleteDepartmentClick(department)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :total="total" :pageChange="handlePageChange" />
</div>
<DepartmentDeleteModal :id="'department-delete-modal'" :closeModal="() => {
departmentDeleteModal!.hide();
}" :onSubmit="handleDeleteDepartmentSubmit" title="确定删除该部门吗" content="删除部门"></DepartmentDeleteModal>
<DepartmentUpsertModal :id="'department-upsert-modal'" :onSubmit="handleUpsertDepartmentSubmit" :closeModal="() => {
departmentUpsertModal!.hide();
}" :department="selectedDepartment" :allDepartments="allDepartments">
</DepartmentUpsertModal>
</template>
<script setup lang="ts">
import DepartmentDeleteModal from "@/components/PopupModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import DepartmentUpsertModal from "@/components/DepartmentUpsertModal.vue";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, 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";
import type { DepartmentUpsertModel } from "@/types/department";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const name = ref<string>("");
const selectedDepartment = ref<components["schemas"]["Department"]>();
const departmentUpsertModal = ref<ModalInterface>();
const departmentDeleteModal = ref<ModalInterface>();
const {
departments,
allDepartments,
fetchDepartmentWith,
fetchAllDepartments,
total,
} = useDepartmentQuery();
const { deleteDepartment } = useDepartmentDelete();
const { upsertDepartment } = useDepartmentUpsert();
const alertStore = useAlertStore();
onMounted(async () => {
await fetchAllDepartments();
await fetchDepartmentWith({
name: name.value,
});
initFlowbite();
const $upsertModalElement: HTMLElement | null = document.querySelector(
"#department-upsert-modal",
);
const $deleteModalElement: HTMLElement | null = document.querySelector(
"#department-delete-modal",
);
departmentUpsertModal.value = new Modal(
$upsertModalElement,
{},
{
id: "department-upsert-modal",
},
);
departmentDeleteModal.value = new Modal(
$deleteModalElement,
{},
{
id: "department-delete-modal",
},
);
});
const handleUpsertDepartmentSubmit = async (
department: DepartmentUpsertModel,
) => {
await upsertDepartment(department);
departmentUpsertModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchDepartmentWith({
name: name.value,
});
fetchAllDepartments();
};
const handleUpsertDepartmentClick = async (
department?: components["schemas"]["Department"],
) => {
selectedDepartment.value = department;
await nextTick(() => {
departmentUpsertModal.value?.show();
});
};
const handleDeleteDepartmentSubmit = async () => {
if (!selectedDepartment?.value?.id) return;
await deleteDepartment(selectedDepartment.value.id);
departmentDeleteModal.value?.hide();
alertStore.showAlert({
content: "删除成功",
level: "success",
});
await fetchDepartmentWith({
name: name.value,
});
fetchAllDepartments();
};
const handleDeleteDepartmentClick = async (
department: components["schemas"]["Department"],
) => {
selectedDepartment.value = department;
await nextTick(() => {
departmentDeleteModal.value?.show();
});
};
const handleSearch = async () => {
await fetchDepartmentWith({
name: name.value,
});
};
const handlePageChange = async (page: number, size: number) => {
await fetchDepartmentWith(
{
name: name.value,
},
page,
size,
);
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="flex flex-col items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
<div
class="w-full max-w-sm p-4 bg-white border border-gray-200 rounded-lg shadow-sm sm:p-6 md:p-8 dark:bg-gray-800 dark:border-gray-700">
<form class="flex flex-col gap-y-4" action="#">
<h5 class="text-xl font-medium text-gray-900 dark:text-white">知路管理后台</h5>
<div>
<label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">用户名</label>
<input type="text" name="email" id="username" v-model="username"
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 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
placeholder="输入任意值" required />
</div>
<div>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">密码</label>
<input type="password" name="password" id="password" v-model="password" placeholder="••••••••"
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 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
required />
</div>
<button type="submit"
class="w-full 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleLogin">登录</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { initFlowbite } from "flowbite";
import { onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { z } from "zod";
import useUserAuth from "../composables/auth/useUserAuth";
import useAlertStore from "../composables/store/useAlertStore";
import { RoutePath } from "../router/constants";
const username = ref("admin");
const password = ref("admin");
const router = useRouter();
const route = useRoute();
const userAuth = useUserAuth();
const alertStore = useAlertStore();
const handleLogin = async () => {
const userSchema = z.object({
username: z.string().min(1, "用户名至少1个字符"),
password: z.string().min(1, "密码至少1个字符"),
});
try {
const validatedData = userSchema.parse({
username: username.value,
password: password.value,
});
await userAuth.signIn(validatedData.username, validatedData.password);
alertStore.showAlert({
level: "success",
content: "登录成功",
});
const redirectPath =
(route.query.redirect as string) ||
`${RoutePath.DASHBOARD}/${RoutePath.USERVIEW}`;
router.push(redirectPath);
} catch (e) {
alertStore.showAlert({
level: "error",
content: e instanceof z.ZodError ? e.errors[0].message : "账号或密码错误",
});
}
};
onMounted(() => {
initFlowbite();
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,17 @@
<script setup lang="ts">
import { RoutePath } from "../router/constants";
</script>
<template>
<main class="grid min-h-full place-items-center bg-white px-6 py-24 sm:py-32 lg:px-8">
<div class="text-center">
<p class="text-base font-semibold text-blue-700">404</p>
<h1 class="mt-4 text-5xl font-semibold tracking-tight text-balance text-gray-900 sm:text-7xl">Page not found</h1>
<p class="mt-6 text-lg font-medium text-pretty text-gray-500 sm:text-xl/8">您访问的资源未找到请点击浏览器后退按钮返回</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.OVERVIEW}`" class="rounded-md px-3.5 py-2.5 text-sm font-semibold bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 text-white shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2">回到主页</RouterLink>
<a href="#" class="text-sm font-semibold text-gray-900">联系我们<span aria-hidden="true">&rarr;</span></a>
</div>
</div>
</main>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,214 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['权限管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">权限管理</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="permissionName"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="权限名" required />
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</div>
</form>
<!-- Create Modal toggle -->
<button @click="handleUpsertPermissionClick(undefined)"
class="flex items-center block gap-x-1 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 absolute right-5 bottom-2"
type="button">
新增权限
</button>
</div>
<table class="w-full text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">权限名称</th>
<th scope="col" class="px-6 py-3">权限编码</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="permission in permissions" :key="permission.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + permission.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + permission.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ permission.name }}
</td>
<td class="px-6 py-4">{{ permission.code }}</td>
<td class="px-6 py-4">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertPermissionClick(permission)"
class="flex items-center block gap-x-1 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 "
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
编辑
</button>
<button
class="flex items-center block gap-x-1
bg-red-700 hover:bg-red-800 focus:outline-none dark:bg-red-600 dark:hover:bg-red-700
focus:ring-red-500 block text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
@click="handleDeletePermissionClick(permission)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<PermissionDeleteModal :id="'permission-delete-modal'" :closeModal="() => {
permissionDeleteModal!.hide();
}" :onSubmit="handleDeleteModalSubmit" title="确定删除该权限吗" content="删除权限"></PermissionDeleteModal>
<PermissionUpsertModal :id="'permission-upsert-modal'" :onSubmit="handleUpsertModalSubmit" :closeModal="() => {
permissionUpsertModal!.hide();
}" :permission="selectedPermission">
</PermissionUpsertModal>
</template>
<script setup lang="ts">
import PermissionUpsertModal from "@/components/PermissionUpsertModal.vue";
import PermissionDeleteModal from "@/components/PopupModal.vue";
import usePermissionDelete from "@/composables/permission/usePermissionDelete";
import type { components } from "@/api/types/schema";
import TablePagination from "@/components/TablePagination.vue";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue";
import usePermissionsQuery from "../composables/permission/usePermissionQuery";
import usePermissionUpsert from "../composables/permission/usePermissionUpsert";
import useAlertStore from "../composables/store/useAlertStore";
import type { PermissionUpsertModel } from "../types/permission";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const permissionName = ref<string>("");
const selectedPermission = ref<components["schemas"]["PermissionRespDto"]>();
const permissionUpsertModal = ref<ModalInterface>();
const permissionDeleteModal = ref<ModalInterface>();
const { total, permissions, fetchPermissionsWith } = usePermissionsQuery();
const { deletePermission } = usePermissionDelete();
const permissionUpsert = usePermissionUpsert();
const alertStore = useAlertStore();
onMounted(async () => {
await fetchPermissionsWith({
name: permissionName.value,
});
initFlowbite();
const $upsertModalElement: HTMLElement | null = document.querySelector(
"#permission-upsert-modal",
);
const $deleteModalElement: HTMLElement | null = document.querySelector(
"#permission-delete-modal",
);
permissionUpsertModal.value = new Modal(
$upsertModalElement,
{},
{ id: "permission-upsert-modal" },
);
permissionDeleteModal.value = new Modal(
$deleteModalElement,
{},
{ id: "permission-delete-modal" },
);
});
const handleUpsertModalSubmit = async (data: PermissionUpsertModel) => {
await permissionUpsert.upsertPermission(data);
await fetchPermissionsWith({
name: permissionName.value,
});
permissionUpsertModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
};
const handleUpsertPermissionClick = async (
permission?: components["schemas"]["PermissionRespDto"],
) => {
selectedPermission.value = permission;
await nextTick(() => {
permissionUpsertModal.value?.show();
});
};
const handleDeleteModalSubmit = async (event: Event) => {
if (!selectedPermission?.value?.id) return;
await deletePermission(selectedPermission.value.id);
permissionDeleteModal.value?.hide();
alertStore.showAlert({
content: "删除成功",
level: "success",
});
};
const handleDeletePermissionClick = async (
permission: components["schemas"]["PermissionRespDto"],
) => {
selectedPermission.value = permission;
await nextTick(() => {
permissionDeleteModal.value?.show();
});
};
const handleSearch = async () => {
await fetchPermissionsWith({
name: permissionName.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchPermissionsWith(
{
name: permissionName.value,
},
page,
pageSize,
);
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,224 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['岗位管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">岗位管理</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4 ">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="name"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="岗位名称" required />
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</div>
</form>
<!-- Create Modal toggle -->
<button @click="handleUpsertPositionClick()"
class="flex items-center block 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 absolute right-5 bottom-2"
type="button">
新增岗位
</button>
</div>
<table class="w-full text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">岗位名称</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="position in positions" :key="position.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + position.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + position.id" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-6 py-4 font-medium text-gray-900 ">
{{ position.name }}
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertPositionClick(position)"
class="flex items-center block gap-x-1 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 "
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
编辑
</button>
<button
class="flex items-center block gap-x-1
bg-red-700 hover:bg-red-800 focus:outline-none dark:bg-red-600 dark:hover:bg-red-700
focus:ring-red-500 block text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
@click="handleDeletePositionClick(position)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :total="total" :pageChange="handlePageChange" />
</div>
<PositionDeleteModal :id="'position-delete-modal'" :closeModal="() => {
positionDeleteModal!.hide();
}" :onSubmit="handleDeletePositionSubmit" title="确定删除该岗位吗" content="删除岗位"></PositionDeleteModal>
<PositionUpsertModal :id="'position-upsert-modal'" :onSubmit="handleUpsertPositionSubmit" :closeModal="() => {
positionUpsertModal!.hide();
}" :position="selectedPosition" :allPositions="allPositions">
</PositionUpsertModal>
</template>
<script setup lang="ts">
import PositionDeleteModal from "@/components/PopupModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import PositionUpsertModal from "@/components/PositionUpsertModal.vue";
import usePositionDelete from "@/composables/position/usePositionDelete";
import { usePositionQuery } from "@/composables/position/usePositionQuery";
import { usePositionUpsert } from "@/composables/position/usePositionUpsert";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue";
import type { components } from "../api/types/schema";
import useAlertStore from "../composables/store/useAlertStore";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const name = ref<string>("");
const selectedPosition = ref<components["schemas"]["Position"]>();
const positionUpsertModal = ref<ModalInterface>();
const positionDeleteModal = ref<ModalInterface>();
const { positions, allPositions, fetchPositionWith, fetchAllPositions, total } =
usePositionQuery();
const { deletePosition } = usePositionDelete();
const { upsertPosition } = usePositionUpsert();
const alertStore = useAlertStore();
onMounted(async () => {
await fetchAllPositions();
await fetchPositionWith({
name: name.value,
});
initFlowbite();
const $upsertModalElement: HTMLElement | null = document.querySelector(
"#position-upsert-modal",
);
const $deleteModalElement: HTMLElement | null = document.querySelector(
"#position-delete-modal",
);
positionUpsertModal.value = new Modal(
$upsertModalElement,
{},
{
id: "position-upsert-modal",
},
);
positionDeleteModal.value = new Modal(
$deleteModalElement,
{},
{
id: "position-delete-modal",
},
);
});
const handleUpsertPositionSubmit = async (
position: components["schemas"]["Position"],
) => {
await upsertPosition(position);
positionUpsertModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
fetchAllPositions();
await fetchPositionWith({
name: name.value,
});
};
const handleUpsertPositionClick = async (
position?: components["schemas"]["Position"],
) => {
selectedPosition.value = position;
await nextTick(() => {
positionUpsertModal.value?.show();
});
};
const handleDeletePositionSubmit = async () => {
if (!selectedPosition?.value?.id) return;
await deletePosition(selectedPosition.value.id);
positionDeleteModal.value?.hide();
alertStore.showAlert({
content: "删除成功",
level: "success",
});
fetchAllPositions();
await fetchPositionWith({
name: name.value,
});
};
const handleDeletePositionClick = async (
position: components["schemas"]["Position"],
) => {
selectedPosition.value = position;
await nextTick(() => {
positionDeleteModal.value?.show();
});
};
const handleSearch = async () => {
await fetchPositionWith({
name: name.value,
});
};
const handlePageChange = async (page: number, size: number) => {
await fetchPositionWith(
{
name: name.value,
},
page,
size,
);
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,233 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['角色管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">角色管理</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="roleName"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="角色名" required />
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</div>
</form>
<!-- Create Modal toggle -->
<button @click="handleUpsertRoleClick(undefined)"
class="flex items-center block gap-x-1 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 absolute right-5 bottom-2"
type="button">
新增角色
</button>
</div>
<table class="w-full text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">角色名称</th>
<th scope="col" class="px-6 py-3">角色编码</th>
<th scope="col" class="px-6 py-3">操作</th>
<th scope="col" class="px-6 py-3">分配</th>
</tr>
</thead>
<tbody>
<tr v-for="role in roles" :key="role.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + role.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + role.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ role.name }}
</td>
<td class="px-6 py-4">{{ role.code }}</td>
<td class="px-6 py-4">
<div class="flex items-center gap-x-2">
<button @click="handleUpsertRoleClick(role)"
class="flex items-center block gap-x-1 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 "
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
编辑
</button>
<button
class="flex items-center block gap-x-1
bg-red-700 hover:bg-red-800 focus:outline-none dark:bg-red-600 dark:hover:bg-red-700
focus:ring-red-500 block text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
@click="handleDeleteRoleClick(role)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
删除
</button>
</div>
</td>
<td class="px-6 py-4">
<div>
<button
class="flex itmes-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5 text-center dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
@click="handleBindPermissionClick(role)" type="button">
分配权限
</button>
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<RoleDeleteModal :id="'role-delete-modal'" :closeModal="() => {
roleDeleteModal!.hide();
}" :onSubmit="handleDeletedModalSubmit" title="确定删除该角色吗" content="删除角色"></RoleDeleteModal>
<RoleUpsertModal :onSubmit="handleUpsertModalSubmit" :closeModal="() => {
roleUpsertModal!.hide();
}" :role="selectedRole">
</RoleUpsertModal>
</template>
<script setup lang="ts">
import RoleDeleteModal from "@/components/PopupModal.vue";
import RoleUpsertModal from "@/components/RoleUpsertModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import useRoleDelete from "@/composables/role/useRoleDelete";
import { useRolesQuery } from "@/composables/role/useRolesQuery";
import { RouteName } from "@/router/constants";
import type { RoleUpsertModel } from "@/types/role";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import type { components } from "../api/types/schema";
import { useRoleUpsert } from "../composables/role/useRoleUpsert";
import useAlertStore from "../composables/store/useAlertStore";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const roleName = ref<string>("");
const selectedRole = ref<components["schemas"]["RoleDto"]>();
const roleUpsertModal = ref<ModalInterface>();
const roleDeleteModal = ref<ModalInterface>();
const { total, roles, fetchRolesWith } = useRolesQuery();
const { deleteRole } = useRoleDelete();
const alertStore = useAlertStore();
const router = useRouter();
const upsertRole = useRoleUpsert();
onMounted(async () => {
await fetchRolesWith({
name: roleName.value,
});
initFlowbite();
const $upsertModalElement: HTMLElement | null =
document.querySelector("#role-upsert-modal");
const $deleteModalElement: HTMLElement | null =
document.querySelector("#role-delete-modal");
roleUpsertModal.value = new Modal(
$upsertModalElement,
{},
{ id: "role-upsert-modal" },
);
roleDeleteModal.value = new Modal(
$deleteModalElement,
{},
{ id: "role-delete-modal" },
);
});
const handleUpsertModalSubmit = async (data: RoleUpsertModel) => {
await upsertRole.upsertRole(data);
await fetchRolesWith({
name: roleName.value,
});
roleUpsertModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
};
const handleUpsertRoleClick = async (
role?: components["schemas"]["RoleDto"],
) => {
selectedRole.value = role;
await nextTick(() => {
roleUpsertModal.value?.show();
});
};
const handleDeletedModalSubmit = async () => {
if (!selectedRole?.value?.id) return;
await deleteRole(selectedRole.value.id);
await fetchRolesWith({
name: roleName.value,
});
roleDeleteModal.value?.hide();
alertStore.showAlert({
content: "删除成功",
level: "success",
});
};
const handleDeleteRoleClick = async (
role: components["schemas"]["RoleDto"],
) => {
selectedRole.value = role;
await nextTick(() => {
roleDeleteModal.value?.show();
});
};
const handleBindPermissionClick = async (
role: components["schemas"]["RoleDto"],
) => {
router.push({
name: RouteName.BINDPERMISSIONVIEW,
params: { roleId: role.id },
});
};
const handleSearch = async () => {
await fetchRolesWith({
name: roleName.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchRolesWith(
{
name: roleName.value,
},
page,
pageSize,
);
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,277 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['任务管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">任务管理</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4 ">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="jobName"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="任务名称" required />
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</div>
</form>
<!-- Create Modal toggle -->
</div>
<table
class="w-full text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 dark:text-gray-400 overflow-x-auto">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">任务</th>
<th scope="col" class="px-6 py-3">触发器</th>
<th scope="col" class="px-6 py-3">开始</th>
<th scope="col" class="px-6 py-3">结束</th>
<th scope="col" class="px-6 py-3">下次执行</th>
<th scope="col" class="px-6 py-3">上次执行</th>
<th scope="col" class="px-6 py-3">类型</th>
<th scope="col" class="px-6 py-3">Cron</th>
<th scope="col" class="px-6 py-3">状态</th>
<th scope="col" class="px-6 py-3">编辑</th>
<th scope="col" class="px-6 py-3">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="job in jobs" :key="job.triggerName"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + job.name" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + job.name" class="sr-only">checkbox</label>
</div>
</td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{{
`${job.name}:${job.group}` }}</td>
<td class="px-6 py-4">{{ `${job.triggerName}:${job.triggerGroup}` }}</td>
<td class="px-6 py-4">{{ new Date(job.startTime!).toLocaleString() }}</td>
<td class="px-6 py-4">{{ job.endTime ? new Date(job.endTime).toLocaleString() : undefined }}</td>
<td class="px-6 py-4">{{ job.nextFireTime ? new Date(job.nextFireTime).toLocaleString() : undefined}}</td>
<td class="px-6 py-4">{{ job.previousFireTime && job.previousFireTime > 0 ? new
Date(job.previousFireTime).toLocaleString() :
undefined
}}
</td>
<td class="px-6 py-4">{{ job.schedulerType }}</td>
<td class="px-6 py-4">{{ job.cronExpression }}</td>
<td class="px-6 py-4">{{ job.triggerState }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-x-2">
<button @click="handleCronUpdateClick(job)" :disabled="job.schedulerType !== 'CRON'"
:class="['flex items-center gap-x-1 block 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800' , { 'opacity-50 cursor-not-allowed': job.schedulerType !== 'CRON' }]"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
编辑
</button>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-x-2">
<button
:class="['text-white bg-green-700 hover:bg-green-800 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-900 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center']"
@click="handleResumeJobClick(job)" type="button">
恢复
</button>
<button
:class="['bg-red-700 hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center']"
@click="handlePauseJobClick(job)" type="button">
暂停
</button>
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<PopupModal :id="'job-resume-modal'" :closeModal="() => {
jobResumeModal!.hide();
}" :onSubmit="handleResumeModalSubmit" title="确定恢复该任务吗?" content="恢复任务"></PopupModal>
<PopupModal :id="'job-pause-modal'" :closeModal="() => {
jobPauseModal!.hide();
}" :onSubmit="handlePauseModalSubmit" title="确定暂停该任务吗" content="暂停任务"></PopupModal>
<SchedulerUpdateModal :job="selectedJob" :id="'job-update-modal'" :closeModal="() => {
jobUpdateModal!.hide();
}" :onSubmit="handleUpdateModalSubmit"></SchedulerUpdateModal>
</template>
<script setup lang="ts">
import SchedulerUpdateModal from "@/components/SchedulerUpdateModal.vue";
import PopupModal from "@/components/PopupModal.vue";
import TablePagination from "@/components/TablePagination.vue";
import { useJobControl } from "@/composables/job/useJobControl";
import { useJobsPaginationQuery } from "@/composables/job/useJobQuery";
import { useJobUpdate } from "@/composables/job/useJobUpdate";
import useAlertStore from "@/composables/store/useAlertStore";
import { RouteName } from "@/router/constants";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue";
import type { components } from "../api/types/schema";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const jobName = ref<string>("");
const jobResumeModal = ref<ModalInterface>();
const jobPauseModal = ref<ModalInterface>();
const jobUpdateModal = ref<ModalInterface>();
const selectedJob = ref<components["schemas"]["JobTriggerDto"]>();
const { jobs, total, fetchJobsWith } = useJobsPaginationQuery();
const alertStore = useAlertStore();
const { resumeTrigger, pauseTrigger } = useJobControl();
const { updateCron } = useJobUpdate();
const handleResumeJobClick = async (
currentJob: components["schemas"]["JobTriggerDto"],
) => {
selectedJob.value = currentJob;
await nextTick(() => {
jobResumeModal.value?.show();
});
};
const handleCronUpdateClick = async (
currentJob: components["schemas"]["JobTriggerDto"],
) => {
selectedJob.value = currentJob;
await nextTick(() => {
jobUpdateModal.value?.show();
});
};
const handlePauseJobClick = async (
currentJob: components["schemas"]["JobTriggerDto"],
) => {
selectedJob.value = currentJob;
await nextTick(() => {
jobPauseModal.value?.show();
});
};
const handleResumeModalSubmit = async () => {
await resumeTrigger({
triggerName: selectedJob.value!.triggerName!,
triggerGroup: selectedJob.value!.triggerGroup!,
});
jobResumeModal.value?.hide();
alertStore.showAlert({
level: "success",
content: "操作成功",
});
await fetchJobsWith({
name: jobName.value,
});
};
const handleUpdateModalSubmit = async (cronExpression: string) => {
await updateCron({
triggerName: selectedJob.value!.triggerName!,
triggerGroup: selectedJob.value!.triggerGroup!,
cron: cronExpression,
});
jobUpdateModal.value?.hide();
alertStore.showAlert({
level: "success",
content: "操作成功",
});
await fetchJobsWith({
name: jobName.value,
});
};
const handlePauseModalSubmit = async () => {
await pauseTrigger({
triggerName: selectedJob!.value!.triggerName!,
triggerGroup: selectedJob!.value!.triggerGroup!,
});
jobPauseModal.value?.hide();
alertStore.showAlert({
level: "success",
content: "操作成功",
});
await fetchJobsWith({
name: jobName.value,
});
};
const handleSearch = async () => {
await fetchJobsWith({
name: jobName.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchJobsWith(
{
name: jobName.value,
},
page,
pageSize,
);
};
onMounted(async () => {
await fetchJobsWith({
name: jobName.value,
});
initFlowbite();
const $jobResumeModalElement: HTMLElement | null =
document.querySelector("#job-resume-modal");
const $jobPauseModalElement: HTMLElement | null =
document.querySelector("#job-pause-modal");
const $jobUpdateModalElement: HTMLElement | null =
document.querySelector("#job-update-modal");
jobResumeModal.value = new Modal(
$jobResumeModalElement,
{},
{
id: "job-resume-modal",
},
);
jobPauseModal.value = new Modal(
$jobPauseModalElement,
{},
{
id: "job-pause-modal",
},
);
jobUpdateModal.value = new Modal(
$jobUpdateModalElement,
{},
{
id: "job-update-modal",
},
);
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,154 @@
<template>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900 ">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumbs :names="['用户设置']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">用户设置</h1>
</div>
<!-- Right Content -->
<div class="col-span-full xl:col-auto">
<div
class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<div class="items-center sm:flex xl:block 2xl:flex sm:space-x-4 xl:space-x-0 2xl:space-x-4">
<img class="mb-4 rounded-lg w-28 h-28 sm:mb-0 xl:mb-4 2xl:mb-0" src="/trump.jpg" alt="Jese picture">
<div>
<h3 class="mb-1 text-xl font-bold text-gray-900 dark:text-white">个人资料</h3>
<div class="mb-4 text-sm text-gray-500 dark:text-gray-400">
JPG, GIF or PNG. Max size of 800K
</div>
<div class="flex items-center space-x-4">
<button type="button" disabled
class="cursor-not-allowed inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white rounded-lg bg-blue-400 dark:bg-blue-500 ">
<svg class="w-4 h-4 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M5.5 13a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 13H11V9.413l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13H5.5z">
</path>
<path d="M9 13h2v5a1 1 0 11-2 0v-5z"></path>
</svg>
Upload picture
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-span-1 row-start-3">
<div
class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">个人信息</h3>
<form action="#">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 ">
<label for="current-username"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">用户名</label>
<input type="text" name="current-username" id="current-username" v-model="userForm.username"
class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
required>
</div>
<div class="col-span-6 ">
<label for="current-password"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">密码</label>
<input type="password" name="current-password" id="current-password" v-model="userForm.password"
class="shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
placeholder="非必填" required>
</div>
<div class="col-span-6 ">
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">确认密码</label>
<input type="password" id="password" v-model="userForm.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 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="非必填" required>
</div>
<div class="col-span-6 ">
<label for="category" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">状态</label>
<select id="category" v-model="userForm.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 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<option :value=true>启用</option>
<option :value=false>禁用</option>
</select>
</div>
<div class="col-span-6 sm:col-full">
<button
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
@click.prevent="handleUpdateClick" type="submit">保存</button>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useUserAuth from "@/composables/auth/useUserAuth";
import useUserStore from "@/composables/store/useUserStore";
import { initFlowbite } from "flowbite";
import { onMounted, ref } from "vue";
import { z } from "zod";
import useAlertStore from "../composables/store/useAlertStore";
import { RouteName } from "../router/constants";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const { user } = useUserStore();
const { upsertCurrentUser } = useUserAuth();
const alertStore = useAlertStore();
const userForm = ref({
username: user.username,
password: user.password,
enable: user.enable,
confirmPassword: user.password,
});
onMounted(() => {
initFlowbite();
});
const handleUpdateClick = async () => {
let validatedData = undefined;
try {
validatedData = z
.object({
username: z
.string({
message: "用户名不能为空",
})
.min(4, "用户名长度不能小于4个字符"),
password: z
.string()
.min(5, "密码长度不能小于5个字符")
.optional()
.nullable(),
confirmPassword: z.string().optional().nullable(),
enable: z.boolean({
message: "状态不能为空",
}),
})
.refine(
(data) => {
if (data.password) {
return data.password === data.confirmPassword;
}
return true;
},
{ message: "密码输入不一致。" },
)
.parse(userForm.value);
await upsertCurrentUser(validatedData);
alertStore.showAlert({
content: "操作成功",
level: "success",
});
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
</script>

View File

@@ -0,0 +1,288 @@
<template>
<div class="relative overflow-x-auto px-4 pt-6 xl:grid-cols-3 xl:gap-4 sm:rounded-lg">
<div class="mb-4 col-span-full">
<Breadcrumbs :names="['用户管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">用户管理</h1>
</div>
<div class="relative">
<form class="max-w-sm mb-4 ">
<label for="default-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
</svg>
</div>
<input type="search" id="default-search" v-model="username"
class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="用户名" required />
<button type="submit"
class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
@click.prevent="handleSearch">搜索</button>
</div>
</form>
<!-- Create Modal toggle -->
<button @click="handleUpsertUserClick(undefined)"
class="flex items-center block 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 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 absolute right-5 bottom-2"
type="button">
新增用户
</button>
</div>
<table class="w-full text-sm text-left rtl:text-right shadow-lg rounded-lg text-gray-500 dark:text-gray-400">
<thead class="text-xs uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="p-4">
<div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="checkbox-all-search" class="sr-only">checkbox</label>
</div>
</th>
<th scope="col" class="px-6 py-3">用户名</th>
<th scope="col" class="px-6 py-3">邮箱</th>
<th scope="col" class="px-6 py-3">创建时间</th>
<th scope="col" class="px-6 py-3">状态</th>
<th scope="col" class="px-6 py-3">操作</th>
<th scope="col" class="px-6 py-3">分配</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id"
class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600">
<td class="w-4 p-4">
<div class="flex items-center">
<input :id="'checkbox-table-search-' + user.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label :for="'checkbox-table-search-' + user.id" class="sr-only">checkbox</label>
</div>
</td>
<td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ user.username }}
</td>
<td class="px-6 py-4">
{{ user.username }}
</td>
<td class="px-6 py-4">
{{ user.createTime }}
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="user.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{
user.enable === true ? "启用" : "禁用" }}
</div>
</td>
<td class="px-6 py-4">
<!-- Edit Modal toggle -->
<div class="flex items-center gap-x-2">
<button @click="handleUpsertUserClick(user)"
class="flex items-center block gap-x-1 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-4 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
编辑
</button>
<button
class="flex items-center block gap-x-1
bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700
dark:focus:ring-red-900 block text-white focus:ring-4 focus:outline-nonefont-medium rounded-lg text-sm px-4 py-2.5 text-center"
@click="handleDeleteUserClick(user)" type="button">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
删除
</button>
</div>
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-x-2">
<button
class="flex itmes-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5 text-center dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
@click="handleBindRoleClick(user)" type="button">
分配角色
</button>
<button
class="flex itmes-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5 text-center dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
@click="handleBindPositionClick(user)" type="button">
分配岗位
</button>
<button
class="flex itmes-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5 text-center dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
@click="handleBindDepartmentClick(user)" type="button">
分配部门
</button>
</div>
</td>
</tr>
</tbody>
</table>
<TablePagination :pageChange="handlePageChange" :total="total" />
</div>
<UserDeleteModal :id="'user-delete-modal'" :closeModal="() => {
userDeleteModal!.hide();
}" :onSubmit="handleDeleteUserSubmit" title="确定删除该用户吗" content="删除用户"></UserDeleteModal>
<UserUpsertModal :id="'user-upsert-modal'" :onSubmit="handleUpsertUserSubmit" :closeModal="() => {
userUpsertModal!.hide();
}" :user="selectedUser">
</UserUpsertModal>
</template>
<script setup lang="ts">
import UserDeleteModal from "@/components/PopupModal.vue";
import UserUpsertModal from "@/components/UserUpsertModal.vue";
import useUserDelete from "@/composables/user/useUserDelete";
import { useUserQuery } from "@/composables/user/useUserQuery";
import { RouteName } from "@/router/constants";
import type { UserUpsertSubmitModel } from "@/types/user";
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import type { components } from "../api/types/schema";
import useAlertStore from "../composables/store/useAlertStore";
import { useUserUpsert } from "../composables/user/useUserUpsert";
import TablePagination from "@/components/TablePagination.vue";
import Breadcrumbs from "@/components/Breadcrumbs.vue";
const username = ref<string>("");
const selectedUser = ref<components["schemas"]["UserRolePermissionDto"]>();
const userUpsertModal = ref<ModalInterface>();
const userDeleteModal = ref<ModalInterface>();
const router = useRouter();
const { total, users, fetchUsersWith } = useUserQuery();
const { deleteUser } = useUserDelete();
const userUpsert = useUserUpsert();
const alertStore = useAlertStore();
onMounted(async () => {
await fetchUsersWith({
username: username.value,
});
initFlowbite();
const $upsertModalElement: HTMLElement | null =
document.querySelector("#user-upsert-modal");
const $deleteModalElement: HTMLElement | null =
document.querySelector("#user-delete-modal");
userUpsertModal.value = new Modal(
$upsertModalElement,
{},
{
id: "user-upsert-modal",
},
);
userDeleteModal.value = new Modal(
$deleteModalElement,
{},
{
id: "user-delete-modal",
},
);
});
const handleUpsertUserSubmit = async (data: UserUpsertSubmitModel) => {
await userUpsert.upsertUser(data);
userUpsertModal.value?.hide();
alertStore.showAlert({
content: "操作成功",
level: "success",
});
await fetchUsersWith({
username: username.value,
});
};
const handleUpsertUserClick = async (
user?: components["schemas"]["UserRolePermissionDto"],
) => {
selectedUser.value = user;
await nextTick(() => {
userUpsertModal.value?.show();
});
};
const handleBindRoleClick = async (
user: components["schemas"]["UserRolePermissionDto"],
) => {
router.push({
name: RouteName.BINDROLEVIEW,
params: {
userId: user.id,
},
});
};
const handleBindDepartmentClick = async (
user: components["schemas"]["UserRolePermissionDto"],
) => {
router.push({
name: RouteName.BINDDEPARTMENTVIEW,
params: {
userId: user.id,
},
});
};
const handleBindPositionClick = async (
user: components["schemas"]["UserRolePermissionDto"],
) => {
router.push({
name: RouteName.BINDPOSITIONVIEW,
params: {
userId: user.id,
},
});
};
const handleDeleteUserSubmit = async () => {
if (!selectedUser?.value?.id) return;
await deleteUser(selectedUser.value.id);
userDeleteModal.value?.hide();
alertStore.showAlert({
content: "删除成功",
level: "success",
});
await fetchUsersWith({
username: username.value,
});
};
const handleDeleteUserClick = async (
user: components["schemas"]["UserRolePermissionDto"],
) => {
selectedUser.value = user;
await nextTick(() => {
userDeleteModal.value?.show();
});
};
const handleSearch = async () => {
await fetchUsersWith({
username: username.value,
});
};
const handlePageChange = async (page: number, pageSize: number) => {
await fetchUsersWith(
{
username: username.value,
},
page,
pageSize,
);
};
</script>
<style scoped></style>