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

11
frontend/src/App.vue Normal file
View File

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

View File

@@ -0,0 +1,69 @@
import createClient, { type Middleware } from "openapi-fetch";
import useAuthStore from "../composables/store/useAuthStore";
import {
ForbiddenError,
SystemError,
UnAuthError,
InternalServerError,
} from "../types/error";
import type { paths } from "./types/schema"; // generated by openapi-typescript
const myMiddleware: Middleware = {
onRequest({ request, options }) {
const authStore = useAuthStore();
request.headers.set("Authorization", authStore.get());
return request;
},
async onResponse({ request, response, options }) {
const { body, ...resOptions } = response;
if (response.status >= 400 && response.status < 500) {
if (response.status === 401) {
handleAuthError(response);
} else if (response.status === 403) {
handleForbiddenError(response);
} else {
handleSystemError(response);
}
} else if (response.status >= 500) {
await handleBusinessError(response);
} else {
return response;
}
},
async onError({ error }) {
// wrap errors thrown by fetch
return;
},
};
const client = createClient<paths>({
baseUrl: `${import.meta.env.VITE_BASE_URL}`,
querySerializer: {
object: {
style: "form",
explode: true,
},
},
});
// register middleware
client.use(myMiddleware);
const handleAuthError = (response: Response) => {
throw new UnAuthError(response.status);
};
const handleForbiddenError = (response: Response) => {
throw new ForbiddenError(response.status);
};
const handleSystemError = (response: Response) => {
throw new SystemError(response.status);
};
const handleBusinessError = async (response: Response) => {
const data = await response.json();
throw new InternalServerError(response.status, data.detail);
};
export default client;

View File

@@ -0,0 +1,10 @@
import { faker } from "@faker-js/faker";
import { http, HttpResponse } from "msw";
export default [
http.post("/auth/sign-in", () => {
const response = HttpResponse.json();
response.headers.set("Authorization", faker.string.alpha(16));
return response;
}),
];

View File

@@ -0,0 +1,39 @@
import { faker } from "@faker-js/faker";
import { http, HttpResponse } from "msw";
export default [
http.get("/department/page-query", () => {
const generateDepartment = () => ({
id: faker.number.int({ min: 1, max: 100 }),
name: faker.company.name(),
parentId: faker.number.int({ min: 1, max: 100 }),
isBound: faker.datatype.boolean(),
parentName: faker.company.name(),
});
const mockData = {
data: faker.helpers.multiple(generateDepartment, { count: 10 }),
total: 30,
};
return HttpResponse.json(mockData);
}),
http.get("/department/query", () => {
const generateDepartment = () => ({
id: faker.number.int({ min: 1, max: 30 }),
name: faker.company.name(),
parentId: faker.number.int({ min: 1, max: 30 }),
parentName: faker.company.name(),
});
const mockData = faker.helpers.multiple(generateDepartment, { count: 30 });
return HttpResponse.json(mockData);
}),
http.post("/department", () => {
console.log("Captured department upsert");
return HttpResponse.json();
}),
http.delete("/department", () => {
console.log("Captured department delete");
return HttpResponse.json();
}),
];

View File

@@ -0,0 +1,43 @@
import { faker } from "@faker-js/faker";
import { http, HttpResponse } from "msw";
export default [
http.get("/iam/permissions", () => {
const generatePermission = () => ({
id: faker.number.int({ min: 1, max: 20 }),
code: `perm_${faker.lorem.words({ min: 1, max: 1 })}`,
name: faker.lorem.words({ min: 1, max: 1 }),
isBound: faker.datatype.boolean(),
});
const mockData = {
data: faker.helpers.multiple(generatePermission, { count: 10 }),
total: 20,
};
return HttpResponse.json(mockData);
}),
http.post("/iam/permission", async ({ request }) => {
console.log('Captured a "POST /posts" request');
return HttpResponse.json();
}),
http.delete("/iam/permission", ({ params }) => {
console.log(`Captured a "DELETE /posts/${params.id}" request`);
return HttpResponse.json();
}),
http.post("/iam/roles/:roleId/bind-permission", ({ params, request }) => {
console.log(
`Captured a "POST /urp/roles/${params.roleId}/bind-permission" request`,
);
return HttpResponse.json();
}),
http.post("/iam/roles/:roleId/unbind-permission", ({ params, request }) => {
console.log(
`Captured a "POST /urp/roles/${params.roleId}/unbind-permission" request`,
);
return HttpResponse.json();
}),
];

View File

@@ -0,0 +1,35 @@
import { faker } from "@faker-js/faker";
import { http, HttpResponse } from "msw";
export default [
http.get("/position/page-query", () => {
const generatePosition = () => ({
id: faker.number.int({ min: 1, max: 100 }),
name: faker.person.jobTitle(),
isBound: faker.datatype.boolean(),
});
const mockData = {
data: faker.helpers.multiple(generatePosition, { count: 10 }),
total: 30,
};
return HttpResponse.json(mockData);
}),
http.get("/position/query", () => {
const generatePosition = () => ({
id: faker.number.int({ min: 1, max: 30 }),
name: faker.person.jobTitle(),
});
const mockData = faker.helpers.multiple(generatePosition, { count: 30 });
return HttpResponse.json(mockData);
}),
http.post("/position", () => {
console.log("Captured position upsert");
return HttpResponse.json();
}),
http.delete("/position", () => {
console.log("Captured position delete");
return HttpResponse.json();
}),
];

View File

@@ -0,0 +1,77 @@
import { faker } from "@faker-js/faker";
import { http, HttpResponse } from "msw";
export default [
http.get("/iam/roles", () => {
const generatePermission = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: `perm_${faker.lorem.word()}`,
name: faker.lorem.words({ min: 1, max: 3 }),
});
const generateRole = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: faker.helpers.arrayElement([
"admin",
"editor",
"viewer",
"manager",
]),
name: faker.person.jobTitle(),
isBound: faker.datatype.boolean(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
});
const mockData = {
data: faker.helpers.multiple(generateRole, { count: 10 }),
total: 20,
};
return HttpResponse.json(mockData);
}),
http.get("/iam/role", ({ params }) => {
const generatePermission = () => ({
id: faker.number.int({ min: 1, max: 10 }),
code: `perm_${faker.lorem.word()}`,
name: faker.lorem.words({ min: 1, max: 3 }),
});
const generateRole = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: faker.helpers.arrayElement([
"admin",
"editor",
"viewer",
"manager",
]),
name: faker.person.jobTitle(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
});
return HttpResponse.json(generateRole());
}),
http.post("/iam/role", async ({ request }) => {
console.log('Captured a "POST /urp/role" request');
return HttpResponse.json();
}),
http.post("/iam/permission/bind", async ({ request }) => {
console.log('Captured a "POST /iam/permission/bind" request');
return HttpResponse.json();
}),
http.post("/iam/permission/unbind", async ({ request }) => {
console.log('Captured a "POST /iam/permission/unbind" request');
return HttpResponse.json();
}),
http.delete("/iam/role", ({ params }) => {
console.log(`Captured a "DELETE /urp/role ${params.id}" request`);
return HttpResponse.json();
}),
];

