math/ui/src/components/study/ProblemList.vue

393 lines
12 KiB
Vue

<template>
<v-data-table :loading="loading" :headers="(headers as any)" :items="problems"
:sort-by="[{ key: 'id', order: 'asc' }, { key: 'problem', order: 'asc' }]" multi-sort items-per-page-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-row>
<v-col>
<v-text-field v-model="editedItem.problem" prepend-inner-icon="mdi-head-question" label="题面"
:rules="notEmptyRules"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="editedItem.answer" prepend-inner-icon="mdi-numeric" label="答案"
append-inner-icon="mdi-auto-fix" @click:append-inner="generateAnswer" :rules="notEmptyRules">
<v-tooltip activator="parent" location="bottom">
点击右侧 <v-icon icon="mdi-auto-fix"></v-icon> 自动生成答案的{{ useFraction ? '带循环节' : '舍入' }}表示
<br />
再次点击还能生成其{{ useFraction ? '舍入' : '带循环节' }}表示。
</v-tooltip>
</v-text-field>
</v-col>
<v-col id="HookElement">
<v-text-field v-model="checkingError" prepend-inner-icon="mdi-sine-wave" :label="checkingErrorTitle"
:readonly="preciseMode" :append-inner-icon="checkingErrorIcon" :rules="checkingErrorRules"
@click:append-inner="preciseMode = !preciseMode">
<v-tooltip activator="parent" location="bottom">
点击右侧
<v-icon :icon="checkingErrorIcon"></v-icon>
切换模式,当前模式为{{ preciseMode ? '精确匹配' : '误差匹配' }}。
<br />
精确匹配:填写答案时必须与预设的标准答案完全相同才判定为正确;
<br />
误差匹配:与标准答案的差值的绝对值不超过误差的所有答案都会被判定为正确。
</v-tooltip>
</v-text-field>
</v-col>
</v-row>
</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.actions="{ item }">
<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>
</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>
</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 * as mathjs from 'mathjs';
const loading = ref(false);
const refresh = async () => {
loading.value = true;
await initialize();
loading.value = 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: 'problem'
},
{
title: '答案',
key: 'answer',
align: 'center',
sortable: false
},
{
title: '误差',
key: 'check_error',
align: 'center',
sortable: false
},
{
title: '操作',
key: 'actions',
align: 'center',
sortable: false
},
]);
const problems = ref<{ id: number, problem: string, answer: string, check_error: number }[]>([]);
const editedIndex = ref(-1);
const editedItem = ref({
id: 0,
problem: '',
answer: '',
check_error: 0,
});
const defaultItem = ref({
id: 0,
problem: '',
answer: '',
check_error: 0,
});
const preciseMode = ref(editedItem.value.check_error == 0);
const checkingErrorTitle = ref('');
const checkingErrorIcon = ref('');
const checkingError = ref('');
watch(preciseMode, () => {
if (preciseMode.value) {
checkingError.value = '精确匹配';
checkingErrorTitle.value = '';
checkingErrorIcon.value = 'mdi-crosshairs-gps';
} else {
checkingError.value = '';
checkingErrorTitle.value = '误差';
checkingErrorIcon.value = 'mdi-crosshairs-question';
}
}, { immediate: true });
watch(checkingError, () => {
if (preciseMode.value) {
editedItem.value.check_error = 0;
} else {
editedItem.value.check_error = parseFloat(checkingError.value);
}
})
const notEmptyRules = [(value: string) => {
if (value.length == 0) return '不能为空';
return true;
}];
const checkingErrorRules = [(value: string) => {
if (preciseMode.value) return true;
if (value.length == 0) return '必须设置误差';
let parsed = parseFloat(value);
if (parsed > 0) return true;
return '误差必须是大于零的实数';
}];
const mathBigNumber = mathjs.create(mathjs.all, {
number: 'BigNumber'
});
const mathFraction = mathjs.create(mathjs.all, {
number: 'Fraction'
});
let useFraction = false;
const generateAnswer = () => {
try {
editedItem.value.answer = (useFraction ? mathFraction : mathBigNumber).evaluate(editedItem.value.problem);
useFraction = !useFraction;
} catch (e) {
dialog('发生异常', `${e}`);
}
};
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;
} {
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 ProblemResponse = { success?: string, problem?: string, answer?: string, check_error?: number, error?: string };
const requestQueryProblem = 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/problems', formData);
return res.data as ProblemResponse;
} catch (e) {
return decorate<ProblemResponse>(e as AxiosError);
}
}
type AddProblemResponse = { success?: string, id?: number, error?: string };
const requestAddProblem = 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("problem", `${editedItem.value.problem}`);
formData.append("answer", `${editedItem.value.answer}`);
formData.append("error", `${preciseMode.value ? 0 : editedItem.value.check_error}`);
let res = await axios.post('/api/study/problems', formData);
return res.data as AddProblemResponse;
} catch (e) {
return decorate<AddProblemResponse>(e as AxiosError);
}
}
type DeleteProblemResponse = { success?: string, error?: string };
const requestDeleteProblem = 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/problems', formData);
return res.data as DeleteProblemResponse;
} catch (e) {
return decorate<DeleteProblemResponse>(e as AxiosError);
}
}
const initialize = async () => {
let res = await requestAllProblem();
if (res?.error) {
dialog('发生异常', res.error);
return;
}
problems.value = [];
let ids = res.result as number[];
for (let i in ids) {
let id = ids[i];
let resp = await requestQueryProblem(id);
let pro = { id, problem: '获取失败', answer: 'N/A', check_error: NaN };
if (resp?.success) {
pro.problem = resp.problem as string;
pro.answer = resp.answer as string;
pro.check_error = resp.check_error as number;
}
problems.value.push(pro);
}
};
onMounted(initialize);
const editItem = (item: any) => {
editedIndex.value = problems.value.indexOf(item);
editedItem.value = Object.assign({}, item);
dialog0.value = true;
};
const deleteItem = (item: any) => {
editedIndex.value = problems.value.indexOf(item);
editedItem.value = Object.assign({}, item);
dialogDelete.value = true;
};
const deleteItemConfirm = async () => {
let res = await requestDeleteProblem(editedItem.value.id);
if (res?.error) {
dialog('发生异常', res.error);
return;
}
problems.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 requestAddProblem();
if (res?.error) {
dialog(`${editedIndex.value > -1 ? '修改' : '添加'}失败`, res.error);
} else {
if (editedIndex.value > -1) {
Object.assign(problems.value[editedIndex.value], editedItem.value);
} else {
editedItem.value.id = res.id as number;
problems.value.push(editedItem.value);
}
}
close1();
};
</script>