🎈 perf: 改进路由

This commit is contained in:
Litrix 2024-12-10 20:04:14 +08:00
parent 4809a1eb85
commit 85300fa2d2
20 changed files with 339 additions and 306 deletions

View File

@ -5,5 +5,5 @@
"singleQuote": true,
"printWidth": 100,
"htmlWhitespaceSensitivity": "ignore",
"trailingComma": "none"
"trailingComma": "all"
}

View File

@ -6,10 +6,10 @@
<div class="header-title" @click="navigate()">社团展示系统</div>
</router-link>
<transition name="header-content-container">
<template v-if="arrayIncludes(['initialized', 'failed'], userStore.userInfoStatus)">
<template v-if="!userStore.isInitializing">
<nav class="header-content-container">
<el-menu
:default-active="route.fullPath"
:default-active="$route.fullPath"
:ellipsis="false"
:router="true"
mode="horizontal"
@ -64,28 +64,6 @@
</transition>
</router-view>
<transition name="page-root"></transition>
<!-- <div
:style="
pageStore.pageLoadingCount
? {
opacity: 1,
transition: 'opacity 0.75s 0.15s',
zIndex: 99
}
: {
opacity: 0,
transition: 'opacity 0.3s',
zIndex: -1
}
"
class="page-loading-mask"
>
<div class="page-loading-icon-container">
<el-icon class="loading-icon is-loading" color="white" size="60">
<icon-cs-loading />
</el-icon>
</div>
</div> -->
</el-main>
</el-container>
<el-dialog
@ -363,27 +341,28 @@ import { type VerifyImagePath } from '@/components/VerifyInput.vue';
import { loginResponseSchema, registerResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
import { usePageStore } from '@/stores/page';
import { arrayIncludes, errorMessage } from '@/utils';
import { errorMessage } from '@/utils';
import { Avatar, CloseBold } from '@element-plus/icons-vue';
import { AxiosError } from 'axios';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { partial } from 'lodash-es';
import { reactive, ref, watch } from 'vue';
import { RouterView, useRoute, useRouter } from 'vue-router';
import { RouterView, useRoute } from 'vue-router';
import router from './router';
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
const pageStore = usePageStore();
const showLoginRegisterDialog = ref(false);
watch(
() => userStore.userInfoStatus,
() => userStore.isInitializing,
(value) => {
if (value === 'initialized' && userStore.token === null) {
console.log(1);
if (!value && userStore.token === null) {
showLoginRegisterDialog.value = true;
}
}
},
{ immediate: true },
);
const showLoginRegisterDialog = ref(false);
watch(showLoginRegisterDialog, (value) => {
if (value) {
logining.value = false;
@ -400,21 +379,21 @@ let loginFormData = reactive({
username: 'wubaopu2',
password: '123456',
verifyImage: 'none' as VerifyImagePath,
verifyCode: ''
verifyCode: '',
});
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [
{ required: true, message: '请输入用户名' },
{ min: 6, message: '用户名长度不能小于3位' }
{ min: 6, message: '用户名长度不能小于6位' },
],
password: [
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码长度不能小于6位' }
{ min: 6, message: '密码长度不能小于6位' },
],
verifyCode: [
{ required: true, message: '请输入验证码' },
{ pattern: /[0-9A-Za-z]{4}/, message: '验证码不符合格式' }
]
{ pattern: /[0-9A-Za-z]{4}/, message: '验证码不符合格式' },
],
});
const logining = ref(false);
const registerFormRef = ref<FormInstance>();
@ -423,7 +402,7 @@ let registerFormData = reactive({
password: '',
confirmPassword: '',
verifyImage: 'none' as VerifyImagePath,
verifyCode: ''
verifyCode: '',
});
const registerFormRules = reactive<FormRules<typeof registerFormData>>({
...loginFormRules,
@ -439,9 +418,9 @@ const registerFormRules = reactive<FormRules<typeof registerFormData>>({
} else {
callback();
}
}
}
]
},
},
],
});
const registering = ref(false);
const loginErrorMessage = partial(errorMessage, '登录');
@ -452,6 +431,12 @@ interface LoginParams {
key: string;
code: string;
}
watch(
() => userStore.userInfo,
() => {
router.push({ path: router.currentRoute.value.fullPath, force: true });
},
);
async function login(params: LoginParams): Promise<boolean> {
const loginRespRaw = await axiosInstance
@ -466,12 +451,10 @@ async function login(params: LoginParams): Promise<boolean> {
ElMessage.error(loginErrorMessage(loginResp.code, loginResp.msg));
return false;
}
await userStore.updateSelfUserInfo(true);
return true;
return userStore.updateSelfUserInfo(true);
}
async function submitLoginForm() {
let succeed = false;
logining.value = true;
try {
try {
@ -487,22 +470,22 @@ async function submitLoginForm() {
username,
password,
verifyImage: { key },
verifyCode: code
verifyCode: code,
} = loginFormData;
succeed = await login({
username,
password,
key,
code
});
if (
!(await login({
username,
password,
key,
code,
}))
)
return;
showLoginRegisterDialog.value = false;
} finally {
if (succeed) {
showLoginRegisterDialog.value = false;
} else {
loginFormData.verifyImage = 'none';
loginFormRef.value?.resetFields('verifyCode');
logining.value = false;
}
loginFormData.verifyImage = 'none';
loginFormRef.value?.resetFields('verifyCode');
logining.value = false;
}
}
@ -512,30 +495,25 @@ interface RegisterParams extends LoginParams {
auth: number;
}
async function register(params: RegisterParams) {
try {
const registerResp = registerResponseSchema.parse(
(await axiosInstance.put('/api/user/create', params)).data
);
if (registerResp.type === 'error') {
ElMessage.error({
message: registerErrorMessage(registerResp.code, registerResp.msg)
});
return false;
}
return true;
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(registerErrorMessage(e.code, e.message));
} else {
ElMessage.error('error');
}
async function register(params: RegisterParams): Promise<boolean> {
const raw = await axiosInstance
.put<RawResp>('/api/user/create', params)
.then((r) => r.data)
.catch((err: AxiosError) => {
ElMessage.error(registerErrorMessage(err.code, err.message));
});
if (!raw) return false;
const resp = registerResponseSchema.parse(raw);
if (resp.type === 'error') {
ElMessage.error({
message: registerErrorMessage(resp.code, resp.msg),
});
return false;
}
return userStore.updateSelfUserInfo(true);
}
async function submitRegisterForm() {
let succeed = false;
registering.value = true;
try {
try {
@ -551,23 +529,19 @@ async function submitRegisterForm() {
username,
password,
verifyImage: { key },
verifyCode: code
verifyCode: code,
} = registerFormData;
succeed = await register({ username, password, key, code, auth: 1 });
if (!(await register({ username, password, key, code, auth: 1 }))) return;
showLoginRegisterDialog.value = false;
} finally {
if (succeed) {
userStore.userInfoStatus = 'uninitialized';
await router.push({ path: route.fullPath, force: true });
} else {
registerFormData.verifyImage = 'none';
registerFormRef.value?.resetFields('verifyCode');
registering.value = false;
}
registerFormData.verifyImage = 'none';
registerFormRef.value?.resetFields('verifyCode');
registering.value = false;
}
}
async function logout() {
userStore.$reset();
userStore.token = null;
await userStore.updateSelfUserInfo(true);
}
</script>

View File

@ -3,7 +3,7 @@ import axios from 'axios';
const baseURL = 'https://wzpmc.cn:18080/';
const axiosInstance = axios.create({
baseURL
baseURL,
});
// 自动添加token到请求中.
axiosInstance.interceptors.request.use((config) => {
@ -16,6 +16,7 @@ axiosInstance.interceptors.response.use((response) => {
const userStore = useUserStore();
const authorization = response.headers['set-authorization'] as string | undefined;
if (authorization) {
console.log(123);
userStore.token = authorization;
}
return response;

View File

@ -41,7 +41,7 @@ import {
type BackgroundTask,
type BackgroundURL,
CancelledError,
useBackgroundStore
useBackgroundStore,
} from '@/stores';
import 'element-plus/theme-chalk/index.css';
import { onMounted, onUnmounted, ref } from 'vue';

View File

@ -23,8 +23,8 @@
:style="[
getTilePosition(y - 1, x - 1),
{
animationDelay: `${0.1 * (x + y - 2)}s`
}
animationDelay: `${0.1 * (x + y - 2)}s`,
},
]"
class="background-tile"
></div>
@ -41,8 +41,8 @@
'tile',
{
'tile-appear-from-others': tile.fromOthers,
'tile-transition-cancelled': transitionCancelled
}
'tile-transition-cancelled': transitionCancelled,
},
]"
:data-tile-id="tile.id"
:style="[getTilePosition(y, x), getNumberTileStyle(tile)]"
@ -204,7 +204,7 @@ enum Directions {
UP = 'ArrowUp',
DOWN = 'ArrowDown',
LEFT = 'ArrowLeft',
RIGHT = 'ArrowRight'
RIGHT = 'ArrowRight',
}
type SingleTileLine = (Tile | undefined)[];
@ -265,7 +265,7 @@ const borderWidth = 15;
const { width, height } = storeToRefs(game2048Store);
const containerSizeStyle = computed(() => ({
minWidth: `${width.value * tileWidth + (width.value + 1) * borderWidth}px`,
minHeight: `${height.value * tileWidth + (height.value + 1) * borderWidth}px`
minHeight: `${height.value * tileWidth + (height.value + 1) * borderWidth}px`,
}));
/**
* 是否显示容器.
@ -288,7 +288,7 @@ watch(gameStatus, (status) => {
message: `单个块分数达到${game2048Store.successNumber}`,
type: 'success',
duration: 3500,
offset: 50
offset: 50,
});
break;
case 'failed':
@ -297,7 +297,7 @@ watch(gameStatus, (status) => {
message: '你无路可走',
type: 'error',
duration: 3500,
offset: 50
offset: 50,
});
break;
}
@ -327,7 +327,7 @@ const tileBackgroundColorMapping: Record<number, string | undefined> = {
256: 'rgb(237, 204, 97)',
512: 'rgb(237, 200, 80)',
1024: 'rgb(237, 197, 63)',
2048: 'rgb(237, 194, 46)'
2048: 'rgb(237, 194, 46)',
};
/**
@ -335,7 +335,7 @@ const tileBackgroundColorMapping: Record<number, string | undefined> = {
*/
function storeTileGrid() {
game2048Store.rawTileGrid = tileGrid.map((row) =>
row.map((tiles) => tiles.map((tile) => pick(tile, 'number', 'removed')))
row.map((tiles) => tiles.map((tile) => pick(tile, 'number', 'removed'))),
);
}
@ -345,7 +345,7 @@ function storeTileGrid() {
function getTilePosition(y: number, x: number) {
return {
left: `${borderWidth + x * (tileWidth + borderWidth)}px`,
top: `${borderWidth + y * (tileWidth + borderWidth)}px`
top: `${borderWidth + y * (tileWidth + borderWidth)}px`,
};
}
@ -357,7 +357,7 @@ function getNumberTileStyle(tile: Tile) {
return {
fontSize: `${number < 128 ? 55 : number < 1024 ? 45 : 35}px`,
color: number < 8 ? 'rgb(119, 110, 101)' : 'white',
backgroundColor: tileBackgroundColorMapping[number] ?? 'rgb(56, 56, 56)'
backgroundColor: tileBackgroundColorMapping[number] ?? 'rgb(56, 56, 56)',
};
}
@ -386,7 +386,7 @@ function addRandomTiles(randomCount: number, randomNumbers: number[] = [2, 4]) {
id: maxId++,
number: sample(randomNumbers) ?? 2,
removed: false,
fromOthers: false
fromOthers: false,
};
tiles.push(tile);
tileGrid[y][x].push(tile);
@ -394,7 +394,7 @@ function addRandomTiles(randomCount: number, randomNumbers: number[] = [2, 4]) {
}
return {
tiles,
promise: Promise.all(tileAddPromises).then<void>(() => undefined)
promise: Promise.all(tileAddPromises).then<void>(() => undefined),
};
}
@ -433,13 +433,13 @@ function mergeLineImpl(tileRelationMap: TileRelationMap, line: SingleTileLine, o
id: maxId++,
number: tile.number * 2,
removed: false,
fromOthers: true
fromOthers: true,
};
tileRelationMap.set(
newTile,
(tileRelationMap.get(prevTile) ?? [prevTile]).concat(
tileRelationMap.get(tile) ?? [tile]
)
tileRelationMap.get(tile) ?? [tile],
),
);
prev = once ? undefined : [i, newTile];
line[i] = newTile;
@ -493,9 +493,9 @@ function mergeLine(line: SingleTileLine): [lineMergeREsult: LineMergeResult, cha
originalLine,
transitionLine,
resultLine: line.map((tile) => (tile !== undefined ? [tile] : [])),
tileRelationMap
tileRelationMap,
},
changed
changed,
];
}
@ -506,14 +506,14 @@ function mergeLine(line: SingleTileLine): [lineMergeREsult: LineMergeResult, cha
*/
async function mergeTiles(direction: Directions) {
function getMergedGrid(
d: Directions = direction
d: Directions = direction,
): [MergedGrid, changed: boolean, maxNumber: number] {
let changed = false;
let maxNumber = 0;
const mergedGrid: MergedGrid = Array(firstIndices.length).fill(undefined);
for (const first of firstIndices(d)) {
const line = secondIndices(d).map((second) =>
get2DArrayItem(tileGrid, first, second, isColFirst(d)).at(0)
get2DArrayItem(tileGrid, first, second, isColFirst(d)).at(0),
);
const [lineMergeResult, lineChanged] = mergeLine(line);
@ -547,8 +547,8 @@ async function mergeTiles(direction: Directions) {
lineIndex: number;
},
first: number,
second: number
) => void
second: number,
) => void,
) {
forEachMergedLine((lineMergeResult, first) => {
for (const [lineIndex, second] of secondIndices().entries()) {
@ -557,10 +557,10 @@ async function mergeTiles(direction: Directions) {
{
tiles,
lineIndex,
...lineMergeResult
...lineMergeResult,
},
first,
second
second,
);
}
});
@ -583,7 +583,7 @@ async function mergeTiles(direction: Directions) {
}
break;
}
})()
})(),
);
}
@ -613,7 +613,7 @@ async function mergeTiles(direction: Directions) {
}
break;
}
})()
})(),
);
}
@ -632,7 +632,7 @@ async function mergeTiles(direction: Directions) {
tileTransitionPromises.push(
...transitionTiles
.filter((tile) => tile !== originalLine[lineIndex]) // .
.map((tile) => tileTransitionFutureMap.add(tile.id))
.map((tile) => tileTransitionFutureMap.add(tile.id)),
);
}
tiles.splice(0, tiles.length, ...transitionTiles);
@ -644,7 +644,7 @@ async function mergeTiles(direction: Directions) {
tileAddPromises.push(
...resultTiles
.filter((tile) => !originalLine.includes(tile)) // .
.map((tile) => tileAddFutureMap.add(tile.id))
.map((tile) => tileAddFutureMap.add(tile.id)),
);
tiles.splice(0, tiles.length, ...resultTiles);
});
@ -738,11 +738,11 @@ onMounted(async () => {
return {
...rawTile,
id,
fromOthers: false
fromOthers: false,
};
})
)
)
}),
),
),
);
if (game2048Store.isInitial) {
const { tiles, promise: newTilePromise } = addRandomTiles(2);
@ -765,7 +765,7 @@ onUnmounted(() => {
isUnmounted = true;
for (const future of chainIterables([
tileAddFutureMap.values(),
tileTransitionFutureMap.values()
tileTransitionFutureMap.values(),
])) {
future.resolve();
}

View File

@ -51,7 +51,7 @@ watch(
() => props.score,
(score) => {
scoreQueue.push(score);
}
},
);
onMounted(async () => {
let continuouslyRolling = false;
@ -61,7 +61,7 @@ onMounted(async () => {
await scoreDiv.value?.animate([{ transform: 'none' }, { transform: 'translateY(-100%)' }], {
easing: continuouslyRolling ? 'linear' : 'ease-in',
fill: 'forwards',
duration: 100 / (scoreQueue.size + 1 + Number(continuouslyRolling))
duration: 100 / (scoreQueue.size + 1 + Number(continuouslyRolling)),
}).finished;
displayedScore.value = score;
await nextTick();
@ -69,7 +69,7 @@ onMounted(async () => {
await scoreDiv.value?.animate([{ transform: 'translateY(100%)' }, { transform: 'none' }], {
easing: continuouslyRolling ? 'linear' : 'ease-out',
fill: 'forwards',
duration: 100 / (scoreQueue.size + 1)
duration: 100 / (scoreQueue.size + 1),
}).finished;
} catch (e) {
if (

View File

@ -6,7 +6,9 @@
</el-icon>
</template>
<template #append>
<el-button v-if="model.verifyImage === 'none'" @click="updateVerifyImage">获取验证码</el-button>
<el-button v-if="model.verifyImage === 'none'" @click="updateVerifyImage">
获取验证码
</el-button>
<el-icon v-else-if="model.verifyImage === 'fetching'" class="is-loading">
<icon-ep-loading />
</el-icon>
@ -66,7 +68,7 @@ watch(
} else {
popOverMessage.value = '看不清? 点击换一张';
}
}
},
);
watch(
() => model.value.verifyImage,
@ -74,7 +76,7 @@ watch(
if (value === 'none') {
refreshCoolDown.value = 0;
}
}
},
);
async function cooldown() {
@ -94,7 +96,7 @@ async function updateVerifyImage() {
try {
try {
const verifyResponse = verifyResponseSchema.parse(
(await axiosInstance.get('/api/user/verify')).data
(await axiosInstance.get('/api/user/verify')).data,
);
console.log(verifyResponse);
if (verifyResponse.type === 'error') {
@ -104,7 +106,7 @@ async function updateVerifyImage() {
const { img, key } = verifyResponse.data;
model.value.verifyImage = {
key: key,
img
img,
};
cooldown().then();
succeed = true;

View File

@ -1,6 +1,6 @@
import { useUserStore } from '@/stores';
import { type PageErrorReason, usePageStore } from '@/stores/page';
import { IdPool } from '@/utils';
import { usePageStore } from '@/stores/page';
import { PageErrorType, type PageErrorReason } from '@/views/ErrorPage.vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import { RoutePermissionId } from './permissions';
@ -17,98 +17,82 @@ declare module 'vue-router' {
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/views/MainPage.vue')
},
{
path: '/user',
component: () => import('@/views/UserPage.vue'),
meta: {
permissionId: RoutePermissionId.USER_PAGE
}
component: () => import('@/views/MainPage.vue'),
},
{
path: '/user/:id',
component: () => import('@/views/UserPage.vue')
component: () => import('@/views/UserPage.vue'),
},
{
path: '/club',
component: () => import('@/views/ClubPage.vue'),
meta: {
permissionId: RoutePermissionId.CLUB_PAGE
}
permissionId: RoutePermissionId.CLUB_PAGE,
},
},
{
path: '/2048',
component: () => import('@/views/Game2048Page.vue')
component: () => import('@/views/Game2048Page.vue'),
// meta: {
// permissionId: RoutePermissionId.USER_PAGE,
// },
},
{
path: '/:path(.*)*',
name: 'NotFound',
component: () => import('@/views/ErrorPage.vue')
}
name: 'notFound',
component: () => import('@/views/ErrorPage.vue'),
props: {
type: PageErrorType.NOT_FOUND,
} satisfies PageErrorReason,
},
];
(function processRoutes(routes: RouteRecordRaw[], idPool: IdPool) {
for (const route of routes) {
if (route.name === undefined) {
route.name = `AnonymousPage${idPool.newId()}`;
processRoutes(route.children ?? [], idPool);
}
}
})(routes, new IdPool(1));
export const tempErrorPageName = '_TempError';
// (function processRoutes(routes: RouteRecordRaw[], idPool: IdPool) {
// for (const route of routes) {
// if (route.name === undefined) {
// route.name = `AnonymousPage${idPool.newId()}`;
// processRoutes(route.children ?? [], idPool);
// }
// }
// })(routes, new IdPool(1));
const router = createRouter({
history: createWebHistory(),
routes: routes
routes: routes,
});
router.beforeEach(async (to, from) => {
router.beforeEach(async (to) => {
const userStore = useUserStore();
const pageStore = usePageStore();
const { permissionId } = to.meta;
pageStore.setNewRouteId(to);
try {
switch (userStore.userInfoStatus) {
case 'uninitialized': {
const succeed = await userStore.updateSelfUserInfo(true);
if (!succeed) {
if (permissionId === undefined) {
return;
}
return pageStore.createTempErrorRoute(
{
type: 'networkError',
originalPath: to.fullPath
},
to
);
if (!userStore.userInfo) {
const succeed = await userStore.updateSelfUserInfo(true);
if (!succeed) {
if (permissionId === undefined) {
return;
}
break;
return pageStore.createTempErrorRoute(
{
type: PageErrorType.NETWORK_ERROR,
},
to,
);
}
case 'initializing':
return false;
}
console.log(userStore.permissions);
if (permissionId) {
if (userStore.hasPermission(permissionId)) {
return true;
} else {
return pageStore.createTempErrorRoute({ type: 'noPermission' }, from);
return pageStore.createTempErrorRoute({ type: PageErrorType.NO_PERMISSION }, to);
}
}
} finally {
pageStore.removeRouteId(to);
}
});
router.beforeEach((to) => {
const pageStore = usePageStore();
switch (to.name) {
case 'NotFound':
pageStore.pageErrorReason = { type: 'notFound' };
break;
case undefined:
pageStore.pageErrorReason = undefined;
}
});
// router.afterEach(
// useThrottleFn(
// async (to) => {
@ -130,8 +114,5 @@ router.beforeEach((to) => {
router.afterEach((to) => {
const pageStore = usePageStore();
pageStore.removeRouteId(to);
if (to.name === tempErrorPageName) {
router.removeRoute(tempErrorPageName);
}
});
export default router;

View File

@ -1,5 +1,5 @@
export enum RoutePermissionId {
MAIN_PAGE = 1,
USER_PAGE = 2,
CLUB_PAGE = 7
CLUB_PAGE = 7,
}

View File

@ -6,25 +6,25 @@ function createResponseSchema<T extends z.ZodTypeAny>(data: T) {
.object({
code: z.number().min(200).max(299),
msg: z.string(),
time: z.coerce.date()
time: z.coerce.date(),
})
.extend({ data: data })
.transform((raw) =>
Object.assign(raw, {
type: 'success'
} as const)
type: 'success',
} as const),
),
z
.object({
code: z.number(),
msg: z.string(),
time: z.coerce.date()
time: z.coerce.date(),
})
.transform((raw) =>
Object.assign(raw, {
type: 'error'
} as const)
)
type: 'error',
} as const),
),
]);
}
@ -35,11 +35,11 @@ export const loginResponseSchema = ordinarySchema;
export const registerResponseSchema = ordinarySchema;
export const idAndNameSchema = z.object({
id: z.number(),
name: z.string()
name: z.string(),
});
const _authSchema = idAndNameSchema.extend({
permissions: idAndNameSchema.array()
permissions: idAndNameSchema.array(),
});
export const userInfoResponseSchema = createResponseSchema(
idAndNameSchema.extend({
@ -48,10 +48,10 @@ export const userInfoResponseSchema = createResponseSchema(
club: z.nullable(
idAndNameSchema.extend({
commit: z.string(),
auth: _authSchema
})
)
})
auth: _authSchema,
}),
),
}),
);
export type SucceedUserInfoResponse = SucceedResponse<z.infer<typeof userInfoResponseSchema>>;
export type UserInfo = SucceedUserInfoResponse['data'];
@ -59,10 +59,10 @@ export const verifyResponseSchema = createResponseSchema(
z
.object({
img: z.string(),
key: z.string()
key: z.string(),
})
.transform((raw) => {
raw.img = `data:image/png;base64,${raw.img}`;
return raw;
})
}),
);

View File

@ -39,14 +39,14 @@ export const useGame2048Store = defineStore('2048', () => {
watch([width, height], $reset, { flush: 'sync' });
const rawTileGrid = useLocalStorage<Pick<Tile, 'number' | 'removed'>[][][]>(
'2048-tile-grid',
create()
create(),
);
watch(
rawTileGrid,
() => {
isInitial.value = false;
},
{ flush: 'sync' }
{ flush: 'sync' },
);
const isInitial = useLocalStorage('2048-is-initial', true);
return {
@ -59,6 +59,6 @@ export const useGame2048Store = defineStore('2048', () => {
rawTileGrid,
isInitial,
gameKey,
$reset
$reset,
};
});

View File

@ -1,5 +1,6 @@
import { DoubleQueue } from '@/utils/double-queue';
import { defineStore } from 'pinia';
import { reactive, ref } from 'vue';
import { reactive, readonly, ref } from 'vue';
export type BackgroundURL = string | undefined;
export type BackgroundOptions = {
@ -12,15 +13,14 @@ export type BackgroundTask = {
};
export class CancelledError extends Error {}
export const useBackgroundStore = defineStore('background', () => {
const taskQueue = reactive<BackgroundTask[]>([]);
const taskQueue = new DoubleQueue<BackgroundTask>();
const getFuturesMap = new Map<number, PromiseWithResolvers<BackgroundTask>>();
const maxCompId = ref(0);
function getURL(id: number) {
const future = Promise.withResolvers<BackgroundTask>();
if (taskQueue.length) {
if (taskQueue.size) {
future.resolve(taskQueue.shift()!);
} else {
getFuturesMap.set(id, future);
@ -30,7 +30,7 @@ export const useBackgroundStore = defineStore('background', () => {
function addURL(
url: BackgroundURL,
options: BackgroundOptions = { resolveTiming: 'transitionEnd' }
options: BackgroundOptions = { resolveTiming: 'transitionEnd' },
) {
const addFuture = Promise.withResolvers<void>();
// addFuture.promise.then(() => console.log('resolve', getFuturesMap));
@ -38,7 +38,7 @@ export const useBackgroundStore = defineStore('background', () => {
const task: BackgroundTask = {
url,
options,
addFuture: addFuture
addFuture: addFuture,
};
if (getFuturesMap.size) {
const [[id, getFuture]] = getFuturesMap;
@ -63,7 +63,6 @@ export const useBackgroundStore = defineStore('background', () => {
for (const task of taskQueue) {
task.addFuture.reject(new CancelledError());
}
taskQueue.splice(0);
console.log(taskQueue);
for (const id of [...getFuturesMap.keys()]) {
unregisterComp(id);
@ -71,11 +70,10 @@ export const useBackgroundStore = defineStore('background', () => {
}
return {
taskQueue: taskQueue,
getURL,
addURL,
newCompId,
unregisterComp,
$reset
$reset,
};
});

View File

@ -1,27 +1,16 @@
import router, { tempErrorPageName } from '@/router';
import router from '@/router';
import { IdPool } from '@/utils';
import type { PageErrorReason } from '@/views/ErrorPage.vue';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { computed } from 'vue';
import {
type RouteLocationNormalized,
type RouteLocationRaw,
type RouteRecordRaw,
useRoute
} from 'vue-router';
export type PageErrorReason =
| {
type: 'notFound';
}
| {
type: 'networkError';
originalPath: string;
}
| {
type: 'noPermission';
};
const tempErrorRouteName = '_TempError';
export const usePageStore = defineStore('page-store', () => {
const pageErrorReason = ref<PageErrorReason>();
const routeIdPool = new IdPool();
const routeIds = routeIdPool.usedIdSet;
const pageLoadingCount = computed(() => routeIds.size);
@ -41,35 +30,30 @@ export const usePageStore = defineStore('page-store', () => {
function createTempErrorRoute(
reason: Exclude<
PageErrorReason,
| {
type: 'notFound';
}
| undefined
{
type: 'notFound';
}
>,
currentRoute: RouteLocationNormalized | undefined = useRoute(),
root: boolean = false
to: RouteLocationNormalized,
): RouteLocationRaw {
pageErrorReason.value = reason;
const tempErrorRoute: RouteRecordRaw = {
path: currentRoute.fullPath,
name: tempErrorPageName,
component: () => import('@/views/ErrorPage.vue')
path: to.fullPath,
name: tempErrorRouteName,
component: () => import('@/views/ErrorPage.vue'),
props: reason,
};
const parentRouteName = currentRoute.matched.at(-2)?.name;
if (parentRouteName !== undefined && !root) {
router.addRoute(parentRouteName, tempErrorRoute);
} else {
router.addRoute(tempErrorRoute);
if (router.hasRoute(tempErrorRouteName)) {
router.removeRoute(tempErrorRouteName);
}
return { name: tempErrorPageName, replace: true };
router.addRoute(tempErrorRoute);
return { name: tempErrorRouteName, replace: true };
}
return {
pageErrorReason,
routeIds,
pageLoadingCount,
setNewRouteId,
removeRouteId,
createTempErrorRoute
createTempErrorRoute,
};
});

View File

@ -12,59 +12,53 @@ import { z } from 'zod';
type Permission = z.infer<typeof idAndNameSchema>;
export const useUserStore = defineStore('user', () => {
const token = useLocalStorage<string | null>('token', null, {
serializer: StorageSerializers.string
serializer: StorageSerializers.string,
});
const userInfo = useLocalStorage<UserInfo | null>('user-info', null, {
serializer: StorageSerializers.object
serializer: StorageSerializers.object,
});
const permissions = computed<Permission[]>(() => userInfo.value?.auth.permissions ?? []);
const userInfoStatus = ref<'uninitialized' | 'initializing' | 'initialized'>('uninitialized');
const isInitializing = ref(false);
watch(
userInfo,
(info) => {
userInfoStatus.value = info ? 'initialized' : 'uninitialized';
isInitializing.value = !!info;
},
{ flush: 'sync' }
{ flush: 'sync' },
);
async function updateSelfUserInfo(
showErrorMessage: boolean,
silent: boolean = false
): Promise<boolean> {
if (!silent) userInfoStatus.value = 'initializing';
const raw = await axiosInstance
.get<RawResp>('/api/user/info')
.then((r) => r.data)
.catch((e: AxiosError) => {
if (!showErrorMessage) return;
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
});
const resp = userInfoResponseSchema.parse(raw);
if (resp.type === 'error') {
if (showErrorMessage) {
ElMessage.error(errorMessage('获取用户信息失败', resp.code, resp.msg));
async function updateSelfUserInfo(showErrorMessage: boolean): Promise<boolean> {
isInitializing.value = true;
try {
const raw = await axiosInstance
.get<RawResp>('/api/user/info')
.then((r) => r.data)
.catch((e: AxiosError) => {
if (!showErrorMessage) return;
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
});
const resp = userInfoResponseSchema.parse(raw);
if (resp.type === 'error') {
if (showErrorMessage) {
ElMessage.error(errorMessage('获取用户信息失败', resp.code, resp.msg));
}
return false;
}
return false;
userInfo.value = resp.data;
return true;
} finally {
isInitializing.value = false;
}
userInfo.value = resp.data;
return true;
}
function hasPermission(permissionId: number) {
return permissions.value.find((permission) => permission.id === permissionId);
}
function $reset() {
token.value = null;
userInfo.value = null;
userInfoStatus.value = 'uninitialized';
}
return {
token,
userInfo,
permissions,
userInfoStatus,
isInitializing,
updateSelfUserInfo,
hasPermission,
$reset
};
});

View File

@ -1,6 +1,6 @@
export function create2DArray<T>(height: number, width: number, cell: T | (() => T)): T[][] {
return Array.from({ length: height }, () =>
Array.from({ length: width }, () => (cell instanceof Function ? cell() : cell))
Array.from({ length: width }, () => (cell instanceof Function ? cell() : cell)),
);
}
@ -8,7 +8,7 @@ export function get2DArrayItem<T>(
grid: T[][],
first: number,
second: number,
colFirst: boolean
colFirst: boolean,
): T {
return colFirst ? grid[first][second] : grid[second][first];
}

View File

@ -1,7 +1,7 @@
export function errorMessage(
operation: string,
code: string | number | undefined,
message: string
message: string,
) {
return `${operation}: ${code !== undefined ? `(${code})${message}` : message}`;
}

87
src/utils/double-queue.ts Normal file
View File

@ -0,0 +1,87 @@
interface Node<T> {
value: T;
prev?: Node<T>;
next?: Node<T>;
}
export class DoubleQueue<T> {
protected head?: Node<T>;
protected tail?: Node<T>;
protected _size = 0;
constructor(it?: Iterable<T>) {
if (it) {
for (const item of it) {
this.push(item);
}
}
}
get size() {
return this._size;
}
push(item: T) {
const node: Node<T> = { value: item };
if (this.size) {
(this.tail!.next = node).prev = this.tail;
this.tail = node;
} else {
this.head = this.tail = node;
}
this._size++;
}
pop(): T | undefined {
if (!this.size) return undefined;
const node = this.tail!;
if (this.size === 1) {
this.head = this.tail = undefined;
} else {
node.prev!.next = undefined;
}
this._size--;
if (this.size === 1) this.head = this.tail;
return node.value;
}
unshift(item: T) {
const node: Node<T> = { value: item };
if (this.size) {
(this.head!.prev = node).next = this.head;
this.head = node;
} else {
this.head = this.tail = node;
}
this._size++;
}
shift(): T | undefined {
if (!this.size) return undefined;
const node = this.head!;
if (this.size === 1) {
this.head = this.tail = undefined;
} else {
node.next!.prev = undefined;
}
this._size--;
if (this.size === 1) this.tail = this.head;
return node.value;
}
get(i: number): T | undefined {
let reversed = i < 0;
if (i < -this.size || i >= this.size) return undefined;
let node = reversed ? this.tail! : this.head!;
if (reversed) i = -i - 1;
for (let j = 1; j <= i; j++) {
node = reversed ? node.prev! : node.next!;
}
return node.value;
}
*iter() {
for (let node = this.head; node; node = node.next) {
yield node.value;
}
}
[Symbol.iterator]() {
return this.iter();
}
*reverseIter() {
for (let node = this.tail; node; node = node.prev) {
yield node.value;
}
}
}

View File

@ -13,7 +13,7 @@ export function useRefresh() {
key,
refresh() {
return ++key.value;
}
},
};
}

View File

@ -1,12 +1,14 @@
<template>
<div class="page-root">
<template v-if="reason !== undefined">
<div class="error-container">
<div v-if="reason.type === 'notFound'" class="error-reason">404</div>
<div v-else-if="reason.type === 'networkError'" class="error-reason">网络错误</div>
<div v-else-if="reason.type === 'noPermission'" class="error-reason">没有权限</div>
<div class="error-container">
<div v-if="reason.type === PageErrorType.NOT_FOUND" class="error-reason">404</div>
<div v-else-if="reason.type === PageErrorType.NETWORK_ERROR" class="error-reason">
网络错误
</div>
</template>
<div v-else-if="reason.type === PageErrorType.NO_PERMISSION" class="error-reason">
没有权限
</div>
</div>
</div>
</template>
@ -21,10 +23,16 @@
font-size: 50px;
}
</style>
<script lang="ts" setup>
import { usePageStore } from '@/stores/page';
import { toRef } from 'vue';
const pageStore = usePageStore();
const reason = toRef(pageStore, 'pageErrorReason');
<script lang="ts">
export enum PageErrorType {
NOT_FOUND,
NETWORK_ERROR,
NO_PERMISSION,
}
</script>
<script lang="ts" setup>
export type PageErrorReason = {
type: PageErrorType.NOT_FOUND | PageErrorType.NETWORK_ERROR | PageErrorType.NO_PERMISSION;
};
const reason = defineProps<PageErrorReason>();
</script>

View File

@ -59,7 +59,7 @@ const userStore = useUserStore();
const initialized = ref(false);
const userInfo = ref<UserInfo>();
const isSelf = computed(() =>
userStore.userInfo === null ? false : userStore.userInfo.id === userInfo.value?.id
userStore.userInfo === null ? false : userStore.userInfo.id === userInfo.value?.id,
);
const route = useRoute();
const id = route.params.id as string | undefined;
@ -72,7 +72,7 @@ watch(
() => userStore.userInfo,
(info) => {
userInfo.value = info ?? undefined;
}
},
);
onBeforeMount(async () => {
if (id == undefined) {
@ -81,14 +81,18 @@ onBeforeMount(async () => {
} else {
try {
const userInfoResponse = userInfoResponseSchema.parse(
(await axiosInstance.get(`/api/user/info/${id}`)).data
(await axiosInstance.get(`/api/user/info/${id}`)).data,
);
if (userInfoResponse.type === 'error') {
return;
}
userInfo.value = userInfoResponse.data;
if (!userInfo.value.avatar) { /* empty */ }
} catch (e) { /* empty */ } finally {
if (!userInfo.value.avatar) {
/* empty */
}
} catch (e) {
/* empty */
} finally {
initialized.value = true;
}
}