重构项目结构,移除无用组件,添加数学库依赖,更新路由配置以支持新页面
This commit is contained in:
parent
035448ca00
commit
12aee0ab67
24
ui/components.d.ts
vendored
24
ui/components.d.ts
vendored
@ -7,15 +7,29 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Admin: typeof import('./src/components/Admin.vue')['default']
|
||||
AdminPanel: typeof import('./src/components/admin/AdminPanel.vue')['default']
|
||||
Auth: typeof import('./src/components/Auth.vue')['default']
|
||||
DeleteAccountDialog: typeof import('./src/components/admin/DeleteAccountDialog.vue')['default']
|
||||
Dialog: typeof import('./src/components/Dialog.vue')['default']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
HelloWorld2: typeof import('./src/components/HelloWorld2.vue')['default']
|
||||
LoginForm: typeof import('./src/components/LoginForm.vue')['default']
|
||||
RegisterForm: typeof import('./src/components/RegisterForm.vue')['default']
|
||||
Homepage: typeof import('./src/components/Homepage.vue')['default']
|
||||
LoginForm: typeof import('./src/components/admin/LoginForm.vue')['default']
|
||||
ManagePanel: typeof import('./src/components/study/ManagePanel.vue')['default']
|
||||
Permission: typeof import('./src/components/admin/Permission.vue')['default']
|
||||
Problem: typeof import('./src/components/study/Problem.vue')['default']
|
||||
ProblemDialog: typeof import('./src/components/study/ProblemDialog.vue')['default']
|
||||
ProblemList: typeof import('./src/components/study/ProblemList.vue')['default']
|
||||
Problems: typeof import('./src/components/Problems.vue')['default']
|
||||
RecordList: typeof import('./src/components/RecordList.vue')['default']
|
||||
RegisterForm: typeof import('./src/components/users/RegisterForm.vue')['default']
|
||||
Repasswd2Dialog: typeof import('./src/components/users/Repasswd2Dialog.vue')['default']
|
||||
'Repasswd2Dialog ': typeof import('./src/components/admin/Repasswd2Dialog .vue')['default']
|
||||
RepasswdDialog: typeof import('./src/components/admin/RepasswdDialog.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SetList: typeof import('./src/components/study/SetList.vue')['default']
|
||||
Study: typeof import('./src/components/Study.vue')['default']
|
||||
Toy: typeof import('./src/components/Toy.vue')['default']
|
||||
UserPanel: typeof import('./src/components/UserPanel.vue')['default']
|
||||
UserPanel: typeof import('./src/components/users/UserPanel.vue')['default']
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Vuetify 3</title>
|
||||
<title>KQM Math</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
@ -17,6 +17,7 @@
|
||||
"core-js": "^3.37.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mathjs": "^14.0.1",
|
||||
"node-forge": "^1.3.1",
|
||||
"pinia": "^2.3.0",
|
||||
"roboto-fontface": "*",
|
||||
|
@ -1,44 +1,52 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-layout class="rounded rounded-md">
|
||||
<v-app-bar color="surface-variant" title="Math">
|
||||
<v-btn icon>
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
<v-app-bar color="primary" prominent>
|
||||
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>mdi-heart</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>KQM Math</v-toolbar-title>
|
||||
|
||||
<v-btn icon>
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<template v-if="$vuetify.display.mdAndUp">
|
||||
<v-btn icon="mdi-magnify" variant="text"></v-btn>
|
||||
|
||||
<v-btn icon="mdi-filter" variant="text"></v-btn>
|
||||
</template>
|
||||
|
||||
<v-btn icon="mdi-dots-vertical" variant="text"></v-btn>
|
||||
</v-app-bar>
|
||||
<v-navigation-drawer>
|
||||
<v-navigation-drawer v-model="drawer" expand-on-hover rail
|
||||
:location="$vuetify.display.mobile ? 'bottom' : 'left'">
|
||||
<v-list nav>
|
||||
<v-list-item to="/" prepend-icon="mdi-email" title="Inbox" value="inbox"></v-list-item>
|
||||
<v-list-item to="/auth" prepend-icon="mdi-account-supervisor-circle" title="登录" value="supervisors"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-clock-start" :title="token" value="clockin"></v-list-item>
|
||||
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
|
||||
<v-list-item to="/" prepend-icon="mdi-home" title="首页"></v-list-item>
|
||||
<v-list-item to="/auth" prepend-icon="mdi-account-supervisor-circle"
|
||||
:title="authStore.isAuthenticated ? '用户面板' : '登录'" value="supervisors"></v-list-item>
|
||||
<v-list-item v-if="authStore.isAuthenticated" to="/problems" prepend-icon="mdi-bookshelf"
|
||||
title="题库"></v-list-item>
|
||||
<v-list-item v-if="authStore.isAuthenticated" to="/sets" prepend-icon="mdi-book-multiple"
|
||||
title="题单"></v-list-item>
|
||||
<v-list-item v-if="authStore.isAuthenticated" to="/records" prepend-icon="mdi-list-box-outline"
|
||||
title="提交记录"></v-list-item>
|
||||
<v-list-item to="/admin" prepend-icon="mdi-security" title="后台管理"></v-list-item>
|
||||
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
<v-main class="d-flex align-center justify-center" style="min-height: 300px;">
|
||||
<router-view></router-view>
|
||||
</v-main>
|
||||
</v-layout>
|
||||
<!-- <v-main>
|
||||
<router-view />
|
||||
</v-main> -->
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useAuthStore } from './store/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const token = ref(authStore.token);
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
const drawer = ref(true);
|
||||
|
||||
</script>
|
||||
|
@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<v-sheet class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4"
|
||||
v-if="!authStore.isAuthenticated" width="640" height="670">
|
||||
<v-carousel v-model="loginMode" height="100%" delimiter-icon="mdi-square" color="blue-darken-2"
|
||||
hide-delimiter-background show-arrows>
|
||||
<v-carousel-item>
|
||||
<v-sheet class="v-center" height="100%">
|
||||
<v-window continuous v-model="loginMode" show-arrows="hover">
|
||||
<v-window-item>
|
||||
<v-sheet class="v-center d-flex align-center justify-center ma-2" height="100%">
|
||||
<LoginForm></LoginForm>
|
||||
</v-sheet>
|
||||
</v-carousel-item>
|
||||
<v-carousel-item>
|
||||
<v-sheet class="v-center" height="100%">
|
||||
</v-window-item>
|
||||
<v-window-item>
|
||||
<v-sheet class="v-center d-flex align-center justify-center ma-2" height="100%">
|
||||
<RegisterForm v-model="loginMode"></RegisterForm>
|
||||
</v-sheet>
|
||||
</v-carousel-item>
|
||||
</v-carousel></v-sheet>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-sheet>
|
||||
<UserPanel v-else></UserPanel>
|
||||
</template>
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
点左边登录。
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//
|
||||
</script>
|
@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive
|
||||
class="align-centerfill-height mx-auto"
|
||||
max-width="900"
|
||||
>
|
||||
<v-img
|
||||
class="mb-4"
|
||||
height="150"
|
||||
src="@/assets/logo.png"
|
||||
/>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">Vuetify 2</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4"></div>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
|
||||
prepend-icon="mdi-rocket-launch-outline"
|
||||
rounded="lg"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #image>
|
||||
<v-img position="top right" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
<div class="text-subtitle-1">
|
||||
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-overlay
|
||||
opacity=".12"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/"
|
||||
prepend-icon="mdi-text-box-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Learn about all things Vuetify in our documentation."
|
||||
target="_blank"
|
||||
title="Documentation"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides"
|
||||
prepend-icon="mdi-star-circle-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Explore available framework Features."
|
||||
target="_blank"
|
||||
title="Features"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://vuetifyjs.com/components/all"
|
||||
prepend-icon="mdi-widgets-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Discover components in the API Explorer."
|
||||
target="_blank"
|
||||
title="Components"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card
|
||||
append-icon="mdi-open-in-new"
|
||||
class="py-4"
|
||||
color="surface-variant"
|
||||
href="https://discord.vuetifyjs.com"
|
||||
prepend-icon="mdi-account-group-outline"
|
||||
rel="noopener noreferrer"
|
||||
rounded="lg"
|
||||
subtitle="Connect with Vuetify developers."
|
||||
target="_blank"
|
||||
title="Community"
|
||||
variant="text"
|
||||
>
|
||||
<v-overlay
|
||||
opacity=".06"
|
||||
scrim="primary"
|
||||
contained
|
||||
model-value
|
||||
persistent
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//
|
||||
</script>
|
71
ui/src/components/Homepage.vue
Normal file
71
ui/src/components/Homepage.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<v-container class="d-flex align-center justify-center fill-height">
|
||||
<!-- 欢迎信息卡片 -->
|
||||
<v-card class="pa-6" outlined elevation="10" style="border-radius: 16px; background-color: #ffffff;">
|
||||
<v-card-title class="headline text-center" style="color: #333;">欢迎使用 KQM Math</v-card-title>
|
||||
<v-card-text class="text-center" style="color: #555;">
|
||||
KQM Math 是一个高效的数学口算系统,帮助你快速解决数学表达式的计算和验证问题。通过简洁的界面,提供准确且快速的计算服务。
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-center">
|
||||
<v-btn color="deep-purple accent-4" @click="theDialog" large depressed rounded class="white--text">
|
||||
点击左上角 <v-icon icon="mdi-menu"></v-icon> 开始使用!
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<v-dialog v-model="dialogShow" width="auto">
|
||||
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||
<template v-slot:actions>
|
||||
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
|
||||
const dialogShow = ref(false);
|
||||
const dialogTitle = ref('');
|
||||
const dialogText = ref('');
|
||||
const dialogClose = ref(() => { });
|
||||
|
||||
const dialog = (title: string, text: string) => {
|
||||
dialogTitle.value = title;
|
||||
dialogText.value = text;
|
||||
dialogShow.value = true;
|
||||
return new Promise(res => {
|
||||
dialogClose.value = res as () => void;
|
||||
});
|
||||
};
|
||||
|
||||
const theDialog = () => {
|
||||
dialog('提示', '请点左上角。');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
border-radius: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.v-card-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
width: auto;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.v-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
80
ui/src/components/Problems.vue
Normal file
80
ui/src/components/Problems.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive class="align-centerfill-height mx-auto" max-width="900">
|
||||
<v-img class="mb-4" height="150" src="@/assets/logo.png" />
|
||||
|
||||
<div class="text-center">
|
||||
<div class="text-body-2 font-weight-light mb-n1">Welcome to</div>
|
||||
|
||||
<h1 class="text-h2 font-weight-bold">Vuetify 2</h1>
|
||||
</div>
|
||||
|
||||
<div class="py-4"></div>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card class="py-4" color="surface-variant"
|
||||
image="https://cdn.vuetifyjs.com/docs/images/one/create/feature.png"
|
||||
prepend-icon="mdi-rocket-launch-outline" rounded="lg" variant="outlined">
|
||||
<template #image>
|
||||
<v-img position="top right" />
|
||||
</template>
|
||||
|
||||
<template #title>
|
||||
<h2 class="text-h5 font-weight-bold">Get started</h2>
|
||||
</template>
|
||||
|
||||
<template #subtitle>
|
||||
<div class="text-subtitle-1">
|
||||
Replace this page by removing <v-kbd>{{ `
|
||||
<HelloWorld />` }}
|
||||
</v-kbd> in <v-kbd>pages/index.vue</v-kbd>.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-overlay opacity=".12" scrim="primary" contained model-value persistent />
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card append-icon="mdi-open-in-new" class="py-4" color="surface-variant" href="https://vuetifyjs.com/"
|
||||
prepend-icon="mdi-text-box-outline" rel="noopener noreferrer" rounded="lg"
|
||||
subtitle="Learn about all things Vuetify in our documentation." target="_blank" title="Documentation"
|
||||
variant="text">
|
||||
<v-overlay opacity=".06" scrim="primary" contained model-value persistent />
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card append-icon="mdi-open-in-new" class="py-4" color="surface-variant"
|
||||
href="https://vuetifyjs.com/introduction/why-vuetify/#feature-guides" prepend-icon="mdi-star-circle-outline"
|
||||
rel="noopener noreferrer" rounded="lg" subtitle="Explore available framework Features." target="_blank"
|
||||
title="Features" variant="text">
|
||||
<v-overlay opacity=".06" scrim="primary" contained model-value persistent />
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card append-icon="mdi-open-in-new" class="py-4" color="surface-variant"
|
||||
href="https://vuetifyjs.com/components/all" prepend-icon="mdi-widgets-outline" rel="noopener noreferrer"
|
||||
rounded="lg" subtitle="Discover components in the API Explorer." target="_blank" title="Components"
|
||||
variant="text">
|
||||
<v-overlay opacity=".06" scrim="primary" contained model-value persistent />
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6">
|
||||
<v-card append-icon="mdi-open-in-new" class="py-4" color="surface-variant"
|
||||
href="https://discord.vuetifyjs.com" prepend-icon="mdi-account-group-outline" rel="noopener noreferrer"
|
||||
rounded="lg" subtitle="Connect with Vuetify developers." target="_blank" title="Community" variant="text">
|
||||
<v-overlay opacity=".06" scrim="primary" contained model-value persistent />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-responsive>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//
|
||||
</script>
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<v-data-table :loading="loading" :headers="(headers as any)" :items="records"
|
||||
<v-data-table :loading="loading" :headers="(headers as any)" :items="records" v-model:search="search"
|
||||
:filter-keys="['', 'user_id', 'problem'][filterMode]" :custom-filter="filter"
|
||||
:sort-by="[{ key: 'id', order: 'desc' }]" multi-sort items-per-page-text="每页" no-data-text="暂时没有提交记录">
|
||||
<template v-slot:loading>
|
||||
<v-skeleton-loader></v-skeleton-loader>
|
||||
@ -7,6 +8,16 @@
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>所有提交记录</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field v-model="search" density="compact" label="过滤" prepend-inner-icon="mdi-magnify"
|
||||
variant="solo-filled" flat hide-details single-line :append-inner-icon="filterIcon"
|
||||
@click:append-inner="++filterMode, filterMode %= 3">
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
当前为{{ ['不', '按用户 ID ', '按题号'][filterMode] }}过滤,点击右侧
|
||||
<v-icon :icon="filterIcon"></v-icon>
|
||||
切换模式。
|
||||
</v-tooltip>
|
||||
</v-text-field>
|
||||
<v-btn @click="refresh" class="mb-2" color="primary" dark>
|
||||
刷新
|
||||
</v-btn>
|
||||
@ -41,6 +52,25 @@ import axios, { Axios, AxiosError } from 'axios';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
||||
|
||||
const search = ref('');
|
||||
const filterMode = ref(0);
|
||||
const filterIcon = computed(() => {
|
||||
if (filterMode.value==1) {
|
||||
return 'mdi-account';
|
||||
} else if (filterMode.value==2){
|
||||
return 'mdi-brain';
|
||||
} else {
|
||||
return 'mdi-cancel';
|
||||
}
|
||||
});
|
||||
const filter = (value: string, query: string, item?: any) => {
|
||||
if (filterMode.value != 0) {
|
||||
return item.columns[['', 'user_id', 'problem'][filterMode.value]] == query;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
@ -178,7 +208,7 @@ const records = ref<Record[]>([]);
|
||||
const decorate = <T>(ex: AxiosError) => {
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as T;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message } as T;
|
||||
}
|
||||
}
|
||||
@ -248,14 +278,6 @@ const initialize = async () => {
|
||||
records.value.push(record);
|
||||
}
|
||||
}
|
||||
records.value.push({
|
||||
id: 114514,
|
||||
user_id: '114514',
|
||||
problem: 1919810,
|
||||
status: 'uke',
|
||||
answer: 'NaN',
|
||||
timestamp: 114514
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(initialize);
|
||||
|
74
ui/src/components/Study.vue
Normal file
74
ui/src/components/Study.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<v-sheet v-if="userPermission == '2'" class="d-flex align-center justify-center flex-wrap mx-auto px-4">
|
||||
<div style="margin: 5px;" v-for="(id, idx) in ids">
|
||||
<Problem v-model="ans[idx]" :id="id" :err="err[idx]" :submit="() => err[idx] = '' + Math.random()"></Problem>
|
||||
</div>
|
||||
</v-sheet>
|
||||
<ManagePanel v-else-if="userPermission == '1'"></ManagePanel>
|
||||
<div v-else>未知的权限,请联系管理员重置权限状态。</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import Problem from './study/Problem.vue';
|
||||
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import ManagePanel from './study/ManagePanel.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
const ids = ref([20,21,22]);
|
||||
const ans = ref<number[]>([]);
|
||||
const err = ref<string[]>([]);
|
||||
|
||||
interface KqmJwt extends JwtPayload {
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
const userId = computed(() => {
|
||||
if (storedToken.value != '') {
|
||||
let data: KqmJwt = jwtDecode(storedToken.value);
|
||||
return data.user_id;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
type UserPermissionResponse = { success?: string, permission?: string, error?: string };
|
||||
|
||||
const queryPermission = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("user_id", userId.value);
|
||||
let res = await axios.post('/api/auth/permission', formData);
|
||||
return res.data as UserPermissionResponse;
|
||||
} catch (e) {
|
||||
let ex = e as AxiosError;
|
||||
return ex.response?.data as UserPermissionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
const userPermission = ref('');
|
||||
|
||||
const updateUserPermission = async () => {
|
||||
let res = await queryPermission();
|
||||
if (res?.success) {
|
||||
userPermission.value = res.permission as string;
|
||||
} else {
|
||||
userPermission.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
watch(userId, updateUserPermission, { immediate: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
@ -1,80 +0,0 @@
|
||||
<template>
|
||||
|
||||
<v-row align="center" justify="center" dense>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card append-icon="mdi-check" class="mx-auto" prepend-icon="mdi-account" subtitle="占位符。占位符。" title="占位符">
|
||||
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="mx-auto" subtitle="占位符。占位符。" title="占位符">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary" icon="mdi-account"></v-icon>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-icon color="success" icon="mdi-check"></v-icon>
|
||||
</template>
|
||||
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card append-avatar="https://cdn.vuetifyjs.com/images/john.jpg" class="mx-auto"
|
||||
prepend-avatar="https://cdn.vuetifyjs.com/images/logos/v-alt.svg" subtitle="占位符。占位符。" title="占位符">
|
||||
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card class="mx-auto" subtitle="占位符。占位符。" title="占位符">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar color="blue-darken-2">
|
||||
<v-icon icon="mdi-alarm"></v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-avatar size="24">
|
||||
<v-img alt="John" src="https://file.aiquickdraw.com/inpaint/1735136797_9819bf1a-7f1d-4a32-be6a-f5f7f3977b71.png"></v-img>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-card-text>占位符。占位符。占位符。占位符。</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const labels = ref([
|
||||
'12am',
|
||||
'3am',
|
||||
'6am',
|
||||
'9am',
|
||||
'12pm',
|
||||
'3pm',
|
||||
'6pm',
|
||||
'9pm',
|
||||
])
|
||||
|
||||
const value = ref([
|
||||
200,
|
||||
675,
|
||||
410,
|
||||
390,
|
||||
310,
|
||||
460,
|
||||
250,
|
||||
240,
|
||||
]);
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-sheet--offset {
|
||||
top: -24px;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-card subtitle="占位符占位符占位符" title="管理员面板" max-width="80%">
|
||||
<v-card title="管理员面板" max-width="80%">
|
||||
<v-card-item>
|
||||
<v-chip class="ma-2" color="pink" label>
|
||||
<v-icon icon="mdi-human" start></v-icon>
|
||||
|
@ -112,7 +112,7 @@ const requestDeleteAccount = async (userId: string) => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as DeleteAccountResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ const login = async () => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as LoginResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ const requestPermission = async () => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as PermissionResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ const requestRepasswd = async () => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as RepasswdResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ const requestRepasswd = async () => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as RepasswdResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
49
ui/src/components/study/ManagePanel.vue
Normal file
49
ui/src/components/study/ManagePanel.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<v-window v-model="onboarding" show-arrows="hover">
|
||||
<v-window-item>
|
||||
<v-card class="d-flex align-center justify-center ma-2" elevation="2">
|
||||
<!-- <ProblemDialog></ProblemDialog> -->
|
||||
<ProblemList></ProblemList>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
<v-dialog v-model="dialogShow" width="auto">
|
||||
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||
<template v-slot:actions>
|
||||
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false"></v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import ProblemList from './ProblemList.vue';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
const dialogShow = ref(false);
|
||||
const dialogTitle = ref('');
|
||||
const dialogText = ref('');
|
||||
|
||||
const dialog = (title: string, text: string) => {
|
||||
dialogTitle.value = title;
|
||||
dialogText.value = text;
|
||||
dialogShow.value = true;
|
||||
};
|
||||
|
||||
const onboarding = ref(0);
|
||||
const length = ref(3);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
142
ui/src/components/study/Problem.vue
Normal file
142
ui/src/components/study/Problem.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<v-card class="mx-auto" max-width="344">
|
||||
<v-card-text>
|
||||
<div>{{ title }}</div>
|
||||
|
||||
<p class="text-h4 font-weight-black">{{ main }}</p>
|
||||
|
||||
<p>{{ info }}</p>
|
||||
|
||||
<div class="text-medium-emphasis">
|
||||
{{ specification }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="ans" prepend-inner-icon="mdi-numeric" label="你的答案" :rules="notEmptyRules"></v-text-field>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="deep-purple-accent-4" text="提交" variant="text" @click="submit"></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-dialog v-model="dialogShow" width="auto">
|
||||
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||
<template v-slot:actions>
|
||||
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false"></v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
const dialogShow = ref(false);
|
||||
const dialogTitle = ref('');
|
||||
const dialogText = ref('');
|
||||
|
||||
const dialog = (title: string, text: string) => {
|
||||
dialogTitle.value = title;
|
||||
dialogText.value = text;
|
||||
dialogShow.value = true;
|
||||
};
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
const { id } = defineProps<{ id: number }>();
|
||||
|
||||
const title = ref('');
|
||||
const main = ref('');
|
||||
const info = ref('');
|
||||
const specification = ref('');
|
||||
|
||||
const ans = defineModel({ required: true });
|
||||
|
||||
type ProblemResponse = { success?: string, problem?: string, check_error?: number, error?: string };
|
||||
|
||||
const requestQueryProblem = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("action", "query");
|
||||
formData.append("token", storedToken.value);
|
||||
formData.append("id", `${id}`);
|
||||
let res = await axios.post('/api/study/problems', formData);
|
||||
return res.data as ProblemResponse;
|
||||
} catch (e) {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as ProblemResponse;
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CheckProblemResponse = { success?: string, result?: boolean, error?: string };
|
||||
|
||||
const requestCheckProblem = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("action", "check");
|
||||
formData.append("token", storedToken.value);
|
||||
formData.append("id", `${id}`);
|
||||
formData.append("answer", `${ans.value}`);
|
||||
let res = await axios.post('/api/study/problems', formData);
|
||||
return res.data as CheckProblemResponse;
|
||||
} catch (e) {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as CheckProblemResponse;
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (ans.value == '') {
|
||||
return;
|
||||
}
|
||||
let res = await requestCheckProblem();
|
||||
if (res?.error) {
|
||||
dialog('发生异常', res.error);
|
||||
} else {
|
||||
dialog('提交成功', res.result ? '并且答案正确。' : '但是答案错误。')
|
||||
}
|
||||
};
|
||||
|
||||
const notEmptyRules: any = [(value: string) => {
|
||||
if (value != '') {
|
||||
return true;
|
||||
} else {
|
||||
return '不能为空';
|
||||
}
|
||||
}];
|
||||
|
||||
onMounted(async () => {
|
||||
let res = await requestQueryProblem();
|
||||
if (res?.error) {
|
||||
title.value = `题目 ${id}`;
|
||||
main.value = '未知题目';
|
||||
info.value = `错误信息:'${res.error}'`;
|
||||
specification.value = `未知题目 (ID = ${id}),题号有误,请联系老师或管理员处理。`;
|
||||
} else {
|
||||
title.value = `题目 ${id}`;
|
||||
main.value = `${res.problem} = ?`;
|
||||
info.value = res.check_error == 0 ? '精确匹配' : `误差不超过 ${res.check_error}`;
|
||||
specification.value = '对于精确匹配的题目,你的答案必须和预设的标准答案完全一致;对于按误差匹配的题目,只要你的答案与标准答案的差值的绝对值不超过误差即可。';
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.v-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
@ -86,30 +86,24 @@
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.problem }}</td>
|
||||
<td>{{ item.answer }}</td>
|
||||
<td v-if="userPermission == '1'">{{ item.answer }}</td>
|
||||
<td>{{ decorateCheckingError(item.check_error) }}</td>
|
||||
<td class="text-center">
|
||||
<v-icon class="me-2" size="small" @click="editItem(item)">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
<v-icon size="small" @click="deleteItem(item)">
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
<div v-if="userPermission == '1'">
|
||||
<v-icon class="me-2" size="small" @click="editItem(item)">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
<v-icon size="small" @click="deleteItem(item)">
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</div>
|
||||
<v-chip v-else class="chips" :color="colorsCache[item.id]"
|
||||
@click="problemId = item.id, dialogProblemShow = true">
|
||||
答题
|
||||
</v-chip>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- <template v-slot:item.actions="{ item }">
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<v-icon class="me-2" size="small" @click="editItem(item)">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
<v-icon size="small" @click="deleteItem(item)">
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</td>
|
||||
</tr>
|
||||
</template> -->
|
||||
</v-data-table>
|
||||
<v-dialog v-model="dialogShow" width="auto">
|
||||
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||
@ -118,6 +112,14 @@
|
||||
</template>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="dialogProblemShow" width="auto">
|
||||
<v-card max-width="400">
|
||||
<Problem v-model="answer" :id="problemId"></Problem>
|
||||
<!-- <template v-slot:actions>
|
||||
<v-btn class="ms-auto" text="Ok" @click="dialogProblemShow = false"></v-btn>
|
||||
</template> -->
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
@ -125,6 +127,53 @@ import axios, { Axios, AxiosError } from 'axios';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import * as mathjs from 'mathjs';
|
||||
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
||||
import Problem from './Problem.vue';
|
||||
|
||||
const dialogProblemShow = ref(false);
|
||||
const answer = ref('');
|
||||
const problemId = ref(0);
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
interface KqmJwt extends JwtPayload {
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
const userId = computed(() => {
|
||||
if (storedToken.value != '') {
|
||||
let data: KqmJwt = jwtDecode(storedToken.value);
|
||||
return data.user_id;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
type UserPermissionResponse = { success?: string, permission?: string, error?: string };
|
||||
|
||||
const queryPermission = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("user_id", userId.value);
|
||||
let res = await axios.post('/api/auth/permission', formData);
|
||||
return res.data as UserPermissionResponse;
|
||||
} catch (e) {
|
||||
let ex = e as AxiosError;
|
||||
return ex.response?.data as UserPermissionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
const userPermission = ref('');
|
||||
|
||||
const updateUserPermission = async () => {
|
||||
let res = await queryPermission();
|
||||
if (res?.success) {
|
||||
userPermission.value = res.permission as string;
|
||||
} else {
|
||||
userPermission.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
watch(userId, updateUserPermission, { immediate: true });
|
||||
|
||||
const loading = ref(false);
|
||||
const refresh = async () => {
|
||||
@ -145,36 +194,72 @@ const dialog = (title: string, text: string) => {
|
||||
|
||||
const dialog0 = ref(false);
|
||||
const dialogDelete = ref(false);
|
||||
const headers = ref([
|
||||
{
|
||||
title: '题号',
|
||||
align: 'center',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '题面',
|
||||
align: 'center',
|
||||
key: 'problem'
|
||||
},
|
||||
{
|
||||
title: '答案',
|
||||
key: 'answer',
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
title: '误差',
|
||||
key: 'check_error',
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
]);
|
||||
// const headers = ref([
|
||||
// {
|
||||
// title: '题号',
|
||||
// align: 'center',
|
||||
// key: 'id'
|
||||
// },
|
||||
// {
|
||||
// title: '题面',
|
||||
// align: 'center',
|
||||
// key: 'problem'
|
||||
// },
|
||||
// {
|
||||
// title: '答案',
|
||||
// key: 'answer',
|
||||
// align: 'center',
|
||||
// sortable: false
|
||||
// },
|
||||
// {
|
||||
// title: '误差',
|
||||
// key: 'check_error',
|
||||
// align: 'center',
|
||||
// sortable: false
|
||||
// },
|
||||
// {
|
||||
// title: '操作',
|
||||
// key: 'actions',
|
||||
// align: 'center',
|
||||
// sortable: false
|
||||
// },
|
||||
// ]);
|
||||
const headers = computed(() => {
|
||||
let result = [
|
||||
{
|
||||
title: '题号',
|
||||
align: 'center',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '题面',
|
||||
align: 'center',
|
||||
key: 'problem'
|
||||
},
|
||||
{
|
||||
title: '答案',
|
||||
key: 'answer',
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
title: '误差',
|
||||
key: 'check_error',
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
];
|
||||
if (userPermission.value == '2') {
|
||||
result.splice(2, 1);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
const problems = ref<{ id: number, problem: string, answer: string, check_error: number }[]>([]);
|
||||
const editedIndex = ref(-1);
|
||||
@ -276,52 +361,10 @@ watch(dialogDelete, (val) => {
|
||||
val || closeDelete();
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
interface KqmJwt extends JwtPayload {
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
const userId = computed(() => {
|
||||
if (storedToken.value != '') {
|
||||
let data: KqmJwt = jwtDecode(storedToken.value);
|
||||
return data.user_id;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
type UserPermissionResponse = { success?: string, permission?: string, error?: string };
|
||||
|
||||
const queryPermission = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("user_id", userId.value);
|
||||
let res = await axios.post('/api/auth/permission', formData);
|
||||
return res.data as UserPermissionResponse;
|
||||
} catch (e) {
|
||||
let ex = e as AxiosError;
|
||||
return ex.response?.data as UserPermissionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
const userPermission = ref('');
|
||||
|
||||
const updateUserPermission = async () => {
|
||||
let res = await queryPermission();
|
||||
if (res?.success) {
|
||||
userPermission.value = res.permission as string;
|
||||
} else {
|
||||
userPermission.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
watch(userId, updateUserPermission, { immediate: true });
|
||||
|
||||
const decorate = <T>(ex: AxiosError) => {
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as T;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message } as T;
|
||||
}
|
||||
}
|
||||
@ -390,15 +433,56 @@ const requestDeleteProblem = async (id: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
type HaveAcResponse = {
|
||||
success?: string,
|
||||
error?: string,
|
||||
result?: boolean
|
||||
};
|
||||
|
||||
const requestHaveAc = async (user_id: string, problem_id: number) => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("action", "have");
|
||||
formData.append("user_id", `${user_id}`);
|
||||
formData.append("problem_id", `${problem_id}`);
|
||||
let res = await axios.post('/api/study/records', formData);
|
||||
return res.data as HaveAcResponse;
|
||||
} catch (e) {
|
||||
return decorate<HaveAcResponse>(e as AxiosError);
|
||||
}
|
||||
}
|
||||
|
||||
const calcColor = async (problem_id: number) => {
|
||||
let res = await requestHaveAc(userId.value, problem_id);
|
||||
if (res?.success && res.result) {
|
||||
return 'green';
|
||||
}
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
const colorsCache = ref<any>({});
|
||||
|
||||
const model = defineModel();
|
||||
|
||||
const getAllProblem = async () => {
|
||||
if (model.value == undefined) {
|
||||
return await requestAllProblem();
|
||||
} else {
|
||||
return { success: 'success', result: model.value } as AllProblemResponse;
|
||||
}
|
||||
}
|
||||
|
||||
const initialize = async () => {
|
||||
let res = await requestAllProblem();
|
||||
let res = await getAllProblem();
|
||||
if (res?.error) {
|
||||
dialog('发生异常', res.error);
|
||||
return;
|
||||
}
|
||||
problems.value = [];
|
||||
let ids = res.result as number[];
|
||||
for (let id of ids) {
|
||||
colorsCache.value[id] = 'blue';
|
||||
}
|
||||
for (let i in ids) {
|
||||
let id = ids[i];
|
||||
let resp = await requestQueryProblem(id);
|
||||
@ -410,6 +494,9 @@ const initialize = async () => {
|
||||
}
|
||||
problems.value.push(pro);
|
||||
}
|
||||
for (let id of ids) {
|
||||
colorsCache.value[id] = await calcColor(id);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(initialize);
|
||||
|
409
ui/src/components/study/SetList.vue
Normal file
409
ui/src/components/study/SetList.vue
Normal file
@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<v-data-table :loading="loading" :headers="(headers as any)" :items="sets" :sort-by="[{ key: 'id', order: 'asc' }]"
|
||||
multi-sort items-per-page-text="每页" no-data-text="暂时没有题单">
|
||||
<template v-slot:loading>
|
||||
<v-skeleton-loader></v-skeleton-loader>
|
||||
</template>
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>所有题单</v-toolbar-title>
|
||||
<v-btn @click="refresh" class="mb-2" color="primary" dark>
|
||||
刷新
|
||||
</v-btn>
|
||||
<v-dialog v-model="dialog0" max-width="500px">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="mb-2" color="primary" dark v-bind="props">
|
||||
新建题单
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-form fast-fail @submit.prevent="save">
|
||||
<v-card class="mx-auto pa-12 pb-8" width="660" elevation="8" rounded="lg">
|
||||
<v-card-title>
|
||||
<span class="text-h5">{{ formTitle }}</span>
|
||||
</v-card-title>
|
||||
<v-container>
|
||||
<v-text-field v-model="editedItem.name" prepend-inner-icon="mdi-alphabetical" label="名称"
|
||||
:rules="notEmptyRules">
|
||||
</v-text-field>
|
||||
<v-combobox prepend-inner-icon="mdi-numeric" v-model="editedItem.problems" clearable chips multiple
|
||||
label="题号" :delimiters="[',', ' ', ';']" :rules="isProblemIds">
|
||||
</v-combobox>
|
||||
</v-container>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue-darken-1" variant="text" @click="close1">
|
||||
取消
|
||||
</v-btn>
|
||||
<v-btn color="blue-darken-1" variant="text" type="submit">
|
||||
保存
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="dialogDelete" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">再次确认</v-card-title>
|
||||
<v-card-text>确定要删除这个题单吗?这是不可逆操作。</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="blue-darken-1" variant="text" @click="closeDelete">放弃操作</v-btn>
|
||||
<v-btn color="blue-darken-1" variant="text" @click="deleteItemConfirm">确定删除</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item="{ item }">
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.problems.length }}</td>
|
||||
<td class="text-center">
|
||||
<div v-if="userPermission == '1'">
|
||||
<v-icon class="me-2" size="small" @click="editItem(item)">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
<v-icon size="small" @click="deleteItem(item)">
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</div>
|
||||
<v-chip v-else class="chips" color="blue" @click="theProblems = item.problems, dialogProblemsShow = true">
|
||||
答题
|
||||
</v-chip>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-dialog v-model="dialogShow" width="auto">
|
||||
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||
<template v-slot:actions>
|
||||
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false"></v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="dialogProblemsShow" width="auto">
|
||||
<ProblemList v-model="theProblems"></ProblemList>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
import axios, { Axios, AxiosError } from 'axios';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
||||
import ProblemList from './ProblemList.vue';
|
||||
|
||||
const theProblems = ref<number[]>([]);
|
||||
|
||||
const loading = ref(false);
|
||||
const refresh = async () => {
|
||||
loading.value = true;
|
||||
await initialize();
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const dialogProblemsShow = ref(false);
|
||||
|
||||
const dialogShow = ref(false);
|
||||
const dialogTitle = ref('');
|
||||
const dialogText = ref('');
|
||||
|
||||
const dialog = (title: string, text: string) => {
|
||||
dialogTitle.value = title;
|
||||
dialogText.value = text;
|
||||
dialogShow.value = true;
|
||||
};
|
||||
|
||||
const dialog0 = ref(false);
|
||||
const dialogDelete = ref(false);
|
||||
const headers = ref([
|
||||
{
|
||||
title: '编号',
|
||||
align: 'center',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
align: 'center',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '题数',
|
||||
key: 'problems',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
sortable: false
|
||||
},
|
||||
]);
|
||||
|
||||
type ProblemSet = { id: number, name: string, problems: string[], data: any };
|
||||
|
||||
const sets = ref<ProblemSet[]>([]);
|
||||
const editedIndex = ref(-1);
|
||||
const editedItem = ref<ProblemSet>({
|
||||
id: 0,
|
||||
name: '',
|
||||
problems: [],
|
||||
data: {}
|
||||
});
|
||||
const defaultItem = ref<ProblemSet>({
|
||||
id: 0,
|
||||
name: '',
|
||||
problems: [],
|
||||
data: {}
|
||||
});
|
||||
|
||||
const notEmptyRules = [(value: string) => {
|
||||
if (value == '') return '不能为空';
|
||||
return true;
|
||||
}];
|
||||
|
||||
const isProblemIds = [() => {
|
||||
for (let id of editedItem.value.problems) {
|
||||
let num = parseInt(id, 10);
|
||||
let flag = num > 0 && num.toString() == id;
|
||||
if (!flag) {
|
||||
return '必须全部是正整数。';
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}];
|
||||
|
||||
const formTitle = computed(() => {
|
||||
return editedIndex.value === -1 ? '新建题单' : `编辑题单 ${editedItem.value.id}`;
|
||||
});
|
||||
|
||||
watch(dialog0, (val) => {
|
||||
val || close1();
|
||||
});
|
||||
watch(dialogDelete, (val) => {
|
||||
val || closeDelete();
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const storedToken = ref(authStore.token);
|
||||
|
||||
const decorate = <T>(ex: AxiosError) => {
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as T;
|
||||
} else {
|
||||
return { error: ex.message } as T;
|
||||
}
|
||||
}
|
||||
|
||||
// type AllProblemResponse = { success?: string, result?: number[], error?: string };
|
||||
|
||||
// const requestAllProblem = async () => {
|
||||
// try {
|
||||
// const formData = new FormData;
|
||||
// formData.append("action", "all");
|
||||
// formData.append("token", storedToken.value);
|
||||
// let res = await axios.post('/api/study/problems', formData);
|
||||
// return res.data as AllProblemResponse;
|
||||
// } catch (e) {
|
||||
// return decorate<AllProblemResponse>(e as AxiosError);
|
||||
// }
|
||||
// };
|
||||
|
||||
type AllSetsResponse = { success?: string, sets?: number[], error?: string };
|
||||
|
||||
const requestAllSets = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("action", "all");
|
||||
formData.append("token", storedToken.value);
|
||||
let res = await axios.post('/api/study/sets', formData);
|
||||
return res.data as AllSetsResponse;
|
||||
} catch (e) {
|
||||
return decorate<AllSetsResponse>(e as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
type SetResponse = {
|
||||
success?: string,
|
||||
set?: {
|
||||
name: string,
|
||||
problems: number[],
|
||||
data: any
|
||||
},
|
||||
error?: string
|
||||
};
|
||||
|
||||
const requestQuerySet = async (id: number) => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("action", "query");
|
||||
formData.append("token", storedToken.value);
|
||||
formData.append("id", `${id}`);
|
||||
let res = await axios.post('/api/study/sets', formData);
|
||||
return res.data as SetResponse;
|
||||
} catch (e) {
|
||||
return decorate<SetResponse>(e as AxiosError);
|
||||
}
|
||||
}
|
||||
|
||||
type AddSetResponse = { success?: string, id?: number, error?: string };
|
||||
|
||||
const requestAddSet = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("action", editedItem.value.id == 0 ? "add" : 'modify');
|
||||
formData.append("token", storedToken.value);
|
||||
if (editedItem.value.id != 0) {
|
||||
formData.append("id", `${editedItem.value.id}`);
|
||||
}
|
||||
formData.append("name", `${editedItem.value.name}`);
|
||||
formData.append("problems", `[${editedItem.value.problems}]`);
|
||||
formData.append("data", `{}`);
|
||||
let res = await axios.post('/api/study/sets', formData);
|
||||
return res.data as AddSetResponse;
|
||||
} catch (e) {
|
||||
return decorate<AddSetResponse>(e as AxiosError);
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteSetResponse = { success?: string, error?: string };
|
||||
|
||||
const requestDeleteSet = async (id: number) => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("action", 'delete');
|
||||
formData.append("token", storedToken.value);
|
||||
formData.append("id", `${id}`);
|
||||
let res = await axios.post('/api/study/sets', formData);
|
||||
return res.data as DeleteSetResponse;
|
||||
} catch (e) {
|
||||
return decorate<DeleteSetResponse>(e as AxiosError);
|
||||
}
|
||||
}
|
||||
|
||||
const initialize = async () => {
|
||||
let res = await requestAllSets();
|
||||
if (res?.error) {
|
||||
dialog('发生异常', res.error);
|
||||
return;
|
||||
}
|
||||
sets.value = [];
|
||||
let ids = res.sets as number[];
|
||||
for (let i in ids) {
|
||||
let id = ids[i];
|
||||
let resp = await requestQuerySet(id);
|
||||
if (resp?.success) {
|
||||
let pro = {
|
||||
id,
|
||||
name: resp.set?.name as string,
|
||||
problems: resp.set?.problems as any,
|
||||
data: {}
|
||||
};
|
||||
sets.value.push(pro);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(initialize);
|
||||
|
||||
const editItem = (item: any) => {
|
||||
editedIndex.value = sets.value.indexOf(item);
|
||||
editedItem.value = Object.assign({}, item);
|
||||
dialog0.value = true;
|
||||
};
|
||||
|
||||
const deleteItem = (item: any) => {
|
||||
editedIndex.value = sets.value.indexOf(item);
|
||||
editedItem.value = Object.assign({}, item);
|
||||
dialogDelete.value = true;
|
||||
};
|
||||
|
||||
const deleteItemConfirm = async () => {
|
||||
let res = await requestDeleteSet(editedItem.value.id);
|
||||
if (res?.error) {
|
||||
dialog('发生异常', res.error);
|
||||
return;
|
||||
}
|
||||
sets.value.splice(editedIndex.value, 1);
|
||||
closeDelete();
|
||||
};
|
||||
|
||||
const close1 = () => {
|
||||
dialog0.value = false;
|
||||
nextTick(() => {
|
||||
editedItem.value = Object.assign({}, defaultItem.value);
|
||||
editedIndex.value = -1;
|
||||
});
|
||||
};
|
||||
|
||||
const closeDelete = () => {
|
||||
dialogDelete.value = false;
|
||||
nextTick(() => {
|
||||
editedItem.value = Object.assign({}, defaultItem.value);
|
||||
editedIndex.value = -1;
|
||||
})
|
||||
};
|
||||
|
||||
const save = async (event: SubmitEvent) => {
|
||||
const results: any = await event;
|
||||
if (!results.valid) return;
|
||||
let res = await requestAddSet();
|
||||
if (res?.error) {
|
||||
dialog(`${editedIndex.value > -1 ? '修改' : '添加'}失败`, res.error);
|
||||
} else {
|
||||
if (editedIndex.value > -1) {
|
||||
Object.assign(sets.value[editedIndex.value], editedItem.value);
|
||||
} else {
|
||||
editedItem.value.id = res.id as number;
|
||||
sets.value.push(editedItem.value);
|
||||
}
|
||||
}
|
||||
close1();
|
||||
};
|
||||
|
||||
interface KqmJwt extends JwtPayload {
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
const userId = computed(() => {
|
||||
if (storedToken.value != '') {
|
||||
let data: KqmJwt = jwtDecode(storedToken.value);
|
||||
return data.user_id;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
type UserPermissionResponse = { success?: string, permission?: string, error?: string };
|
||||
|
||||
const queryPermission = async () => {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("user_id", userId.value);
|
||||
let res = await axios.post('/api/auth/permission', formData);
|
||||
return res.data as UserPermissionResponse;
|
||||
} catch (e) {
|
||||
let ex = e as AxiosError;
|
||||
return ex.response?.data as UserPermissionResponse;
|
||||
}
|
||||
}
|
||||
|
||||
const userPermission = ref('');
|
||||
|
||||
const updateUserPermission = async () => {
|
||||
let res = await queryPermission();
|
||||
if (res?.success) {
|
||||
userPermission.value = res.permission as string;
|
||||
} else {
|
||||
userPermission.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
watch(userId, updateUserPermission, { immediate: true });
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
td {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -80,7 +80,7 @@ const requestDeleteAccount = async () => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as DeleteAccountResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<v-card class="mx-auto pa-12 pb-8" width="448" elevation="8" rounded="lg">
|
||||
<v-form fast-fail @submit.prevent="submit">
|
||||
<!-- <v-img class="mx-auto my-6" max-width="228"
|
||||
src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img> -->
|
||||
|
||||
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
|
||||
label="账号"></v-text-field>
|
||||
|
||||
@ -80,7 +77,7 @@ const login = async (userId: string, password: string) => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as LoginResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-form fast-fail @submit.prevent="submit">
|
||||
<!-- <v-img class="mx-auto my-6" max-width="228"
|
||||
src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img> -->
|
||||
|
||||
<v-card class="mx-auto pa-12 pb-8" elevation="8" max-width="448" rounded="lg">
|
||||
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
|
||||
label="账号"></v-text-field>
|
||||
@ -24,7 +21,7 @@
|
||||
|
||||
<v-card class="mb-12" color="surface-variant" variant="tonal">
|
||||
<v-card-text class="text-medium-emphasis text-caption">
|
||||
这只是个占位符文本,不知道为什么没有这段话整个布局就会乱成一坨,我还在研究怎么让它表现得正常一点,这个组件库的文档太抽象了,看不懂,唉。
|
||||
新注册的账号默认是学生身份,老师请联系管理员提权。
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@ -87,7 +84,7 @@ const register = async (userId: string, password: string) => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as RegisterResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ const requestRepasswd = async () => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as RepasswdResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ const requestRepasswd = async () => {
|
||||
let ex = e as AxiosError;
|
||||
if (ex.response?.data) {
|
||||
return ex.response?.data as RepasswdResponse;
|
||||
} {
|
||||
} else {
|
||||
return { error: ex.message };
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-card subtitle="占位符占位符占位符" title="用户信息面板" max-width="80%">
|
||||
<v-card title="用户信息面板" max-width="80%">
|
||||
<v-card-item>
|
||||
<v-chip class="ma-2" color="primary" label>
|
||||
<v-icon icon="mdi-account-circle-outline" start></v-icon>
|
||||
@ -21,11 +21,10 @@
|
||||
删除账号
|
||||
</v-chip>
|
||||
</v-card-item>
|
||||
<v-card-item>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel title="Token (放出来玩玩,之后会换个机制)" :text="token">
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<v-card-item v-if="userPermission == '1'">
|
||||
<v-chip class="chips" color="blue" @click="dialogRepasswd2Show = true">
|
||||
修改学生账号的密码
|
||||
</v-chip>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
<v-dialog v-model="dialogShow" width="auto">
|
||||
@ -36,6 +35,7 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<RepasswdDialog v-model="dialogRepasswdShow"></RepasswdDialog>
|
||||
<Repasswd2Dialog v-model="dialogRepasswd2Show"></Repasswd2Dialog>
|
||||
<DeleteAccountDialog v-model="dialogDeleteAccountShow"></DeleteAccountDialog>
|
||||
</template>
|
||||
|
||||
@ -46,6 +46,7 @@ import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import DeleteAccountDialog from './DeleteAccountDialog.vue';
|
||||
import RepasswdDialog from './RepasswdDialog.vue';
|
||||
import Repasswd2Dialog from './Repasswd2Dialog.vue';
|
||||
|
||||
const dialogShow = ref(false);
|
||||
const dialogTitle = ref('');
|
||||
@ -55,6 +56,7 @@ const dialogClose = ref(() => { });
|
||||
const dialogDeleteAccountShow = ref(false);
|
||||
|
||||
const dialogRepasswdShow = ref(false);
|
||||
const dialogRepasswd2Show = ref(false);
|
||||
|
||||
const dialog = (title: string, text: string) => {
|
||||
dialogTitle.value = title;
|
||||
|
@ -5,22 +5,50 @@
|
||||
*/
|
||||
|
||||
// Composables
|
||||
import Admin from '@/components/Admin.vue'
|
||||
import Auth from '@/components/Auth.vue'
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
import HelloWorld2 from '@/components/HelloWorld2.vue'
|
||||
import Login from '@/components/RegisterForm.vue'
|
||||
import Study from '@/components/Study.vue'
|
||||
import Homepage from '@/components/Homepage.vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import RecordList from '@/components/RecordList.vue'
|
||||
import ProblemList from '@/components/study/ProblemList.vue'
|
||||
import SetList from '@/components/study/SetList.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HelloWorld
|
||||
component: Homepage
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
name: 'home2',
|
||||
name: 'auth',
|
||||
component: Auth
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'admin',
|
||||
component: Admin
|
||||
},
|
||||
{
|
||||
path: '/study',
|
||||
name: 'study',
|
||||
component: Study
|
||||
},
|
||||
{
|
||||
path: '/problems',
|
||||
name: 'problems',
|
||||
component: ProblemList
|
||||
},
|
||||
{
|
||||
path: '/sets',
|
||||
name: 'sets',
|
||||
component: SetList
|
||||
},
|
||||
{
|
||||
path: '/records',
|
||||
name: 'sub',
|
||||
component: RecordList
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -1,20 +1,37 @@
|
||||
import axios from "axios";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('token') || '',
|
||||
adminToken: sessionStorage.getItem('adminToken') || ''
|
||||
}),
|
||||
actions: {
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
localStorage.setItem('token', token);
|
||||
},
|
||||
clearToken() {
|
||||
async clearToken() {
|
||||
try {
|
||||
const formData = new FormData;
|
||||
formData.append("token", this.token);
|
||||
await axios.post('/api/auth/logout', formData);
|
||||
} catch (e) {
|
||||
}
|
||||
this.token = '';
|
||||
localStorage.removeItem('token');
|
||||
},
|
||||
setAdminToken(token: string) {
|
||||
this.adminToken = token;
|
||||
sessionStorage.setItem('adminToken', token);
|
||||
},
|
||||
clearAdminToken() {
|
||||
this.adminToken = '';
|
||||
sessionStorage.removeItem('adminToken');
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.token,
|
||||
isAdmin: (state) => !!state.adminToken,
|
||||
},
|
||||
});
|
||||
|
@ -50,6 +50,12 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8081',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
@ -4,7 +4,7 @@ add_rules("plugin.compile_commands.autoupdate")
|
||||
set_warnings("all")
|
||||
set_warnings("error")
|
||||
|
||||
add_requires("civetweb", "cjson", "leveldb", "jwt-cpp", "cryptopp")
|
||||
add_requires("civetweb", "cjson", "leveldb", "jwt-cpp", "cryptopp", "nlohmann_json")
|
||||
|
||||
local npm = "npm"
|
||||
if is_host("windows") then
|
||||
@ -28,7 +28,7 @@ target("hash")
|
||||
target("db")
|
||||
set_languages("c++23")
|
||||
set_kind("static")
|
||||
add_packages("leveldb")
|
||||
add_packages("leveldb", "nlohmann_json")
|
||||
add_deps("hash")
|
||||
add_includedirs("include/db", "include")
|
||||
add_files("src/db/**.cpp")
|
||||
|
Loading…
Reference in New Issue
Block a user