View File

@@ -0,0 +1,53 @@
import { faker } from "@faker-js/faker";
import { http, HttpResponse } from "msw";
export default [
http.get("/scheduler/page-query", () => {
const generateJobs = () => ({
name: faker.word.sample(),
group: faker.helpers.arrayElement(["default", "system", "custom"]),
className: `com.example.jobs.${faker.word.sample()}Job`,
jobDataMap: {
dirty: faker.datatype.boolean(),
allowsTransientData: faker.datatype.boolean(),
keys: faker.helpers.multiple(() => faker.word.sample(), { count: 3 }),
empty: false,
wrappedMap: {},
},
triggerName: faker.word.sample(),
triggerGroup: faker.helpers.arrayElement(["DEFAULT", "SYSTEM"]),
schedulerType: faker.helpers.arrayElement(["CRON", "SIMPLE"]),
triggerState: faker.helpers.arrayElement(["PAUSE", "WAITING"]),
cronExpression: "0 0/30 * * * ?",
startTime: faker.date.past().getTime(),
endTime: faker.date.future().getTime(),
nextFireTime: faker.date.soon().getTime(),
previousFireTime: faker.date.recent().getTime(),
triggerJobDataMap: {
dirty: faker.datatype.boolean(),
allowsTransientData: true,
keys: [],
empty: true,
wrappedMap: {},
},
});
const mockData = {
data: faker.helpers.multiple(generateJobs, { count: 20 }),
total: 20,
};
return HttpResponse.json(mockData);
}),
http.post("/scheduler/trigger/resume", () => {
console.log('Captured a "POST /scheduler/trigger/resume" request');
return HttpResponse.json();
}),
http.post("/scheduler/trigger/pause", () => {
console.log('Captured a "POST /scheduler/trigger/pause" request');
return HttpResponse.json();
}),
http.put("/scheduler/job/update", () => {
console.log('Captured a "POST /scheduler/job/update" request');
return HttpResponse.json();
}),
];

View File

@@ -0,0 +1,17 @@
import { setupWorker } from "msw/browser";
import authHandlers from "./authHandlers";
import jobHandlers from "./schedulerHandlers";
import permissionHandlers from "./permissionHandlers";
import roleHandlers from "./roleHandlers";
import userHandlers from "./userHandlers";
import departmentHandlers from "./departmentHandlers";
import positionHandlers from "./positionHandlers";
export const worker = setupWorker(
...userHandlers,
...authHandlers,
...roleHandlers,
...permissionHandlers,
...jobHandlers,
...departmentHandlers,
...positionHandlers,
);

View File

@@ -0,0 +1,188 @@
import { faker } from "@faker-js/faker";
import { http, HttpResponse } from "msw";
import { ROLE } from "../../router/constants";
export default [
http.get("/iam/user", () => {
const generatePermission = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: `perm_${faker.lorem.words({ min: 1, max: 1 })}`,
name: faker.lorem.words({ min: 1, max: 1 }),
});
const generateRole = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: faker.helpers.arrayElement([
ROLE.ADMIN,
"editor",
"viewer",
"manager",
]),
name: faker.person.jobTitle(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
});
const generateDepartment = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: `dept_${faker.lorem.word()}`,
name: faker.company.name(),
parentId: faker.number.int({ min: 1, max: 30 }),
enable: faker.datatype.boolean(),
});
const generateUser = () => ({
id: faker.number.int({ min: 1, max: 100 }),
username: faker.internet.email(),
password: faker.internet.password(),
enable: faker.datatype.boolean(),
roles: faker.helpers.multiple(generateRole, {
count: { min: 1, max: 3 },
}),
createTime: faker.date.recent({ days: 30 }).toISOString(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
departments: faker.helpers.multiple(generateDepartment, {
count: { min: 0, max: 3 },
}),
});
return HttpResponse.json(generateUser());
}),
http.get("/iam/users", () => {
const generatePermission = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: `perm_${faker.lorem.word()}`,
name: faker.lorem.words({ min: 1, max: 3 }),
});
const generateRole = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: [ROLE.ADMIN, "editor", "viewer", "manager"],
name: faker.person.jobTitle(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
});
const generateDepartment = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: `dept_${faker.lorem.word()}`,
name: faker.company.name(),
parentId: faker.number.int({ min: 1, max: 30 }),
enable: faker.datatype.boolean(),
});
const generateUser = () => ({
id: faker.number.int({ min: 1, max: 100 }),
username: faker.internet.email(),
password: faker.internet.password(),
enable: faker.datatype.boolean(),
roles: faker.helpers.multiple(generateRole, {
count: { min: 1, max: 3 },
}),
createTime: faker.date.recent({ days: 30 }).toISOString(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
departments: faker.helpers.multiple(generateDepartment, {
count: { min: 0, max: 3 },
}),
});
const mockData = {
data: faker.helpers.multiple(generateUser, { count: 10 }),
total: 30,
};
return HttpResponse.json(mockData);
}),
http.post("/api/users/:userId/departments", () => {
console.log('Captured a "POST /api/users/:userId/departments" request');
return HttpResponse.json({ success: true });
}),
http.delete("/api/users/:userId/departments", () => {
console.log('Captured a "DELETE /api/users/:userId/departments" request');
return HttpResponse.json({ success: true });
}),
http.post("/iam/user", () => {
console.log('Captured a "POST /posts" request');
return HttpResponse.json();
}),
http.delete("/iam/user", ({ params }) => {
console.log(`Captured a "DELETE /posts/${params.id}" request`);
return HttpResponse.json();
}),
http.post("/iam/me", () => {
console.log('Captured a "POST /posts" request');
return HttpResponse.json();
}),
http.post("/iam/role/bind", () => {
console.log('Captured a "POST /posts" request');
return HttpResponse.json();
}),
http.post("/iam/role/unbind", () => {
console.log('Captured a "POST /posts" request');
return HttpResponse.json();
}),
http.get("/iam/me", () => {
const generatePermission = () => ({
id: faker.number.int({ min: 1, max: 1000 }),
code: `perm_${faker.lorem.word()}`,
name: faker.lorem.words({ min: 1, max: 3 }),
});
const generateRole = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: [ROLE.ADMIN, "editor", "viewer", "manager"],
name: faker.person.jobTitle(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
});
const generateDepartment = () => ({
id: faker.number.int({ min: 1, max: 100 }),
code: `dept_${faker.lorem.word()}`,
name: faker.company.name(),
parentId: faker.number.int({ min: 1, max: 30 }),
enable: faker.datatype.boolean(),
});
const generateUser = () => ({
id: faker.number.int({ min: 1, max: 100 }),
username: faker.internet.email(),
password: faker.internet.password(),
enable: faker.datatype.boolean(),
roles: faker.helpers.multiple(generateRole, {
count: { min: 1, max: 3 },
}),
createTime: faker.date.recent({ days: 30 }).toISOString(),
permissions: faker.helpers.multiple(generatePermission, {
count: { min: 1, max: 5 },
}),
departments: faker.helpers.multiple(generateDepartment, {
count: { min: 0, max: 3 },
}),
});
const mockData = generateUser();
return HttpResponse.json(mockData);
}),
http.post("/department/unbind", () => {
console.log("Captured a 'POST /department/unbind' request");
return HttpResponse.json();
}),
http.post("/department/bind", () => {
console.log("Captured a 'POST /department/bind' request");
return HttpResponse.json();
}),
http.post("/iam/position/bind", () => {
console.log('Captured a "POST /posts" request');
return HttpResponse.json();
}),
http.post("/iam/position/unbind", () => {
console.log('Captured a "POST /posts" request');
return HttpResponse.json();
}),
];

