diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 340274e..70b356c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -36,7 +36,6 @@ module.exports = { // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead) 'standard' - ], plugins: [ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 518f8c8..2feb79f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,6 @@ "recommendations": [ "dbaeumer.vscode-eslint", "editorconfig.editorconfig", - "vue.volar", "wayou.vscode-todo-highlight" ], "unwantedRecommendations": [ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 223cd65..603dfa2 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/README.md b/README.md index 10f2dcf..ebceb3a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Quasar App (quasar-project) +# Leafage MS -A Quasar Project +Integrated with [mswjs](https://mswjs.io) for mock request. ## Install the dependencies ```bash @@ -11,6 +11,8 @@ npm install ### Start the app in development mode (hot-code reloading, error reporting, etc.) ```bash +yarn dev +# or quasar dev ``` diff --git a/package.json b/package.json index 4bdf7de..17b24c3 100644 --- a/package.json +++ b/package.json @@ -12,35 +12,41 @@ "build": "quasar build" }, "dependencies": { - "@quasar/extras": "^1.16.9", - "axios": "^1.6.3", + "@quasar/extras": "^1.16.11", + "axios": "^1.6.8", "pinia": "^2.1.7", - "quasar": "^2.14.4", - "vue": "^3.4.19", - "vue-i18n": "^9.9.1", - "vue-router": "^4.2.5" + "quasar": "^2.15.1", + "vue": "^3.4.21", + "vue-i18n": "^9.11.0", + "vue-router": "^4.3.0" }, "devDependencies": { "@intlify/vite-plugin-vue-i18n": "^7.0.0", - "@quasar/app-vite": "^1.7.3", - "@types/node": "^20.10.6", - "@typescript-eslint/eslint-plugin": "^6.16.0", - "@typescript-eslint/parser": "^6.16.0", - "autoprefixer": "^10.4.16", - "eslint": "^8.56.0", + "@quasar/app-vite": "^1.8.0", + "@types/node": "^20.12.5", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0", + "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-n": "^16.6.0", + "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-vue": "^9.19.2", + "eslint-plugin-vue": "^9.24.0", "lottie-web": "^5.12.2", - "postcss": "^8.4.34", - "typescript": "^5.3.3" + "msw": "^2.2.13", + "postcss": "^8.4.38", + "typescript": "^5.4.4" }, "engines": { "node": "^20 || ^18", "npm": ">= 6.13.4", "yarn": ">= 1.21.1" }, - "packageManager": "yarn@4.0.2" + "packageManager": "yarn@4.1.1", + "msw": { + "workerDirectory": [ + "public" + ] + } } diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..2d139da --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.2.13' +const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/quasar.config.js b/quasar.config.js index b66aba7..743e175 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -30,7 +30,8 @@ module.exports = configure(function (ctx) { // https://v2.quasar.dev/quasar-cli-vite/boot-files boot: [ 'i18n', - 'axios' + 'axios', + 'msw-server' ], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css @@ -56,7 +57,7 @@ module.exports = configure(function (ctx) { build: { target: { browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], - node: 'node16' + node: 'node18' }, vueRouterMode: 'history', // available values: 'hash', 'history' @@ -75,7 +76,9 @@ module.exports = configure(function (ctx) { : 'https://console.leafage.top/api' }, - // rawDefine: {} + rawDefine: { + 'globalThis.process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) + }, // ignorePublicFolder: true, // minify: false, // polyfillModulePreload: true, @@ -102,19 +105,24 @@ module.exports = configure(function (ctx) { // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer devServer: { // https: true - open: true, // opens browser window automatically - proxy: { - '^/api': { - target: 'http://127.0.0.1:8763', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, '') - } - } + open: true // opens browser window automatically + // proxy: { + // '^/api': { + // target: 'http://127.0.0.1:8763', + // changeOrigin: true, + // rewrite: (path) => path.replace(/^\/api/, '') + // } + // } }, // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework framework: { - config: {}, + config: { + dark: 'auto', + notify: { + position: 'top' + } + }, iconSet: 'material-symbols-rounded', // Quasar icon set lang: 'en-US', // Quasar language pack diff --git a/src/api/models.type.ts b/src/api/models.type.ts index 6b57f8b..c4aa9b7 100644 --- a/src/api/models.type.ts +++ b/src/api/models.type.ts @@ -1,55 +1,43 @@ interface AudtiMetadata { - id?: number; - lastModifiedDate?: Date; -} - -export interface Post extends AudtiMetadata { - cover: string; - tags: Array; - content: string; -} - -export interface Category extends AudtiMetadata { - name: string; - enabled?: boolean; - description?: string; + id?: number + lastModifiedDate?: Date } export interface User extends AudtiMetadata { - username: string; - firstname: string; - lastname: string; - avatar?: string; - accountNonLocked?: boolean; - accountExpiresAt?: Date; - credentialsExpiresAt?: Date; - enabled?: boolean; - description?: string; + username: string + firstname: string + lastname: string + avatar?: string + accountNonLocked?: boolean + accountExpiresAt?: Date + credentialsExpiresAt?: Date + enabled?: boolean + description?: string } export interface Group extends AudtiMetadata { - name: string; - superior_id?: number; - enabled?: boolean; - description?: string; + groupName: string + enabled?: boolean } export interface Role extends AudtiMetadata { - name: string; - enabled?: boolean; - description?: string; + name: string + enabled?: boolean + description?: string } export interface Dictionary extends AudtiMetadata { - name: string; - superior_id?: number; - enabled?: boolean; - description?: string; + name: string + superiorId?: number + enabled?: boolean + description?: string } export interface Region extends AudtiMetadata { - name: string; - superior_id?: number; - enabled?: boolean; - description?: string; + name: string + superiorId?: number + areaCode: number + postalCode: number + enabled?: boolean + description?: string } diff --git a/src/boot/axios.ts b/src/boot/axios.ts index 27b52e1..fdda089 100644 --- a/src/boot/axios.ts +++ b/src/boot/axios.ts @@ -43,7 +43,7 @@ export default boot(({ app }) => { }, (error: AxiosError) => { // 如果请求失败,取消后续请求 - abortController.abort() + // abortController.abort() return Promise.reject(error) } ) diff --git a/src/boot/msw-server.ts b/src/boot/msw-server.ts new file mode 100644 index 0000000..873112d --- /dev/null +++ b/src/boot/msw-server.ts @@ -0,0 +1,11 @@ +import { boot } from 'quasar/wrappers' +import { setupWorker } from 'msw/browser' +import { handlers } from 'src/mocks/handlers' // 您的请求处理程序 + +export default boot(() => { + // dev + if (process.env.DEV) { + const worker = setupWorker(...handlers) + worker.start() + } +}) diff --git a/src/css/app.scss b/src/css/app.scss index 5c0ab3f..329d8c0 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -1 +1,21 @@ -// app global css in SCSS form \ No newline at end of file +// app global css in SCSS form + +.bg-primary-gradient { + background: radial-gradient(circle at top right, #ffffff, $primary); +} + +.bg-negative-gradient { + background: radial-gradient(circle at center bottom, #e05969, $negative); +} + +.bg-positive-gradient { + background: radial-gradient(circle at center right, #26A69A, $positive); +} + +.bg-warning-gradient { + background: radial-gradient(circle at bottom left, #cf9900, $warning); +} + +.rounded-full { + border-radius: 50%; +} \ No newline at end of file diff --git a/src/i18n/en-US/index.ts b/src/i18n/en-US/index.ts index 2df7f14..a0db661 100644 --- a/src/i18n/en-US/index.ts +++ b/src/i18n/en-US/index.ts @@ -34,7 +34,8 @@ export default { username: 'Username', password: 'Password', - nickname: 'Nickname', + firstname: 'First Name', + lastname: 'Last Name', accountLocked: 'Account Locked', accountExpiresAt: 'User Expires At', credentialsExpiresAt: 'Credentials Expires At', diff --git a/src/i18n/zh-CN/index.ts b/src/i18n/zh-CN/index.ts index 7bdadae..b26493d 100644 --- a/src/i18n/zh-CN/index.ts +++ b/src/i18n/zh-CN/index.ts @@ -15,9 +15,9 @@ export default { profile: '个人中心', settings: '设置', help: '帮助', - signin: '登录', + signin: '登 录', signinTo: '登录您的帐户', - signup: '注册', + signup: '注 册', signout: '退出登录', accesslogs: '操作日志', @@ -34,7 +34,8 @@ export default { username: '账号', password: '密码', - nickname: '昵称', + firstname: '姓', + lastname: '名字', accountLocked: '账号锁状态', accountExpiresAt: '账号失效时间', credentialsExpiresAt: '密码失效时间', @@ -96,6 +97,7 @@ export default { messages: '消息通知', status: '状态', - welcome: '欢迎', + welcome: '嗨!欢迎回来', + subtitle: '请填写您的账号和密码,让我们继续探索', rememberMe: '记住我 ?' } diff --git a/src/i18n/zh-TW/index.ts b/src/i18n/zh-TW/index.ts index 938bfc8..01acffa 100644 --- a/src/i18n/zh-TW/index.ts +++ b/src/i18n/zh-TW/index.ts @@ -15,9 +15,9 @@ export default { profile: '個人中心', settings: '設置', help: '幫助', - signin: '登錄', + signin: '登 錄', signinTo: '登錄您的帳戶', - signup: '註冊', + signup: '註 冊', signout: '退出登錄', accesslogs: '操作日誌', @@ -34,7 +34,8 @@ export default { username: '賬號', password: '密碼', - nickname: '昵稱', + firstname: '姓', + lastname: '名字', accountLocked: '賬號鎖狀態', accountExpiresAt: '賬號失效時間', credentialsExpiresAt: '密碼失效時間', @@ -96,6 +97,7 @@ export default { messages: '消息通知', status: '狀態', - welcome: '歡迎', + welcome: '嗨!歡迎回來', + subtitle: '請填寫您的帳號和密碼,讓我們繼續探索', rememberMe: '記住我 ?' } diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index f09aeba..fce964a 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -1,58 +1,59 @@ @@ -80,11 +77,10 @@ import SideBarLeft from './SideBarLeft.vue' const userStore = useUserStore() const { locale } = useI18n({ useScope: 'global' }) -const darkTheme = ref(false) + const leftDrawerOpen = ref(false) function toggleLeftDrawer() { leftDrawerOpen.value = !leftDrawerOpen.value } -src/stores/user-store diff --git a/src/layouts/SideBarLeft.vue b/src/layouts/SideBarLeft.vue index ddb6194..ee44819 100644 --- a/src/layouts/SideBarLeft.vue +++ b/src/layouts/SideBarLeft.vue @@ -45,7 +45,7 @@ const essentialLinks: EssentialLinkProps[] = [ }, { title: 'dictionaries', - icon: 'sym_r_library_books', + icon: 'sym_r_menu_book', link: '/system/dictionaries' }, { diff --git a/src/mocks/common.ts b/src/mocks/common.ts new file mode 100644 index 0000000..1d4580a --- /dev/null +++ b/src/mocks/common.ts @@ -0,0 +1,21 @@ +import { http, HttpResponse } from 'msw' + +export const commonHandlers = [ + http.post('/api/login', async ({ request }) => { + const info = await request.formData() + + const username = info.get('username') + // Read the intercepted request body. + return HttpResponse.json({ username }, { + headers: { + 'Set-Cookie': 'logged_in=yes' + } + }) + }), + http.post('/api/logout', ({ cookies }) => { + if (!cookies.logged_in) { + return new HttpResponse(null, { status: 401 }) + } + return new HttpResponse() + }) +] diff --git a/src/mocks/dictionaries.ts b/src/mocks/dictionaries.ts new file mode 100644 index 0000000..ca9afb3 --- /dev/null +++ b/src/mocks/dictionaries.ts @@ -0,0 +1,80 @@ +import { http, HttpResponse } from 'msw' +import type { Dictionary } from 'src/api/models.type' + +const datas: Dictionary[] = [] +const subDatas: Dictionary[] = [] + +for (let i = 0; i < 20; i++) { + const data: Dictionary = { + id: i, + name: 'dictionary_' + i, + description: 'this is description for this row', + enabled: i % 3 > 0, + lastModifiedDate: new Date() + } + for (let j = 0; j < i; j++) { + const data: Dictionary = { + id: j, + name: 'dictionary_' + i + '_' + j, + superiorId: i, + enabled: j % 2 > 0, + description: 'description', + lastModifiedDate: new Date() + } + subDatas.push(data) + } + datas.push(data) +} + +export const dictionariesHandlers = [ + http.get('/api/dictionaries/:id/subset', ({ params }) => { + const superiorId = params.id + return HttpResponse.json(subDatas.filter(item => item.superiorId === Number(superiorId))) + }), + http.get('/api/dictionaries', ({ request }) => { + const url = new URL(request.url) + + const page = url.searchParams.get('page') + const size = url.searchParams.get('size') + + // Construct a JSON response with the list of all Dictionarys + // as the response body. + const data = { + content: Array.from(datas.slice(Number(page) * Number(size), (Number(page) + 1) * Number(size))), + totalElements: datas.length + } + + return HttpResponse.json(data) + }), + http.post('/api/dictionaries', async ({ request }) => { + // Read the intercepted request body as JSON. + const newData = await request.json() as Dictionary + + // Push the new Dictionary to the map of all Dictionarys. + datas.push(newData) + + // Don't forget to declare a semantic "201 Created" + // response and send back the newly created Dictionary! + return HttpResponse.json(newData, { status: 201 }) + }), + http.delete('/api/dictionaries/:id', ({ params }) => { + // All request path params are provided in the "params" + // argument of the response resolver. + const { id } = params + + // Let's attempt to grab the Dictionary by its ID. + const deletedData = datas.filter(item => item.id === Number(id)) + + // Respond with a "404 Not Found" response if the given + // Dictionary ID does not exist. + if (!deletedData) { + return new HttpResponse(null, { status: 404 }) + } + + // Delete the Dictionary from the "allDictionarys" map. + datas.pop() + + // Respond with a "200 OK" response and the deleted Dictionary. + return HttpResponse.json(deletedData) + }) +] diff --git a/src/mocks/groups.ts b/src/mocks/groups.ts new file mode 100644 index 0000000..574189e --- /dev/null +++ b/src/mocks/groups.ts @@ -0,0 +1,61 @@ +import { http, HttpResponse } from 'msw' +import type { Group } from 'src/api/models.type' + +const datas: Group[] = [] + +for (let i = 0; i < 20; i++) { + const data: Group = { + id: i, + groupName: 'group_' + i, + enabled: i % 3 > 0, + lastModifiedDate: new Date() + } + datas.push(data) +} + +export const groupsHandlers = [ + http.get('/api/groups', ({ request }) => { + const url = new URL(request.url) + const page = url.searchParams.get('page') + const size = url.searchParams.get('size') + // Construct a JSON response with the list of all Dictionarys + // as the response body. + const data = { + content: Array.from(datas.slice(Number(page) * Number(size), (Number(page) + 1) * Number(size))), + totalElements: datas.length + } + + return HttpResponse.json(data) + }), + http.post('/api/groups', async ({ request }) => { + // Read the intercepted request body as JSON. + const newData = await request.json() as Group + + // Push the new Dictionary to the map of all Dictionarys. + datas.push(newData) + + // Don't forget to declare a semantic "201 Created" + // response and send back the newly created Dictionary! + return HttpResponse.json(newData, { status: 201 }) + }), + http.delete('/api/groups/:id', ({ params }) => { + // All request path params are provided in the "params" + // argument of the response resolver. + const { id } = params + + // Let's attempt to grab the Dictionary by its ID. + const deletedData = datas.filter(item => item.id === Number(id)) + + // Respond with a "404 Not Found" response if the given + // Dictionary ID does not exist. + if (!deletedData) { + return new HttpResponse(null, { status: 404 }) + } + + // Delete the Dictionary from the "allDictionarys" map. + datas.pop() + + // Respond with a "200 OK" response and the deleted Dictionary. + return HttpResponse.json(deletedData) + }) +] diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts new file mode 100644 index 0000000..9250de0 --- /dev/null +++ b/src/mocks/handlers.ts @@ -0,0 +1,15 @@ +import { dictionariesHandlers } from './dictionaries' +import { commonHandlers } from './common' +import { groupsHandlers } from './groups' +import { regionsHandlers } from './regions' +import { rolesHandlers } from './roles' +import { usersHandlers } from './users' + +export const handlers = [ + ...commonHandlers, + ...dictionariesHandlers, + ...groupsHandlers, + ...regionsHandlers, + ...rolesHandlers, + ...usersHandlers +] diff --git a/src/mocks/regions.ts b/src/mocks/regions.ts new file mode 100644 index 0000000..a4c2709 --- /dev/null +++ b/src/mocks/regions.ts @@ -0,0 +1,64 @@ +import { http, HttpResponse } from 'msw' +import type { Region } from 'src/api/models.type' + +const datas: Region[] = [] + +for (let i = 0; i < 20; i++) { + const data: Region = { + id: i, + name: 'region_' + i, + areaCode: i, + postalCode: i, + enabled: i % 3 > 0, + description: 'description', + lastModifiedDate: new Date() + } + datas.push(data) +} + +export const regionsHandlers = [ + http.get('/api/regions', ({ request }) => { + const url = new URL(request.url) + const page = url.searchParams.get('page') + const size = url.searchParams.get('size') + // Construct a JSON response with the list of all Dictionarys + // as the response body. + const data = { + content: Array.from(datas.slice(Number(page) * Number(size), (Number(page) + 1) * Number(size))), + totalElements: datas.length + } + + return HttpResponse.json(data) + }), + http.post('/api/regions', async ({ request }) => { + // Read the intercepted request body as JSON. + const newData = await request.json() as Region + + // Push the new Dictionary to the map of all Dictionarys. + datas.push(newData) + + // Don't forget to declare a semantic "201 Created" + // response and send back the newly created Dictionary! + return HttpResponse.json(newData, { status: 201 }) + }), + http.delete('/api/regions/:id', ({ params }) => { + // All request path params are provided in the "params" + // argument of the response resolver. + const { id } = params + + // Let's attempt to grab the Dictionary by its ID. + const deletedData = datas.filter(item => item.id === Number(id)) + + // Respond with a "404 Not Found" response if the given + // Dictionary ID does not exist. + if (!deletedData) { + return new HttpResponse(null, { status: 404 }) + } + + // Delete the Dictionary from the "allDictionarys" map. + datas.pop() + + // Respond with a "200 OK" response and the deleted Dictionary. + return HttpResponse.json(deletedData) + }) +] diff --git a/src/mocks/roles.ts b/src/mocks/roles.ts new file mode 100644 index 0000000..f07a498 --- /dev/null +++ b/src/mocks/roles.ts @@ -0,0 +1,62 @@ +import { http, HttpResponse } from 'msw' +import type { Role } from 'src/api/models.type' + +const datas: Role[] = [] + +for (let i = 0; i < 20; i++) { + const data: Role = { + id: i, + name: 'role_' + i, + enabled: i % 3 > 0, + description: 'description', + lastModifiedDate: new Date() + } + datas.push(data) +} + +export const rolesHandlers = [ + http.get('/api/roles', ({ request }) => { + const url = new URL(request.url) + const page = url.searchParams.get('page') + const size = url.searchParams.get('size') + // Construct a JSON response with the list of all Dictionarys + // as the response body. + const data = { + content: Array.from(datas.slice(Number(page) * Number(size), (Number(page) + 1) * Number(size))), + totalElements: datas.length + } + + return HttpResponse.json(data) + }), + http.post('/api/roles', async ({ request }) => { + // Read the intercepted request body as JSON. + const newData = await request.json() as Role + + // Push the new Dictionary to the map of all Dictionarys. + datas.push(newData) + + // Don't forget to declare a semantic "201 Created" + // response and send back the newly created Dictionary! + return HttpResponse.json(newData, { status: 201 }) + }), + http.delete('/api/roles/:id', ({ params }) => { + // All request path params are provided in the "params" + // argument of the response resolver. + const { id } = params + + // Let's attempt to grab the Dictionary by its ID. + const deletedData = datas.filter(item => item.id === Number(id)) + + // Respond with a "404 Not Found" response if the given + // Dictionary ID does not exist. + if (!deletedData) { + return new HttpResponse(null, { status: 404 }) + } + + // Delete the Dictionary from the "allDictionarys" map. + datas.pop() + + // Respond with a "200 OK" response and the deleted Dictionary. + return HttpResponse.json(deletedData) + }) +] diff --git a/src/mocks/users.ts b/src/mocks/users.ts new file mode 100644 index 0000000..ae36a8e --- /dev/null +++ b/src/mocks/users.ts @@ -0,0 +1,66 @@ +import { http, HttpResponse } from 'msw' +import type { User } from 'src/api/models.type' + +const datas: User[] = [] + +for (let i = 0; i < 20; i++) { + const data: User = { + id: i, + username: 'username' + i, + firstname: 'firstname_' + i, + lastname: 'lastname_' + i, + enabled: i % 2 > 0, + accountNonLocked: i % 3 > 0, + accountExpiresAt: new Date(), + credentialsExpiresAt: new Date(), + lastModifiedDate: new Date() + } + datas.push(data) +} + +export const usersHandlers = [ + http.get('/api/users', ({ request }) => { + const url = new URL(request.url) + const page = url.searchParams.get('page') + const size = url.searchParams.get('size') + // Construct a JSON response with the list of all Dictionarys + // as the response body. + const data = { + content: Array.from(datas.slice(Number(page) * Number(size), (Number(page) + 1) * Number(size))), + totalElements: datas.length + } + + return HttpResponse.json(data) + }), + http.post('/api/users', async ({ request }) => { + // Read the intercepted request body as JSON. + const newData = await request.json() as User + + // Push the new Dictionary to the map of all Dictionarys. + datas.push(newData) + + // Don't forget to declare a semantic "201 Created" + // response and send back the newly created Dictionary! + return HttpResponse.json(newData, { status: 201 }) + }), + http.delete('/api/users/:id', ({ params }) => { + // All request path params are provided in the "params" + // argument of the response resolver. + const { id } = params + + // Let's attempt to grab the Dictionary by its ID. + const deletedData = datas.filter(item => item.id === Number(id)) + + // Respond with a "404 Not Found" response if the given + // Dictionary ID does not exist. + if (!deletedData) { + return new HttpResponse(null, { status: 404 }) + } + + // Delete the Dictionary from the "allDictionarys" map. + datas.pop() + + // Respond with a "200 OK" response and the deleted Dictionary. + return HttpResponse.json(deletedData) + }) +] diff --git a/src/pages/IndexPage.vue b/src/pages/IndexPage.vue index 8d98e94..0bb3c85 100644 --- a/src/pages/IndexPage.vue +++ b/src/pages/IndexPage.vue @@ -1,6 +1,8 @@ diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue index 77d4916..5281b12 100644 --- a/src/pages/LoginPage.vue +++ b/src/pages/LoginPage.vue @@ -2,72 +2,41 @@ - + - - - - - - - English(US) - - - 中文(简体) - - - 中文(繁體) - - - - + + + + + English(US) + + + 中文(简体) + + + 中文(繁體) + + + + - - + -
-
-
-
+
+
+
+
@@ -75,7 +44,7 @@
-
+
{{ $t('welcome') }} @@ -89,7 +58,8 @@ - +
@@ -104,7 +74,8 @@
+ :placeholder="$t('username')" + :rules="[(val) => (val && val.length > 0) || $t('username')]" /> @@ -116,7 +87,7 @@ + :loading="loading" class="full-width" title="signin_submit" />
@@ -128,10 +99,12 @@ - - - -

Copyright © 2018 - {{ new Date().getFullYear() }} leafage.top All rights reserved.

+ + + +

Copyright © 2018 - {{ new Date().getFullYear() }} + leafage.top + All rights reserved.

@@ -141,10 +114,12 @@ import { onBeforeMount, ref, onMounted } from 'vue' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' import lottie from 'lottie-web' +import { useQuasar } from 'quasar' import { api } from 'boot/axios' import { useUserStore } from 'stores/user-store' const router = useRouter() +const $q = useQuasar() const userStore = useUserStore() onBeforeMount(() => { @@ -153,7 +128,6 @@ onBeforeMount(() => { const { locale } = useI18n({ useScope: 'global' }) const isPwd = ref(true) const rememberMe = ref(true) -const darkTheme = ref(false) const loading = ref(false) const lottieRef = ref(null) @@ -178,17 +152,15 @@ function onSubmit() { loading.value = true api.post('/login', new URLSearchParams(form.value)).then(res => { - console.log(res.data) - userStore.updateUser(form.value.username) + userStore.updateUser(res.data.username) // 获取之前路由 const redirectRoute = router.currentRoute.value.query.redirect as string | undefined router.replace(redirectRoute || '/') - }).catch(error => { - console.error('Request failed:', error.message) - }).finally(() => { - // 在请求结束后执行 - loading.value = false - }) + }).catch(error => $q.notify({ type: 'negative', message: error.message })) + .finally(() => { + // 在请求结束后执行 + loading.value = false + }) } function show() { @@ -203,4 +175,3 @@ function show() { } } -src/stores/user-store diff --git a/src/pages/system/dictionary/IndexPage.vue b/src/pages/system/dictionary/IndexPage.vue index c712698..b86bc06 100644 --- a/src/pages/system/dictionary/IndexPage.vue +++ b/src/pages/system/dictionary/IndexPage.vue @@ -24,26 +24,49 @@ - + -