-
+
+
+
-
+
-
+
+
+
@@ -56,7 +64,9 @@
:disabled="logining"
>
-
+
+
+
@@ -66,6 +76,7 @@
-
+
-
+
+
+
@@ -96,7 +114,9 @@
:disabled="registering"
>
-
+
+
+
@@ -108,7 +128,9 @@
:disabled="registering"
>
-
+
+
+
@@ -118,6 +140,7 @@
-
diff --git a/src/components/VerifyInput.vue b/src/components/VerifyInput.vue
index 1663965..ea1dd5f 100644
--- a/src/components/VerifyInput.vue
+++ b/src/components/VerifyInput.vue
@@ -1,7 +1,9 @@
-
+
+
+
获取验证码
@@ -16,6 +18,7 @@
draggable="false"
:src="model.verifyImage.img"
@click="getVerifyImage"
+ alt="验证码"
/>
@@ -36,7 +39,8 @@ import { errorMessage, timeout } from '@/utils';
import { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { partial } from 'lodash-es';
-import { computed, ref, watchEffect } from 'vue';
+import { computed, ref, watch } from 'vue';
+
export type VerifyImagePath =
| 'none'
| 'fetching'
@@ -54,22 +58,32 @@ const canRefresh = computed(() => refreshCoolDown.value === 0);
const popOverMessage = ref('');
const refreshCoolDown = ref(0);
const verifyErrorMessage = partial(errorMessage, '获取验证码');
-watchEffect(() => {
- if (refreshCoolDown.value > 0) {
- popOverMessage.value = `过${refreshCoolDown.value.toFixed(0)}秒再换吧`;
- } else {
- popOverMessage.value = '看不清? 点击换一张';
+watch(
+ () => refreshCoolDown.value,
+ (value) => {
+ if (value > 0) {
+ popOverMessage.value = `过${refreshCoolDown.value.toFixed(0)}秒再换吧`;
+ } else {
+ popOverMessage.value = '看不清? 点击换一张';
+ }
}
-});
+);
+watch(
+ () => model.value.verifyImage,
+ (value) => {
+ if (value === 'none') {
+ refreshCoolDown.value = 0;
+ }
+ }
+);
+
async function cooldown() {
- for (
- refreshCoolDown.value = coolDownDuration;
- refreshCoolDown.value > 0;
- refreshCoolDown.value--
- ) {
+ for (refreshCoolDown.value = coolDownDuration; refreshCoolDown.value > 0; ) {
+ refreshCoolDown.value--;
await timeout(1000);
}
}
+
async function getVerifyImage() {
if (model.value.verifyImage === 'fetching' || !canRefresh.value) {
return;
diff --git a/src/keys/index.ts b/src/keys/index.ts
index 94a59f8..cb0ff5c 100644
--- a/src/keys/index.ts
+++ b/src/keys/index.ts
@@ -1,2 +1 @@
-import { type InjectionKey } from 'vue';
export {};
diff --git a/src/main.ts b/src/main.ts
index 44c7c38..1a4f57f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -5,5 +5,6 @@ import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from '@/App.vue';
import router from '@/router';
+
const app = createApp(App);
app.use(createPinia()).use(router).mount('#app');
diff --git a/src/router/index.ts b/src/router/index.ts
index ad868e6..058ea2d 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,61 +1,91 @@
-import { type Route } from '@/schemas';
+import { userInfoResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
-import { watch, type Component } from 'vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
+import axiosInstance from '@/api';
+import { ElMessage } from 'element-plus';
+import { errorMessage } from '@/utils';
+import { AxiosError } from 'axios';
+import { RoutePermission } from './permissions';
+
+export * from './permissions';
+
+declare module 'vue-router' {
+ // noinspection JSUnusedGlobalSymbols
+ interface RouteMeta {
+ permission: RoutePermission;
+ }
+}
const routes: RouteRecordRaw[] = [
{
path: '/',
- redirect: '/home'
+ component: () => import('@/views/MainPage.vue'),
+ meta: {
+ permission: RoutePermission.MAIN_PAGE
+ }
},
{
- path: '/404',
- component: () => import('@/views/NotFoundPage.vue')
+ path: '/user',
+ component: () => import('@/views/UserPage.vue'),
+ meta: {
+ permission: RoutePermission.USER_PAGE
+ }
+ },
+ {
+ path: '/user/:id',
+ component: () => import('@/views/UserPage.vue')
+ },
+ {
+ path: '/club',
+ component: () => import('@/views/ClubPage.vue'),
+ meta: {
+ permission: RoutePermission.CLUB_PAGE
+ }
+ },
+ {
+ path: '/:brokenPath',
+ redirect: '/'
}
];
const router = createRouter({
history: createWebHistory(),
routes: routes
});
-const componentMap: Record Promise> = {
- 'MainPage.vue': () => import('@/views/MainPage.vue')
-};
-function _getAvailableRoutes(routeResponses: Route[], root: boolean, pathList: string[]) {
- const res: RouteRecordRaw[] = [];
- const { routeMap } = useUserStore();
- for (const routeResponse of routeResponses) {
- const path = routeResponse.name.toLowerCase();
- pathList.push(path);
- const route = {
- path: root ? `/${path}` : path,
- component: componentMap[routeResponse.component],
- children: _getAvailableRoutes(routeResponse.children, false, pathList.concat(path))
- };
- routeMap.set(`/${pathList.join('/')}`, root ? route : routeMap.get(`/${pathList[0]}`)!);
- res.push(route);
- }
- return res;
-}
-export function getAvailableRoutes(routeResponse: Route[]) {
- _getAvailableRoutes(routeResponse, true, []);
-}
router.beforeEach(async (to) => {
+ console.log(to);
const userStore = useUserStore();
- console.log(2, userStore.userInfo);
- // await timeout(1000);
- console.log(userStore.initialized);
- if (!userStore.initialized) {
- await new Promise((resolve) => watch(() => userStore.initialized, resolve));
+ console.log(userStore.isInitialized);
+ if (!userStore.isInitialized) {
+ try {
+ const userInfoResponse = userInfoResponseSchema.parse(
+ (await axiosInstance.get('/api/user/info')).data
+ );
+ if (userInfoResponse.type === 'error') {
+ ElMessage.error(
+ errorMessage('获取用户信息失败', userInfoResponse.code, userInfoResponse.msg)
+ );
+ return false;
+ }
+ // 判断是否已登录.
+ if (userStore.token !== null) {
+ userStore.updateUserInfo(userInfoResponse);
+ }
+ userStore.permissions.push(...userInfoResponse.data.auth.permissions);
+ } catch (e) {
+ if (e instanceof AxiosError) {
+ ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
+ }
+ return false;
+ } finally {
+ userStore.isInitialized = true;
+ }
}
- if (userStore.addedRouteSet.has(to.fullPath) || to.fullPath === '/404') {
+ if (
+ to.meta.permission === undefined ||
+ userStore.permissions.find((fullPermission) => fullPermission.id === to.meta.permission)
+ ) {
return true;
- }
- const route = userStore.routeMap.get(to.fullPath);
- if (route !== undefined) {
- router.addRoute(route);
- userStore.addedRouteSet.add(to.fullPath);
- return to;
} else {
- return '/404';
+ return '/';
}
});
export default router;
diff --git a/src/router/permissions.ts b/src/router/permissions.ts
new file mode 100644
index 0000000..59bdfc9
--- /dev/null
+++ b/src/router/permissions.ts
@@ -0,0 +1,5 @@
+export enum RoutePermission {
+ MAIN_PAGE = 1,
+ USER_PAGE = 2,
+ CLUB_PAGE = 7
+}
diff --git a/src/schemas/index.ts b/src/schemas/index.ts
index 4dac032..4d50afd 100644
--- a/src/schemas/index.ts
+++ b/src/schemas/index.ts
@@ -1,4 +1,5 @@
import { z } from 'zod';
+
function createResponseSchema(data: T) {
return z.union([
z
@@ -10,8 +11,8 @@ function createResponseSchema(data: T) {
.extend({ data: data })
.transform((raw) =>
Object.assign(raw, {
- type: 'success' as const
- })
+ type: 'success'
+ } as const)
),
z
.object({
@@ -21,35 +22,39 @@ function createResponseSchema(data: T) {
})
.transform((raw) =>
Object.assign(raw, {
- type: 'error' as const
- })
+ type: 'error'
+ } as const)
)
]);
}
+
export type SucceedResponse = Extract;
export type ErrorResponse = Exclude, T>;
-const ordinaryResponse = createResponseSchema(z.literal(true));
-export const loginResponseSchema = ordinaryResponse;
-export const registerResponseSchema = ordinaryResponse;
-const idAndNameSchema = z.object({
+export const ordinarySchema = createResponseSchema(z.literal(true));
+export const loginResponseSchema = ordinarySchema;
+export const registerResponseSchema = ordinarySchema;
+export const idAndNameSchema = z.object({
id: z.number(),
name: z.string()
});
-export interface Route extends z.infer {
- component: string;
- children: Route[];
-}
-const routeResponsesSchema: z.ZodSchema = z.lazy(() =>
- idAndNameSchema.extend({ component: z.string(), children: routeResponsesSchema }).array()
-);
+
+const _authSchema = idAndNameSchema.extend({
+ permissions: idAndNameSchema.array()
+});
export const userInfoResponseSchema = createResponseSchema(
idAndNameSchema.extend({
avatar: z.nullable(z.string()),
- auth: idAndNameSchema.extend({
- permissions: idAndNameSchema.extend({ routers: routeResponsesSchema }).array()
- })
+ auth: _authSchema,
+ club: z.nullable(
+ idAndNameSchema.extend({
+ commit: z.string(),
+ auth: _authSchema
+ })
+ )
})
);
+export type SucceedUserInfoResponse = SucceedResponse>;
+export type UserInfo = SucceedUserInfoResponse['data'];
export const verifyResponseSchema = createResponseSchema(
z
.object({
diff --git a/src/stores/2048.ts b/src/stores/2048.ts
index 936e3c5..77a67b4 100644
--- a/src/stores/2048.ts
+++ b/src/stores/2048.ts
@@ -2,22 +2,27 @@ import { create2DArray, useRefresh } from '@/utils';
import { useLocalStorage } from '@vueuse/core';
import { defineStore } from 'pinia';
import { watch } from 'vue';
+
export interface Tile {
id: number;
number: number;
removed: boolean;
fromOthers: boolean;
}
+
export const useGame2048Store = defineStore('2048', () => {
const { key: gameKey, refresh: refreshGame } = useRefresh();
+
function create() {
return create2DArray(height.value, width.value, () => []);
}
+
function $reset() {
rawTileGrid.value = create();
isInitial.value = true;
refreshGame();
}
+
const width = useLocalStorage('2048-width', 4);
const height = useLocalStorage('2048-height', 4);
watch([width, height], $reset, { flush: 'sync' });
diff --git a/src/stores/background.ts b/src/stores/background.ts
index b8d80f2..5365652 100644
--- a/src/stores/background.ts
+++ b/src/stores/background.ts
@@ -1,6 +1,7 @@
import { IdDispenser } from '@/utils';
import { defineStore } from 'pinia';
import { reactive, readonly } from 'vue';
+
export type BackgroundURL = string | undefined;
export type BackgroundOptions = {
resolveTiming: 'transitionStart' | 'transitionEnd';
@@ -10,11 +11,14 @@ export type BackgroundTask = {
options: BackgroundOptions;
addFuture: PromiseWithResolvers;
};
+
export class CancelledError extends Error {}
+
export const useBackgroundStore = defineStore('background', () => {
const taskQueue = reactive([]);
const getFuturesMap = new Map>();
const compIdDispenser = new IdDispenser();
+
function getURL(id: number) {
const future = Promise.withResolvers();
if (taskQueue.length) {
@@ -24,6 +28,7 @@ export const useBackgroundStore = defineStore('background', () => {
}
return future.promise;
}
+
function addURL(
url: BackgroundURL,
options: BackgroundOptions = { resolveTiming: 'transitionEnd' }
@@ -45,13 +50,16 @@ export const useBackgroundStore = defineStore('background', () => {
}
return addFuture.promise;
}
+
function newCompId() {
return compIdDispenser.getId();
}
+
function unregisterComp(id: number) {
getFuturesMap.get(id)?.reject(new CancelledError());
getFuturesMap.delete(id);
}
+
function $reset() {
for (const task of taskQueue) {
task.addFuture.reject(new CancelledError());
@@ -62,6 +70,7 @@ export const useBackgroundStore = defineStore('background', () => {
unregisterComp(id);
}
}
+
return {
taskQueue: readonly(taskQueue),
getURL,
diff --git a/src/stores/user.ts b/src/stores/user.ts
index 2fbeac1..ad24852 100644
--- a/src/stores/user.ts
+++ b/src/stores/user.ts
@@ -1,12 +1,11 @@
-import { userInfoResponseSchema, type SucceedResponse } from '@/schemas';
+import type { SucceedUserInfoResponse, UserInfo } from '@/schemas';
+import { type idAndNameSchema } from '@/schemas';
import { StorageSerializers, useLocalStorage } from '@vueuse/core';
-import { pick } from 'lodash-es';
import { defineStore } from 'pinia';
import { reactive, ref } from 'vue';
-import { type RouteRecordRaw } from 'vue-router';
import { z } from 'zod';
-type SucceedUserInfoResponse = SucceedResponse>;
-type UserInfo = Pick;
+
+type FullPermission = z.infer;
export const useUserStore = defineStore('user', () => {
const token = useLocalStorage('token', null, {
serializer: StorageSerializers.string
@@ -14,25 +13,25 @@ export const useUserStore = defineStore('user', () => {
const userInfo = useLocalStorage('user-info', null, {
serializer: StorageSerializers.object
});
- const routeMap = reactive(new Map());
- const addedRouteSet = reactive(new Set());
+ const permissions = reactive([]);
const isInitialized = ref(false);
+
function updateUserInfo(userInfoResponse: SucceedUserInfoResponse) {
- userInfo.value = pick(userInfoResponse.data, 'id', 'name', 'avatar');
+ userInfo.value = userInfoResponse.data;
}
+
function $reset() {
token.value = null;
userInfo.value = null;
- routeMap.clear();
- addedRouteSet.clear();
+ permissions.splice(0);
isInitialized.value = false;
}
+
return {
token,
userInfo,
- routeMap,
- addedRouteSet,
- initialized: isInitialized,
+ permissions,
+ isInitialized,
updateUserInfo,
$reset
};
diff --git a/src/utils/2d-array.ts b/src/utils/2d-array.ts
index e152bf1..7882226 100644
--- a/src/utils/2d-array.ts
+++ b/src/utils/2d-array.ts
@@ -3,6 +3,7 @@ export function create2DArray(height: number, width: number, cell: T | (() =>
Array.from({ length: width }, () => (cell instanceof Function ? cell() : cell))
);
}
+
export function get2DArrayItem(
grid: T[][],
first: number,
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 2dd95f6..be4666a 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,16 +1,20 @@
-import { ref, watch } from 'vue';
+import { ref } from 'vue';
+
export function timeout(delay: number = 0) {
return new Promise((resolve) => setTimeout(resolve, delay));
}
+
export class IdDispenser {
#maxId: number;
#usedIdSet: Set;
#unUsedIdSet: Set;
+
constructor() {
this.#maxId = 0;
this.#usedIdSet = new Set();
this.#unUsedIdSet = new Set();
}
+
getId(...identifiersToRemove: number[]) {
let id: number;
if (this.#unUsedIdSet.size) {
@@ -23,6 +27,7 @@ export class IdDispenser {
this.removeId(...identifiersToRemove);
return id;
}
+
removeId(...identifiers: number[]) {
for (const id of identifiers) {
if (!this.#usedIdSet.delete(id)) {
@@ -31,6 +36,7 @@ export class IdDispenser {
this.#unUsedIdSet.add(id);
}
}
+
reset() {
this.#maxId = 0;
this.#unUsedIdSet.clear();
diff --git a/src/views/ClubPage.vue b/src/views/ClubPage.vue
new file mode 100644
index 0000000..2041921
--- /dev/null
+++ b/src/views/ClubPage.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/views/Game2048Page.vue b/src/views/Game2048Page.vue
index 402f5a8..e78d4fa 100644
--- a/src/views/Game2048Page.vue
+++ b/src/views/Game2048Page.vue
@@ -23,9 +23,11 @@
align-items: center;
background-color: rgb(250, 248, 239);
}
+
.el-input {
width: auto;
}
+
.game-container {
margin-bottom: 10px;
}
@@ -34,8 +36,10 @@
import Game2048 from '@/components/Game2048.vue';
import { useGame2048Store } from '@/stores';
import { ref } from 'vue';
+
const showSettings = ref(false);
const game2048Store = useGame2048Store();
+
function click() {
// game2048Store.width = 4;
// game2048Store.height = 4;
diff --git a/src/views/MainPage.vue b/src/views/MainPage.vue
index e08b31a..2041921 100644
--- a/src/views/MainPage.vue
+++ b/src/views/MainPage.vue
@@ -1,5 +1,5 @@
-test{{ Math.random() }}
-
-
+
+
+
+
+
diff --git a/src/views/NotFoundPage.vue b/src/views/NotFoundPage.vue
deleted file mode 100644
index 2300138..0000000
--- a/src/views/NotFoundPage.vue
+++ /dev/null
@@ -1 +0,0 @@
-404
diff --git a/src/views/UserPage.vue b/src/views/UserPage.vue
new file mode 100644
index 0000000..7969f3a
--- /dev/null
+++ b/src/views/UserPage.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tsconfig.app.json b/tsconfig.app.json
index da9aa92..47b1681 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -8,7 +8,7 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["src/*"]
},
"types": ["unplugin-icons/types/vue"],
"jsx": "preserve",