File diff suppressed because it is too large Load Diff

1512
frontend/src/api/types/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
/* color palette from <https://github.com/vuejs/theme> */
@import "tailwindcss";
@import "flowbite/src/themes/default";
@plugin "flowbite/plugin";
@source "../node_modules/flowbite";
@source "../node_modules/flowbite-datepicker";
@theme {
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;
--font-sans:
"Inter", "ui-sans-serif", "system-ui", "-apple-system", "system-ui", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-body:
"Inter", "ui-sans-serif", "system-ui", "-apple-system", "system-ui", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-mono: "ui-monospace", "SFMono-Regular", "Menlo", "Monaco", "Consolas",
"Liberation Mono", "Courier New", "monospace";
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1 @@
@import "./base.css";

View File

@@ -0,0 +1,15 @@
<template>
<div id="globalAlert" :class="['flex space-x-2 items-center rounded-lg p-4 mb-4 text-sm fixed top-8 right-5 transition-all duration-200 ease-out z-50', alertStore.levelClassName, alertStore.alertStorage.isShow ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full']" role="alert">
<svg v-if="alertStore.alertStorage.level==='info'" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info-icon lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
<svg v-else-if="alertStore.alertStorage.level === 'warning'" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
<svg v-if="alertStore.alertStorage.level === 'success'" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
<svg v-if="alertStore.alertStorage.level === 'error'" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
<span class="font-medium">{{ alertStore.alertStorage.content }}</span>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "../composables/store/useAlertStore";
const alertStore = useAlertStore();
</script>

View File

@@ -0,0 +1,36 @@
<template>
<nav class="flex mb-5" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 text-sm font-medium md:space-x-2">
<li class="inline-flex items-center">
<RouterLink :to="{name: RouteName.USERVIEW}"
class="inline-flex items-center text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-white">
<svg class="w-5 h-5 mr-2.5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z">
</path>
</svg>
首页
</RouterLink>
</li>
<li v-for="name in names" :key="name">
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
<span class="ml-1 text-gray-400 hover:text-primary-600 md:ml-2 dark:text-gray-500 dark:hover:text-white">{{
name }}</span>
</div>
</li>
</ol>
</nav>
</template>
<script setup lang="ts">
import { RouteName } from "@/router/constants";
const { names } = defineProps<{
names: string[];
}>();
</script>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
import Headbar from "./Headbar.vue";
import Sidebar from "./Sidebar.vue";
</script>
<template>
<Headbar></Headbar>
<Sidebar>
</Sidebar>
<article class="ml-64 mt-14">
<RouterView></RouterView>
</article>
</template>

View File

@@ -0,0 +1,113 @@
<template>
<!-- Main modal -->
<div id="user-upsert-modal" tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 dark:bg-gray-900/80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm dark:bg-gray-700">
<!-- Modal header -->
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600 border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
部门管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">部门名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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"
required />
</div>
<div class="col-span-2">
<label for="category" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">上级部门</label>
<select id="category" v-model="formData.parentId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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 v-for="department in allDepartments" :key="department.id" :value="department.id"
:selected="department.id === formData.parentId">{{
department.name
}}</option>
</select>
</div>
</div>
<button type="submit" @click="handleSubmit"
class="text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 self-start mt-5">
保存
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
import type { DepartmentUpsertModel } from "../types/department";
const alertStore = useAlertStore();
const { department, allDepartments, onSubmit } = defineProps<{
department?: components["schemas"]["Department"];
allDepartments: components["schemas"]["Department"][];
closeModal: () => void;
onSubmit: (department: DepartmentUpsertModel) => Promise<void>;
}>();
const formData = ref();
watch(
() => department,
(newDepartment) => {
formData.value = {
id: newDepartment?.id,
name: newDepartment?.name,
parentId: newDepartment?.parentId,
};
},
{ immediate: true },
);
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
parentId: z.number().nullable().optional(),
name: z
.string({
message: "部门名称不能为空",
})
.min(2, "部门名称至少2个字符"),
});
try {
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,135 @@
<template>
<nav class="fixed top-0 z-40 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700">
<div class="px-3 py-3 lg:px-5 lg:pl-3">
<div class="flex items-center justify-between">
<div class="flex items-center justify-start rtl:justify-end">
<button data-drawer-target="logo-sidebar" data-drawer-toggle="logo-sidebar" aria-controls="logo-sidebar"
type="button"
class="inline-flex items-center p-2 text-sm text-gray-500 rounded-lg sm:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600">
<span class="sr-only">Open sidebar</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path clip-rule="evenodd" fill-rule="evenodd"
d="M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z">
</path>
</svg>
</button>
<a href="https://www.mjga.cc" target="_blank" class="flex ms-2 md:me-24">
<span class="self-center text-xl font-semibold sm:text-2xl whitespace-nowrap dark:text-white">知路后台管理</span>
</a>
</div>
<div class="flex items-center space-x-3">
<a href="https://github.com/ccmjga/mjga-scaffold" target="_blank"
class="flex items-center border rounded-sm border-gray-300">
<span class=" bg-gray-200 rounded-r-none border-r border-r-gray-300">
<svg class="me-0.5 inline pl-1.5 pb-1 w-6 h-6 text-gray-800 dark:text-white bg-gray-200 "
aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"
viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2"
d="M11.083 5.104c.35-.8 1.485-.8 1.834 0l1.752 4.022a1 1 0 0 0 .84.597l4.463.342c.9.069 1.255 1.2.556 1.771l-3.33 2.723a1 1 0 0 0-.337 1.016l1.03 4.119c.214.858-.71 1.552-1.474 1.106l-3.913-2.281a1 1 0 0 0-1.008 0L7.583 20.8c-.764.446-1.688-.248-1.474-1.106l1.03-4.119A1 1 0 0 0 6.8 14.56l-3.33-2.723c-.698-.571-.342-1.702.557-1.771l4.462-.342a1 1 0 0 0 .84-.597l1.753-4.022Z" />
</svg>
<span class="text-sm pl-0.5 pr-2 font-medium">Star</span>
</span>
<span class="text-sm py-0.5 px-2 font-medium">0.45k</span>
</a>
<span class="flex space-x-2">
<svg class="w-6 h-6 text-gray-500" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z">
</path>
</svg>
<svg class="w-6 h-6 text-gray-500" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z">
</path>
</svg>
</span>
<div class="flex items-center ms-3">
<div>
<button type="button" id="dropdown-button"
class="flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
aria-expanded="false" data-dropdown-toggle="dropdown-user">
<span class="sr-only">Open user menu</span>
<img class="w-8 h-8 rounded-full" src="/public/trump.jpg" alt="user photo">
</button>
</div>
<div
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-sm shadow-sm dark:bg-gray-700 dark:divide-gray-600"
id="dropdown-user">
<div class="px-4 py-3" role="none">
<p class="text-sm font-medium text-gray-900 truncate dark:text-gray-300" role="none">
{{ user.username }}
</p>
</div>
<ul class="py-1" role="none">
<li>
<button @click="() => {
userDropDownMenu?.toggle()
router.push(RouteName.SETTINGS)
}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem">Settings</button>
</li>
<li>
<button @click="() => {
userDropDownMenu?.toggle()
router.push(RouteName.USERVIEW)
}"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem">Dashboard</button>
</li>
<li>
<button @click="handleLogoutClick"
class="flex items-center space-x-1 block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-log-out-icon w-4 h-4 lucide-log-out">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" x2="9" y1="12" y2="12" />
</svg><span>
Sign out
</span>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import useUserStore from "@/composables/store/useUserStore";
import { Dropdown, initFlowbite, type DropdownInterface } from "flowbite";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import useUserAuth from "../composables/auth/useUserAuth";
import { RouteName, RoutePath } from "../router/constants";
const userDropDownMenu = ref<DropdownInterface>();
onMounted(() => {
initFlowbite();
const $dropdownUser = document.getElementById("dropdown-user");
const $dropdownButton = document.getElementById("dropdown-button");
userDropDownMenu.value = new Dropdown(
$dropdownUser,
$dropdownButton,
{},
{ id: "dropdownMenu", override: true },
);
});
const { user } = useUserStore();
const { signOut } = useUserAuth();
const router = useRouter();
const handleLogoutClick = () => {
signOut();
router.push(RoutePath.LOGIN);
};
</script>

