重构项目结构,移除无用组件,添加数学库依赖,更新路由配置以支持新页面
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 */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
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']
|
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']
|
Dialog: typeof import('./src/components/Dialog.vue')['default']
|
||||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
Homepage: typeof import('./src/components/Homepage.vue')['default']
|
||||||
HelloWorld2: typeof import('./src/components/HelloWorld2.vue')['default']
|
LoginForm: typeof import('./src/components/admin/LoginForm.vue')['default']
|
||||||
LoginForm: typeof import('./src/components/LoginForm.vue')['default']
|
ManagePanel: typeof import('./src/components/study/ManagePanel.vue')['default']
|
||||||
RegisterForm: typeof import('./src/components/RegisterForm.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']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
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']
|
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">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Welcome to Vuetify 3</title>
|
<title>KQM Math</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"core-js": "^3.37.1",
|
"core-js": "^3.37.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"mathjs": "^14.0.1",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"pinia": "^2.3.0",
|
"pinia": "^2.3.0",
|
||||||
"roboto-fontface": "*",
|
"roboto-fontface": "*",
|
||||||
|
@ -1,44 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<v-layout class="rounded rounded-md">
|
<v-layout class="rounded rounded-md">
|
||||||
<v-app-bar color="surface-variant" title="Math">
|
<v-app-bar color="primary" prominent>
|
||||||
<v-btn icon>
|
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
||||||
<v-icon>mdi-magnify</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn icon>
|
<v-toolbar-title>KQM Math</v-toolbar-title>
|
||||||
<v-icon>mdi-heart</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn icon>
|
<v-spacer></v-spacer>
|
||||||
<v-icon>mdi-dots-vertical</v-icon>
|
|
||||||
</v-btn>
|
<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-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 nav>
|
||||||
<v-list-item to="/" prepend-icon="mdi-email" title="Inbox" value="inbox"></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="登录" value="supervisors"></v-list-item>
|
<v-list-item to="/auth" prepend-icon="mdi-account-supervisor-circle"
|
||||||
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
|
:title="authStore.isAuthenticated ? '用户面板' : '登录'" value="supervisors"></v-list-item>
|
||||||
<v-list-item prepend-icon="mdi-clock-start" :title="token" value="clockin"></v-list-item>
|
<v-list-item v-if="authStore.isAuthenticated" to="/problems" prepend-icon="mdi-bookshelf"
|
||||||
<v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item>
|
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-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<v-main class="d-flex align-center justify-center" style="min-height: 300px;">
|
<v-main class="d-flex align-center justify-center" style="min-height: 300px;">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</v-main>
|
</v-main>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
<!-- <v-main>
|
|
||||||
<router-view />
|
|
||||||
</v-main> -->
|
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useAuthStore } from './store/auth';
|
import { useAuthStore } from './store/auth';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const token = ref(authStore.token);
|
const storedToken = ref(authStore.token);
|
||||||
|
|
||||||
|
const drawer = ref(true);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-sheet class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4"
|
<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-if="!authStore.isAuthenticated" width="640" height="670">
|
||||||
<v-carousel v-model="loginMode" height="100%" delimiter-icon="mdi-square" color="blue-darken-2"
|
<v-window continuous v-model="loginMode" show-arrows="hover">
|
||||||
hide-delimiter-background show-arrows>
|
<v-window-item>
|
||||||
<v-carousel-item>
|
<v-sheet class="v-center d-flex align-center justify-center ma-2" height="100%">
|
||||||
<v-sheet class="v-center" height="100%">
|
|
||||||
<LoginForm></LoginForm>
|
<LoginForm></LoginForm>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-carousel-item>
|
</v-window-item>
|
||||||
<v-carousel-item>
|
<v-window-item>
|
||||||
<v-sheet class="v-center" height="100%">
|
<v-sheet class="v-center d-flex align-center justify-center ma-2" height="100%">
|
||||||
<RegisterForm v-model="loginMode"></RegisterForm>
|
<RegisterForm v-model="loginMode"></RegisterForm>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-carousel-item>
|
</v-window-item>
|
||||||
</v-carousel></v-sheet>
|
</v-window>
|
||||||
|
</v-sheet>
|
||||||
<UserPanel v-else></UserPanel>
|
<UserPanel v-else></UserPanel>
|
||||||
</template>
|
</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>
|
<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="暂时没有提交记录">
|
:sort-by="[{ key: 'id', order: 'desc' }]" multi-sort items-per-page-text="每页" no-data-text="暂时没有提交记录">
|
||||||
<template v-slot:loading>
|
<template v-slot:loading>
|
||||||
<v-skeleton-loader></v-skeleton-loader>
|
<v-skeleton-loader></v-skeleton-loader>
|
||||||
@ -7,6 +8,16 @@
|
|||||||
<template v-slot:top>
|
<template v-slot:top>
|
||||||
<v-toolbar flat>
|
<v-toolbar flat>
|
||||||
<v-toolbar-title>所有提交记录</v-toolbar-title>
|
<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 @click="refresh" class="mb-2" color="primary" dark>
|
||||||
刷新
|
刷新
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@ -41,6 +52,25 @@ import axios, { Axios, AxiosError } from 'axios';
|
|||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
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 authStore = useAuthStore();
|
||||||
const storedToken = ref(authStore.token);
|
const storedToken = ref(authStore.token);
|
||||||
|
|
||||||
@ -178,7 +208,7 @@ const records = ref<Record[]>([]);
|
|||||||
const decorate = <T>(ex: AxiosError) => {
|
const decorate = <T>(ex: AxiosError) => {
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as T;
|
return ex.response?.data as T;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message } as T;
|
return { error: ex.message } as T;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,14 +278,6 @@ const initialize = async () => {
|
|||||||
records.value.push(record);
|
records.value.push(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
records.value.push({
|
|
||||||
id: 114514,
|
|
||||||
user_id: '114514',
|
|
||||||
problem: 1919810,
|
|
||||||
status: 'uke',
|
|
||||||
answer: 'NaN',
|
|
||||||
timestamp: 114514
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(initialize);
|
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>
|
<template>
|
||||||
<v-card subtitle="占位符占位符占位符" title="管理员面板" max-width="80%">
|
<v-card title="管理员面板" max-width="80%">
|
||||||
<v-card-item>
|
<v-card-item>
|
||||||
<v-chip class="ma-2" color="pink" label>
|
<v-chip class="ma-2" color="pink" label>
|
||||||
<v-icon icon="mdi-human" start></v-icon>
|
<v-icon icon="mdi-human" start></v-icon>
|
||||||
|
@ -112,7 +112,7 @@ const requestDeleteAccount = async (userId: string) => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as DeleteAccountResponse;
|
return ex.response?.data as DeleteAccountResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ const login = async () => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as LoginResponse;
|
return ex.response?.data as LoginResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ const requestPermission = async () => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as PermissionResponse;
|
return ex.response?.data as PermissionResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ const requestRepasswd = async () => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as RepasswdResponse;
|
return ex.response?.data as RepasswdResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ const requestRepasswd = async () => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as RepasswdResponse;
|
return ex.response?.data as RepasswdResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
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>
|
<tr>
|
||||||
<td>{{ item.id }}</td>
|
<td>{{ item.id }}</td>
|
||||||
<td>{{ item.problem }}</td>
|
<td>{{ item.problem }}</td>
|
||||||
<td>{{ item.answer }}</td>
|
<td v-if="userPermission == '1'">{{ item.answer }}</td>
|
||||||
<td>{{ decorateCheckingError(item.check_error) }}</td>
|
<td>{{ decorateCheckingError(item.check_error) }}</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<v-icon class="me-2" size="small" @click="editItem(item)">
|
<div v-if="userPermission == '1'">
|
||||||
mdi-pencil
|
<v-icon class="me-2" size="small" @click="editItem(item)">
|
||||||
</v-icon>
|
mdi-pencil
|
||||||
<v-icon size="small" @click="deleteItem(item)">
|
</v-icon>
|
||||||
mdi-delete
|
<v-icon size="small" @click="deleteItem(item)">
|
||||||
</v-icon>
|
mdi-delete
|
||||||
|
</v-icon>
|
||||||
|
</div>
|
||||||
|
<v-chip v-else class="chips" :color="colorsCache[item.id]"
|
||||||
|
@click="problemId = item.id, dialogProblemShow = true">
|
||||||
|
答题
|
||||||
|
</v-chip>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</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-data-table>
|
||||||
<v-dialog v-model="dialogShow" width="auto">
|
<v-dialog v-model="dialogShow" width="auto">
|
||||||
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
|
||||||
@ -118,6 +112,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</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>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useAuthStore } from '@/store/auth';
|
import { useAuthStore } from '@/store/auth';
|
||||||
@ -125,6 +127,53 @@ import axios, { Axios, AxiosError } from 'axios';
|
|||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
import * as mathjs from 'mathjs';
|
import * as mathjs from 'mathjs';
|
||||||
import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
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 loading = ref(false);
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
@ -145,36 +194,72 @@ const dialog = (title: string, text: string) => {
|
|||||||
|
|
||||||
const dialog0 = ref(false);
|
const dialog0 = ref(false);
|
||||||
const dialogDelete = ref(false);
|
const dialogDelete = ref(false);
|
||||||
const headers = ref([
|
// const headers = ref([
|
||||||
{
|
// {
|
||||||
title: '题号',
|
// title: '题号',
|
||||||
align: 'center',
|
// align: 'center',
|
||||||
key: 'id'
|
// key: 'id'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: '题面',
|
// title: '题面',
|
||||||
align: 'center',
|
// align: 'center',
|
||||||
key: 'problem'
|
// key: 'problem'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: '答案',
|
// title: '答案',
|
||||||
key: 'answer',
|
// key: 'answer',
|
||||||
align: 'center',
|
// align: 'center',
|
||||||
sortable: false
|
// sortable: false
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: '误差',
|
// title: '误差',
|
||||||
key: 'check_error',
|
// key: 'check_error',
|
||||||
align: 'center',
|
// align: 'center',
|
||||||
sortable: false
|
// sortable: false
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: '操作',
|
// title: '操作',
|
||||||
key: 'actions',
|
// key: 'actions',
|
||||||
align: 'center',
|
// align: 'center',
|
||||||
sortable: false
|
// 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 problems = ref<{ id: number, problem: string, answer: string, check_error: number }[]>([]);
|
||||||
const editedIndex = ref(-1);
|
const editedIndex = ref(-1);
|
||||||
@ -276,52 +361,10 @@ watch(dialogDelete, (val) => {
|
|||||||
val || closeDelete();
|
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) => {
|
const decorate = <T>(ex: AxiosError) => {
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as T;
|
return ex.response?.data as T;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message } as T;
|
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 () => {
|
const initialize = async () => {
|
||||||
let res = await requestAllProblem();
|
let res = await getAllProblem();
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
dialog('发生异常', res.error);
|
dialog('发生异常', res.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
problems.value = [];
|
problems.value = [];
|
||||||
let ids = res.result as number[];
|
let ids = res.result as number[];
|
||||||
|
for (let id of ids) {
|
||||||
|
colorsCache.value[id] = 'blue';
|
||||||
|
}
|
||||||
for (let i in ids) {
|
for (let i in ids) {
|
||||||
let id = ids[i];
|
let id = ids[i];
|
||||||
let resp = await requestQueryProblem(id);
|
let resp = await requestQueryProblem(id);
|
||||||
@ -410,6 +494,9 @@ const initialize = async () => {
|
|||||||
}
|
}
|
||||||
problems.value.push(pro);
|
problems.value.push(pro);
|
||||||
}
|
}
|
||||||
|
for (let id of ids) {
|
||||||
|
colorsCache.value[id] = await calcColor(id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(initialize);
|
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;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as DeleteAccountResponse;
|
return ex.response?.data as DeleteAccountResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card class="mx-auto pa-12 pb-8" width="448" elevation="8" rounded="lg">
|
<v-card class="mx-auto pa-12 pb-8" width="448" elevation="8" rounded="lg">
|
||||||
<v-form fast-fail @submit.prevent="submit">
|
<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"
|
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
|
||||||
label="账号"></v-text-field>
|
label="账号"></v-text-field>
|
||||||
|
|
||||||
@ -80,7 +77,7 @@ const login = async (userId: string, password: string) => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as LoginResponse;
|
return ex.response?.data as LoginResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-form fast-fail @submit.prevent="submit">
|
<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-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"
|
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
|
||||||
label="账号"></v-text-field>
|
label="账号"></v-text-field>
|
||||||
@ -24,7 +21,7 @@
|
|||||||
|
|
||||||
<v-card class="mb-12" color="surface-variant" variant="tonal">
|
<v-card class="mb-12" color="surface-variant" variant="tonal">
|
||||||
<v-card-text class="text-medium-emphasis text-caption">
|
<v-card-text class="text-medium-emphasis text-caption">
|
||||||
这只是个占位符文本,不知道为什么没有这段话整个布局就会乱成一坨,我还在研究怎么让它表现得正常一点,这个组件库的文档太抽象了,看不懂,唉。
|
新注册的账号默认是学生身份,老师请联系管理员提权。
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
@ -87,7 +84,7 @@ const register = async (userId: string, password: string) => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as RegisterResponse;
|
return ex.response?.data as RegisterResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ const requestRepasswd = async () => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as RepasswdResponse;
|
return ex.response?.data as RepasswdResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ const requestRepasswd = async () => {
|
|||||||
let ex = e as AxiosError;
|
let ex = e as AxiosError;
|
||||||
if (ex.response?.data) {
|
if (ex.response?.data) {
|
||||||
return ex.response?.data as RepasswdResponse;
|
return ex.response?.data as RepasswdResponse;
|
||||||
} {
|
} else {
|
||||||
return { error: ex.message };
|
return { error: ex.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card subtitle="占位符占位符占位符" title="用户信息面板" max-width="80%">
|
<v-card title="用户信息面板" max-width="80%">
|
||||||
<v-card-item>
|
<v-card-item>
|
||||||
<v-chip class="ma-2" color="primary" label>
|
<v-chip class="ma-2" color="primary" label>
|
||||||
<v-icon icon="mdi-account-circle-outline" start></v-icon>
|
<v-icon icon="mdi-account-circle-outline" start></v-icon>
|
||||||
@ -21,11 +21,10 @@
|
|||||||
删除账号
|
删除账号
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
<v-card-item>
|
<v-card-item v-if="userPermission == '1'">
|
||||||
<v-expansion-panels>
|
<v-chip class="chips" color="blue" @click="dialogRepasswd2Show = true">
|
||||||
<v-expansion-panel title="Token (放出来玩玩,之后会换个机制)" :text="token">
|
修改学生账号的密码
|
||||||
</v-expansion-panel>
|
</v-chip>
|
||||||
</v-expansion-panels>
|
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
</v-card>
|
</v-card>
|
||||||
<v-dialog v-model="dialogShow" width="auto">
|
<v-dialog v-model="dialogShow" width="auto">
|
||||||
@ -36,6 +35,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
<RepasswdDialog v-model="dialogRepasswdShow"></RepasswdDialog>
|
<RepasswdDialog v-model="dialogRepasswdShow"></RepasswdDialog>
|
||||||
|
<Repasswd2Dialog v-model="dialogRepasswd2Show"></Repasswd2Dialog>
|
||||||
<DeleteAccountDialog v-model="dialogDeleteAccountShow"></DeleteAccountDialog>
|
<DeleteAccountDialog v-model="dialogDeleteAccountShow"></DeleteAccountDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ import { jwtDecode, type JwtPayload } from 'jwt-decode';
|
|||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import DeleteAccountDialog from './DeleteAccountDialog.vue';
|
import DeleteAccountDialog from './DeleteAccountDialog.vue';
|
||||||
import RepasswdDialog from './RepasswdDialog.vue';
|
import RepasswdDialog from './RepasswdDialog.vue';
|
||||||
|
import Repasswd2Dialog from './Repasswd2Dialog.vue';
|
||||||
|
|
||||||
const dialogShow = ref(false);
|
const dialogShow = ref(false);
|
||||||
const dialogTitle = ref('');
|
const dialogTitle = ref('');
|
||||||
@ -55,6 +56,7 @@ const dialogClose = ref(() => { });
|
|||||||
const dialogDeleteAccountShow = ref(false);
|
const dialogDeleteAccountShow = ref(false);
|
||||||
|
|
||||||
const dialogRepasswdShow = ref(false);
|
const dialogRepasswdShow = ref(false);
|
||||||
|
const dialogRepasswd2Show = ref(false);
|
||||||
|
|
||||||
const dialog = (title: string, text: string) => {
|
const dialog = (title: string, text: string) => {
|
||||||
dialogTitle.value = title;
|
dialogTitle.value = title;
|
||||||
|
@ -5,22 +5,50 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Composables
|
// Composables
|
||||||
|
import Admin from '@/components/Admin.vue'
|
||||||
import Auth from '@/components/Auth.vue'
|
import Auth from '@/components/Auth.vue'
|
||||||
import HelloWorld from '@/components/HelloWorld.vue'
|
import Study from '@/components/Study.vue'
|
||||||
import HelloWorld2 from '@/components/HelloWorld2.vue'
|
import Homepage from '@/components/Homepage.vue'
|
||||||
import Login from '@/components/RegisterForm.vue'
|
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
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 = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HelloWorld
|
component: Homepage
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/auth',
|
path: '/auth',
|
||||||
name: 'home2',
|
name: 'auth',
|
||||||
component: 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";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", {
|
export const useAuthStore = defineStore("auth", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
token: localStorage.getItem('token') || '',
|
token: localStorage.getItem('token') || '',
|
||||||
|
adminToken: sessionStorage.getItem('adminToken') || ''
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setToken(token: string) {
|
setToken(token: string) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
localStorage.setItem('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 = '';
|
this.token = '';
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
},
|
},
|
||||||
|
setAdminToken(token: string) {
|
||||||
|
this.adminToken = token;
|
||||||
|
sessionStorage.setItem('adminToken', token);
|
||||||
|
},
|
||||||
|
clearAdminToken() {
|
||||||
|
this.adminToken = '';
|
||||||
|
sessionStorage.removeItem('adminToken');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
isAuthenticated: (state) => !!state.token,
|
isAuthenticated: (state) => !!state.token,
|
||||||
|
isAdmin: (state) => !!state.adminToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -50,6 +50,12 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8081',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
|
@ -4,7 +4,7 @@ add_rules("plugin.compile_commands.autoupdate")
|
|||||||
set_warnings("all")
|
set_warnings("all")
|
||||||
set_warnings("error")
|
set_warnings("error")
|
||||||
|
|
||||||
add_requires("civetweb", "cjson", "leveldb", "jwt-cpp", "cryptopp")
|
add_requires("civetweb", "cjson", "leveldb", "jwt-cpp", "cryptopp", "nlohmann_json")
|
||||||
|
|
||||||
local npm = "npm"
|
local npm = "npm"
|
||||||
if is_host("windows") then
|
if is_host("windows") then
|
||||||
@ -28,7 +28,7 @@ target("hash")
|
|||||||
target("db")
|
target("db")
|
||||||
set_languages("c++23")
|
set_languages("c++23")
|
||||||
set_kind("static")
|
set_kind("static")
|
||||||
add_packages("leveldb")
|
add_packages("leveldb", "nlohmann_json")
|
||||||
add_deps("hash")
|
add_deps("hash")
|
||||||
add_includedirs("include/db", "include")
|
add_includedirs("include/db", "include")
|
||||||
add_files("src/db/**.cpp")
|
add_files("src/db/**.cpp")
|
||||||
|
Loading…
Reference in New Issue
Block a user