重构项目结构,移除无用组件,添加数学库依赖,更新路由配置以支持新页面

This commit is contained in:
keqingmoe 2025-01-04 21:07:25 +08:00
parent 035448ca00
commit 12aee0ab67
32 changed files with 1175 additions and 415 deletions

24
ui/components.d.ts vendored
View File

@ -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']
} }
} }

View File

@ -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>

View File

@ -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": "*",

View File

@ -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>

View File

@ -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>

View File

@ -1,7 +0,0 @@
<template>
点左边登录
</template>
<script setup lang="ts">
//
</script>

View File

@ -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>

View 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>

View 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>

View File

@ -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);

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View 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>

View 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>

View File

@ -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);

View 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>

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View File

@ -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 };
} }
} }

View File

@ -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;

View File

@ -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
} }
] ]

View File

@ -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,
}, },
}); });

View File

@ -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: {

View File

@ -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")