View File

@@ -0,0 +1,103 @@
<template>
<div :id tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 dark:bg-gray-900/80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow-sm dark:bg-gray-700">
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600 border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
权限管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">权限名称</label>
<input type="text" name="权限名称" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true">
</div>
<div class="col-span-2">
<label for="code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">权限编码</label>
<input type="text" id="code" v-model="formData.code"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click.prevent="handleSubmit"
class="text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center self-start mt-5">
保存
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import type { PermissionUpsertModel } from "@/types/permission";
import { ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
const { permission, onSubmit, closeModal } = defineProps<{
id: string;
permission?: components["schemas"]["PermissionRespDto"];
closeModal: () => void;
onSubmit: (data: PermissionUpsertModel) => Promise<void>;
}>();
const alertStore = useAlertStore();
const formData = ref();
watch(
() => permission,
(newPermission) => {
formData.value = {
id: newPermission?.id,
name: newPermission?.name,
code: newPermission?.code,
};
},
{ immediate: true },
);
const handleSubmit = async () => {
const permissionSchema = z.object({
id: z.number().optional(),
name: z
.string({
message: "权限名称不能为空",
})
.min(2, "权限名称至少2个字符"),
code: z
.string({
message: "权限代码不能为空",
})
.min(2, "权限代码至少2个字符"),
});
try {
const validatedData = permissionSchema.parse(formData.value);
await onSubmit(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
</script>

View File

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

View File

@@ -0,0 +1,102 @@
<template>
<!-- Main modal -->
<div :id tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 dark:bg-gray-900/80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm dark:bg-gray-700">
<!-- Modal header -->
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600 border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
岗位管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">岗位名称</label>
<input type="text" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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"
required />
</div>
</div>
<button type="submit" @click="handleSubmit"
class="text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 self-start mt-5">
保存
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
import type { PositionUpsertModel } from "../types/position";
import { tr } from "@faker-js/faker";
const alertStore = useAlertStore();
const { id, position, onSubmit } = defineProps<{
id: string;
position?: components["schemas"]["Position"];
closeModal: () => void;
onSubmit: (position: PositionUpsertModel) => Promise<void>;
}>();
const formData = ref();
watch(
() => position,
(newPosition) => {
formData.value = {
id: newPosition?.id,
name: newPosition?.name,
};
},
{ immediate: true },
);
const handleSubmit = async () => {
const schema = z.object({
id: z.number().optional(),
name: z
.string({
message: "岗位名称不能为空",
})
.min(2, "岗位名称至少2个字符"),
});
try {
const validatedData = schema.parse(formData.value);
await onSubmit(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,107 @@
<template>
<div id="role-upsert-modal" tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 dark:bg-gray-900/80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow-sm dark:bg-gray-700">
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600 border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
角色管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<div class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">角色名称</label>
<input type="text" name="角色名称" id="name" v-model="formData.name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true">
</div>
<div class="col-span-2">
<label for="code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">角色代码</label>
<input type="text" id="code" v-model="formData.code"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required />
</div>
</div>
<button type="submit" @click.prevent="handleSubmit"
class="text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center self-start mt-5">
保存
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import type { RoleUpsertModel } from "@/types/role";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
const alertStore = useAlertStore();
const { role, onSubmit } = defineProps<{
role?: components["schemas"]["RoleDto"];
closeModal: () => void;
onSubmit: (data: RoleUpsertModel) => Promise<void>;
}>();
const formData = ref();
watch(
() => role,
(newRole) => {
formData.value = {
id: newRole?.id,
name: newRole?.name,
code: newRole?.code,
};
},
{ immediate: true },
);
const handleSubmit = async () => {
const roleSchema = z.object({
id: z.number().optional(),
name: z
.string({
message: "角色名称不能为空",
})
.min(2, "角色名称至少2个字符"),
code: z
.string({
message: "角色代码不能为空",
})
.min(2, "角色代码至少2个字符"),
});
try {
const validatedData = roleSchema.parse(formData.value);
await onSubmit(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,88 @@
<template>
<div :id tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 dark:bg-gray-900/80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600 border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{{ '更新表达式' }}
</h3>
<button @click="closeModal" type="button"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
</button>
</div>
<form @submit.prevent="handleSubmit" class="p-4 md:p-5">
<div class="grid gap-4 mb-4">
<div>
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Cron 表达式</label>
<input type="text" v-model="formData.cronExpression" name="name" id="name"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
</div>
</div>
<button type="submit"
class="text-white inline-flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
提交
</button>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import type { components } from "@/api/types/schema";
import { ref, watch } from "vue";
import { z } from "zod";
const { job, closeModal, onSubmit } = defineProps<{
id: string;
job?: components["schemas"]["JobTriggerDto"];
closeModal: () => void;
onSubmit: (cronExpression: string) => Promise<void>;
}>();
const alertStore = useAlertStore();
const formData = ref({});
watch(
() => job,
(newJob) => {
if (newJob) {
formData.value = {
cronExpression: newJob.cronExpression,
};
}
},
{ immediate: true },
);
const handleSubmit = async () => {
const jobSchema = z.object({
cronExpression: z
.string({
message: "表达式不可为空",
})
.min(5, "表达式的长度非法"),
});
try {
const validatedData = jobSchema.parse(formData.value);
await onSubmit(validatedData.cronExpression);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
</script>

View File

@@ -0,0 +1,147 @@
<template>
<aside id="logo-sidebar"
class="fixed top-0 left-0 z-30 w-64 min-h-screen overflow-y-auto pt-20 transition-transform -translate-x-full bg-white border-r border-gray-200 sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
aria-label="Sidebar">
<div class="h-full px-3 pb-4 overflow-y-auto bg-white dark:bg-gray-800">
<ul class="space-y-2 font-medium">
<!-- <li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.OVERVIEW}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide shrink-0 text-gray-600 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white lucide-chart-pie-icon lucide-chart-pie">
<path
d="M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z" />
<path d="M21.21 15.89A10 10 0 1 1 8 2.83" />
</svg>
<span>
总览
</span>
</RouterLink>
</li> -->
<li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.USERVIEW}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-users-icon lucide-users shrink-0 text-gray-700 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
<span>
用户管理
</span>
</RouterLink>
</li>
<li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.ROLEVIEW}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-shield-user-icon lucide-shield-user shrink-0 text-gray-700 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white">
<path
d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
<path d="M6.376 18.91a6 6 0 0 1 11.249.003" />
<circle cx="12" cy="11" r="4" />
</svg>
<span>
角色管理
</span>
</RouterLink>
</li>
<li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.PERMISSIONVIEW}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-key-round-icon lucide-key-round shrink-0 text-gray-700 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white">
<path
d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
<span>
权限管理
</span>
</RouterLink>
</li>
<li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.DEPARTMENTVIEW}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg class=" text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 15v3c0 .5523.44772 1 1 1h4v-4m-5 0v-4m0 4h5m-5-4V6c0-.55228.44772-1 1-1h16c.5523 0 1 .44772 1 1v1.98935M3 11h5v4m9.4708 4.1718-.8696-1.4388-2.8164-.235-2.573-4.2573 1.4873-2.8362 1.4441 2.3893c.3865.6396 1.2183.8447 1.8579.4582.6396-.3866.8447-1.2184.4582-1.858l-1.444-2.38925h3.1353l2.6101 4.27715-1.0713 2.5847.8695 1.4388" />
</svg>
<span>
部门管理
</span>
</RouterLink>
</li>
<li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.POSITIONVIEW}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14.6144 7.19994c.3479.48981.5999 1.15357.5999 1.80006 0 1.6569-1.3432 3-3 3-1.6569 0-3.00004-1.3431-3.00004-3 0-.67539.22319-1.29865.59983-1.80006M6.21426 6v4m0-4 6.00004-3 6 3-6 2-2.40021-.80006M6.21426 6l3.59983 1.19994M6.21426 19.8013v-2.1525c0-1.6825 1.27251-3.3075 2.95093-3.6488l3.04911 2.9345 3-2.9441c1.7026.3193 3 1.9596 3 3.6584v2.1525c0 .6312-.5373 1.1429-1.2 1.1429H7.41426c-.66274 0-1.2-.5117-1.2-1.1429Z" />
</svg>
<span>
岗位管理
</span>
</RouterLink>
</li>
<li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.SETTINGS}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-user-cog-icon lucide-user-cog shrink-0 text-gray-700 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white">
<circle cx="18" cy="15" r="3" />
<circle cx="9" cy="7" r="4" />
<path d="M10 15H6a4 4 0 0 0-4 4v2" />
<path d="m21.7 16.4-.9-.3" />
<path d="m15.2 13.9-.9-.3" />
<path d="m16.6 18.7.3-.9" />
<path d="m19.1 12.2.3-.9" />
<path d="m19.6 18.7-.4-1" />
<path d="m16.8 12.3-.4-1" />
<path d="m14.3 16.6 1-.4" />
<path d="m20.7 13.8 1-.4" />
</svg>
<span>
个人中心
</span>
</RouterLink>
</li>
<li>
<RouterLink :to="`${RoutePath.DASHBOARD}/${RoutePath.SCHEDULERVIEW}`"
class="flex items-center p-2 gap-x-2 text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700 group">
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span>
定时任务
</span>
</RouterLink>
</li>
</ul>
</div>
</aside>
</template>
<script setup lang="ts">
import { RoutePath } from "@/router/constants";
import { initFlowbite } from "flowbite";
import { onMounted } from "vue";
import { RouterLink } from "vue-router";
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,78 @@
<template>
<nav class="flex items-center flex-column flex-wrap md:flex-row justify-between pt-4 px-5 pb-5"
aria-label="Table navigation">
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto">
显示
<span class="font-semibold text-gray-900 dark:text-white">
{{ displayRange.start }}-{{ displayRange.end }}
</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ total }}</span>
</span>
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
<li>
<a href="#" @click.prevent="handlePageChangeClick(currentPage - 1)" :class="[
'flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white',
{ 'opacity-50 cursor-not-allowed': isFirstPage }
]">上一页</a>
</li>
<li v-for="page in pageNumbers" :key="page">
<button @click.prevent="handlePageChangeClick(page)" :class="[
'flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700 dark:hover:text-white',
currentPage === page
? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white'
: 'text-gray-500 bg-white dark:text-gray-400'
]">{{ page }}</button>
</li>
<li>
<button @click.prevent="handlePageChangeClick(currentPage + 1)" :class="[
'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white',
{ 'opacity-50 cursor-not-allowed': isLastPage }
]">下一页</button>
</li>
</ul>
</nav>
</template>
<script setup lang="ts">
import { usePagination } from "@/composables/page";
import { watch } from "vue";
const { pageChange, total } = defineProps<{
pageChange: (page: number, size: number) => Promise<void>;
total: number;
}>();
const {
currentPage,
pageNumbers,
pageSize,
displayRange,
isFirstPage,
isLastPage,
totalPages,
updatePaginationState,
} = usePagination();
const handlePageChangeClick = async (page: number) => {
if (page < 1 || page > totalPages.value) return;
await pageChange(page, pageSize.value);
updatePaginationState({
currentPage: page,
pageSize: pageSize.value,
total,
});
};
watch(
() => total,
() => {
updatePaginationState({
currentPage: currentPage.value,
pageSize: pageSize.value,
total,
});
},
);
</script>

View File

@@ -0,0 +1,150 @@
<template>
<!-- Main modal -->
<div id="user-upsert-modal" tabindex="-1" aria-hidden="true"
class="bg-gray-900/50 dark:bg-gray-900/80 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm dark:bg-gray-700">
<!-- Modal header -->
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600 border-gray-200">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
用户管理
</h3>
<button type="button" @click="closeModal"
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<form class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-2">
<div class="col-span-2">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">用户名</label>
<input type="text" name="用户名" id="name" v-model="formData.username"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 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"
required="true">
</div>
<div class="col-span-2">
<label for="password"
class="block mb-2 text-sm font-medium autocompletetext-gray-900 dark:text-white">密码</label>
<input type="password" id="password" autocomplete="new-password" v-model="formData.password"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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-2">
<label for="confirm_password"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">确认密码</label>
<input type="password" id="confirm_password" autocomplete="new-password"
v-model="formData.confirmPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 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"
required placeholder="非必填" />
</div>
<div class="col-span-2 sm:col-span-1">
<label for="category" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">状态</label>
<select id="category" v-model="formData.enable"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 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>
<button type="submit" @click.prevent="handleSubmit"
class="text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 self-start mt-5">
保存
</button>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useAlertStore from "@/composables/store/useAlertStore";
import type { UserUpsertSubmitModel } from "@/types/user";
import { initFlowbite } from "flowbite";
import { onMounted, ref, watch } from "vue";
import { z } from "zod";
import type { components } from "../api/types/schema";
const alertStore = useAlertStore();
const { user, onSubmit } = defineProps<{
user?: components["schemas"]["UserRolePermissionDto"];
closeModal: () => void;
onSubmit: (data: UserUpsertSubmitModel) => Promise<void>;
}>();
const formData = ref();
watch(
() => user,
(newUser) => {
formData.value = {
id: newUser?.id,
username: newUser?.username,
password: undefined,
enable: newUser?.enable,
confirmPassword: undefined,
};
},
{
immediate: true,
},
);
const handleSubmit = async () => {
const userSchema = z
.object({
id: z.number().optional(),
username: z
.string({
message: "用户名不能为空",
})
.min(4, "用户名至少4个字符"),
enable: z.boolean(),
password: z
.string({
message: "密码不能为空",
})
.min(5, "密码至少5个字符")
.optional(),
confirmPassword: z
.string({
message: "密码不能为空",
})
.min(5, "密码至少5个字符")
.optional(),
})
.refine(
(data) => {
if (!data.password) return true;
return data.password === data.confirmPassword;
},
{
message: "密码输入不一致。",
},
);
try {
const validatedData = userSchema.parse(formData.value);
await onSubmit(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
alertStore.showAlert({
level: "error",
content: error.errors[0].message,
});
}
throw error;
}
};
onMounted(() => {
initFlowbite();
});
</script>

View File

@@ -0,0 +1,71 @@
import client from "@/api/client";
import { ref } from "vue";
import useAuthStore from "../store/useAuthStore";
import useUserStore from "../store/useUserStore";
const useUserAuth = () => {
const isAuthenticated = ref(false);
const authStore = useAuthStore();
const userStore = useUserStore();
const queryCurrentUser = async () => {
const { data } = await client.GET("/iam/me");
return data;
};
const refreshCurrentUser = async () => {
const currentUser = await queryCurrentUser();
if (currentUser) {
userStore.set(currentUser);
isAuthenticated.value = true;
}
};
const upsertCurrentUser = async ({
username,
password,
enable,
}: {
username: string;
password?: string | null;
enable: boolean;
}) => {
await client.POST("/iam/me", {
body: {
username,
password: password ?? undefined,
enable,
},
});
await refreshCurrentUser();
};
const signIn = async (username: string, password: string) => {
const signInResponse = await client.POST("/auth/sign-in", {
body: {
username,
password,
},
});
authStore.set(
signInResponse.response.headers.get("authorization") ?? undefined,
);
await refreshCurrentUser();
};
const signOut = () => {
authStore.remove();
isAuthenticated.value = false;
userStore.remove();
};
return {
isAuthenticated,
signIn,
signOut,
queryCurrentUser,
upsertCurrentUser,
};
};
export default useUserAuth;

View File

@@ -0,0 +1,38 @@
import client from "@/api/client";
export function useDepartmentBind() {
const bindDepartment = async (userId: number, departmentIds: number[]) => {
try {
await client.POST("/iam/department/bind", {
body: {
userId,
departmentIds,
},
});
return true;
} catch (error) {
console.error("Error binding departments:", error);
return false;
}
};
const unbindDepartment = async (userId: number, departmentIds: number[]) => {
try {
await client.POST("/iam/department/unbind", {
body: {
userId,
departmentIds,
},
});
return true;
} catch (error) {
console.error("Error unbinding departments:", error);
return false;
}
};
return {
bindDepartment,
unbindDepartment,
};
}

View File

@@ -0,0 +1,18 @@
import client from "@/api/client";
export const useDepartmentDelete = () => {
const deleteDepartment = async (departmentId: number) => {
await client.DELETE("/department", {
params: {
query: {
id: departmentId,
},
},
});
};
return {
deleteDepartment,
};
};
export default useDepartmentDelete;

View File

@@ -0,0 +1,45 @@
import client from "@/api/client";
import { ref } from "vue";
import type { components } from "../../api/types/schema";
export const useDepartmentQuery = () => {
const total = ref<number>(0);
const departments = ref<components["schemas"]["DepartmentRespDto"][]>([]);
const allDepartments = ref<components["schemas"]["Department"][]>([]);
const fetchAllDepartments = async () => {
const { data } = await client.GET("/department/query");
allDepartments.value = data ?? [];
};
const fetchDepartmentWith = async (
param: {
name?: string;
enable?: boolean;
userId?: number;
bindState?: "ALL" | "BIND" | "UNBIND";
},
page = 1,
size = 10,
) => {
const { data } = await client.GET("/department/page-query", {
params: {
query: {
pageRequestDto: {
page,
size,
},
departmentQueryDto: param,
},
},
});
total.value = !data || !data.total ? 0 : data.total;
departments.value = data?.data ?? [];
};
return {
total,
departments,
allDepartments,
fetchDepartmentWith,
fetchAllDepartments,
};
};

View File

@@ -0,0 +1,18 @@
import client from "../../api/client";
import type { DepartmentUpsertModel } from "../../types/department";
export const useDepartmentUpsert = () => {
const upsertDepartment = async (department: DepartmentUpsertModel) => {
await client.POST("/department", {
body: {
id: department.id,
name: department.name,
parentId: department.parentId ?? undefined,
},
});
};
return {
upsertDepartment,
};
};

View File

@@ -0,0 +1,38 @@
import client from "@/api/client";
export const useJobControl = () => {
const resumeTrigger = async (trigger: {
triggerName: string;
triggerGroup: string;
jobQueryParam?: {
name?: string;
};
}) => {
await client.POST("/scheduler/trigger/resume", {
body: {
name: trigger.triggerName,
group: trigger.triggerGroup,
},
});
};
const pauseTrigger = async (trigger: {
triggerName: string;
triggerGroup: string;
jobQueryParam?: {
name?: string;
};
}) => {
await client.POST("/scheduler/trigger/pause", {
body: {
name: trigger.triggerName,
group: trigger.triggerGroup,
},
});
};
return {
pauseTrigger,
resumeTrigger,
};
};

View File

@@ -0,0 +1,36 @@
import client from "@/api/client";
import { ref } from "vue";
import type { components } from "../../api/types/schema";
export const useJobsPaginationQuery = () => {
const total = ref<number>(0);
const jobs = ref<components["schemas"]["JobTriggerDto"][]>();
const fetchJobsWith = async (
queryParam?: {
name?: string;
},
page = 1,
size = 10,
) => {
const { data } = await client.GET("/scheduler/page-query", {
params: {
query: {
pageRequestDto: {
page: page,
size: size,
},
queryDto: {
name: queryParam?.name,
},
},
},
});
total.value = !data || !data.total ? 0 : data.total;
jobs.value = data?.data ?? [];
};
return {
total,
jobs,
fetchJobsWith,
};
};

View File

@@ -0,0 +1,24 @@
import client from "@/api/client";
export const useJobUpdate = () => {
const updateCron = async (trigger: {
triggerName: string;
triggerGroup: string;
cron: string;
}) => {
await client.PUT("/scheduler/job/update", {
params: {
query: {
cron: trigger.cron,
},
},
body: {
name: trigger.triggerName,
group: trigger.triggerGroup,
},
});
};
return {
updateCron,
};
};

View File

@@ -0,0 +1,67 @@
import { computed, ref } from "vue";
export interface PaginationState {
currentPage: number;
pageSize: number;
total: number;
}
export interface UsePaginationOptions {
initialPage?: number;
initialPageSize?: number;
initialTotal?: number;
}
export function usePagination(options: UsePaginationOptions = {}) {
const { initialPage = 1, initialPageSize = 10, initialTotal = 0 } = options;
const currentPage = ref(initialPage);
const pageSize = ref(initialPageSize);
const total = ref(initialTotal);
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
const pageNumbers = computed(() => {
const pages = [];
for (let i = 1; i <= totalPages.value; i++) {
pages.push(i);
}
return pages;
});
const displayRange = computed(() => {
const start =
total.value === 0 ? 0 : (currentPage.value - 1) * pageSize.value + 1;
const end =
total.value === 0
? 0
: Math.min(currentPage.value * pageSize.value, total.value);
return { start, end };
});
const isFirstPage = computed(
() => total.value === 0 || currentPage.value === 1,
);
const isLastPage = computed(
() => total.value === 0 || currentPage.value === totalPages.value,
);
const updatePaginationState = (state: Partial<PaginationState>) => {
if (state.currentPage !== undefined) currentPage.value = state.currentPage;
if (state.pageSize !== undefined) pageSize.value = state.pageSize;
if (state.total !== undefined) total.value = state.total;
};
return {
currentPage,
pageSize,
total,
totalPages,
pageNumbers,
displayRange,
isFirstPage,
isLastPage,
updatePaginationState,
};
}

View File

@@ -0,0 +1,36 @@
import client from "@/api/client";
export const usePermissionBind = () => {
const bindPermission = async ({
roleId,
permissionIds,
}: {
roleId: number;
permissionIds: number[];
}) => {
await client.POST("/iam/permission/bind", {
body: {
roleId,
permissionIds,
},
});
};
const unbindPermission = async ({
roleId,
permissionIds,
}: {
roleId: number;
permissionIds: number[];
}) => {
await client.POST("/iam/permission/unbind", {
body: {
roleId,
permissionIds,
},
});
};
return {
bindPermission,
unbindPermission,
};
};

View File

@@ -0,0 +1,19 @@
import client from "@/api/client";
const usePermissionDelete = () => {
const deletePermission = async (id: number) => {
await client.DELETE("/iam/permission", {
params: {
query: {
permissionId: id,
},
},
});
};
return {
deletePermission,
};
};
export default usePermissionDelete;

View File

@@ -0,0 +1,48 @@
import client from "@/api/client";
import { ref } from "vue";
import type { components } from "../../api/types/schema";
const usePermissionsQuery = () => {
const total = ref<number>(0);
const permissions = ref<components["schemas"]["PermissionRespDto"][]>([]);
const fetchPermissionsWith = async (
query: {
name?: string;
roleId?: number;
bindState?: "BIND" | "ALL" | "UNBIND";
},
page = 1,
size = 10,
) => {
const { data } = await client.GET("/iam/permissions", {
params: {
query: {
pageRequestDto: {
page,
size,
},
permissionQueryDto: {
permissionName: query.name,
roleId: query.roleId,
bindState: query.bindState,
},
},
},
});
if (!data) {
throw new Error("获取权限数据失败");
}
total.value = data.total ?? 0;
permissions.value = data.data ?? [];
};
return {
total,
permissions,
fetchPermissionsWith,
};
};
export default usePermissionsQuery;

View File

@@ -0,0 +1,19 @@
import client from "../../api/client";
import type { PermissionUpsertModel } from "../../types/permission";
const usePermissionUpsert = () => {
const upsertPermission = async (permission: PermissionUpsertModel) => {
await client.POST("/iam/permission", {
body: {
id: permission.id,
name: permission.name,
code: permission.code,
},
});
};
return {
upsertPermission,
};
};
export default usePermissionUpsert;

View File

@@ -0,0 +1,38 @@
import client from "@/api/client";
export function usePositionBind() {
const bindPosition = async (userId: number, positionIds: number[]) => {
try {
await client.POST("/iam/position/bind", {
body: {
userId,
positionIds,
},
});
return true;
} catch (error) {
console.error("Error binding positions:", error);
return false;
}
};
const unbindPosition = async (userId: number, positionIds: number[]) => {
try {
await client.POST("/iam/position/unbind", {
body: {
userId,
positionIds,
},
});
return true;
} catch (error) {
console.error("Error unbinding positions:", error);
return false;
}
};
return {
bindPosition,
unbindPosition,
};
}

View File

@@ -0,0 +1,18 @@
import client from "@/api/client";
export const usePositionDelete = () => {
const deletePosition = async (positionId: number) => {
await client.DELETE("/position", {
params: {
query: {
id: positionId,
},
},
});
};
return {
deletePosition,
};
};
export default usePositionDelete;

View File

@@ -0,0 +1,45 @@
import client from "@/api/client";
import { ref } from "vue";
import type { components } from "../../api/types/schema";
export const usePositionQuery = () => {
const total = ref<number>(0);
const positions = ref<components["schemas"]["PositionRespDto"][]>([]);
const allPositions = ref<components["schemas"]["Position"][]>([]);
const fetchAllPositions = async () => {
const { data } = await client.GET("/position/query");
allPositions.value = data ?? [];
};
const fetchPositionWith = async (
param: {
name?: string;
enable?: boolean;
userId?: number;
bindState?: "ALL" | "BIND" | "UNBIND";
},
page = 1,
size = 10,
) => {
const { data } = await client.GET("/position/page-query", {
params: {
query: {
pageRequestDto: {
page,
size,
},
positionQueryDto: param,
},
},
});
total.value = !data || !data.total ? 0 : data.total;
positions.value = data?.data ?? [];
};
return {
total,
positions,
allPositions,
fetchPositionWith,
fetchAllPositions,
};
};

View File

@@ -0,0 +1,16 @@
import client from "../../api/client";
import type { components } from "../../api/types/schema";
export const usePositionUpsert = () => {
const upsertPosition = async (
position: components["schemas"]["Position"],
) => {
await client.POST("/position", {
body: position,
});
};
return {
upsertPosition,
};
};

View File

@@ -0,0 +1,28 @@
import client from "@/api/client";
export const useRoleBind = () => {
const bindRole = async ({
userId,
roleIds,
}: { userId: number; roleIds: number[] }) => {
await client.POST("/iam/role/bind", {
body: {
userId,
roleIds,
},
});
};
const unbindRole = async (userId: number, roleIds: number[]) => {
await client.POST("/iam/role/unbind", {
body: {
userId,
roleIds,
},
});
};
return {
bindRole,
unbindRole,
};
};

View File

@@ -0,0 +1,19 @@
import client from "../../api/client";
const useRoleDelete = () => {
const deleteRole = async (roleId: number) => {
await client.DELETE("/iam/role", {
params: {
query: {
roleId,
},
},
});
};
return {
deleteRole,
};
};
export default useRoleDelete;

View File

@@ -0,0 +1,17 @@
import client from "../../api/client";
export const useRoleUpsert = () => {
const upsertRole = async (role: {
id?: number;
name: string;
code: string;
}) => {
await client.POST("/iam/role", {
body: role,
});
};
return {
upsertRole,
};
};

View File

@@ -0,0 +1,61 @@
import client from "@/api/client";
import { ref } from "vue";
import type { components } from "../../api/types/schema";
export const useRolesQuery = () => {
const total = ref<number>(0);
const roles = ref<components["schemas"]["RoleDto"][]>();
const roleWithDetail = ref<components["schemas"]["RoleDto"]>();
const getRoleWithDetail = async (roleId: number) => {
const { data } = await client.GET("/iam/role", {
params: {
query: {
roleId,
},
},
});
roleWithDetail.value = data;
};
const fetchRolesWith = async (
param: {
name?: string;
userId?: number;
bindState?: "BIND" | "ALL" | "UNBIND";
},
page = 1,
size = 10,
) => {
const { data } = await client.GET("/iam/roles", {
params: {
query: {
pageRequestDto: {
page,
size,
},
roleQueryDto: {
roleName: param.name,
userId: param.userId,
bindState: param.bindState,
},
},
},
});
if (!data) {
throw new Error("获取角色数据失败");
}
total.value = data.total ?? 0;
roles.value = data.data ?? [];
};
return {
total,
roles,
roleWithDetail,
getRoleWithDetail,
fetchRolesWith,
};
};

View File

@@ -0,0 +1,57 @@
import { StorageSerializers, useStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed } from "vue";
import type { AlertProps } from "../../types/alert";
const useAlertStore = defineStore("alertStore", () => {
const alertStorage = useStorage<AlertProps>(
"alert-storage",
{
content: undefined,
level: undefined,
isShow: undefined,
timer: undefined,
},
localStorage,
{
serializer: StorageSerializers.object,
},
);
const showAlert = ({
content: newContent,
level: newLevel,
}: { content: string; level: "info" | "success" | "warning" | "error" }) => {
clearTimeout(alertStorage.value.timer);
alertStorage.value = {
content: newContent,
level: newLevel,
isShow: true,
timer: setTimeout(() => {
alertStorage.value.isShow = false;
}, 3000),
};
};
const levelClassName = computed(() => {
if (!alertStorage.value.level) {
return;
}
return {
info: "text-blue-800 bg-blue-50 dark:bg-gray-800 dark:text-blue-400 ",
success:
"text-green-800 bg-green-50 dark:bg-gray-800 dark:text-green-400",
warning:
"text-yellow-800 bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300",
error: "text-red-800 bg-red-50 dark:bg-gray-800 dark:text-red-400",
}[alertStorage.value.level];
});
return {
alertStorage,
showAlert,
levelClassName,
};
});
export default useAlertStore;

Some files were not shown because too many files have changed in this diff Show More