1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-07 12:39:22 +00:00

Refactor index.vue to replace v-dialog with homework-edit-dialog for improved homework editing experience. Add HomeworkTemplateCard to settings.vue for homework management. Enhance kvServerProvider with additional error logging for better debugging.

This commit is contained in:
SunWuyuan 2025-07-05 13:32:34 +08:00
parent 53ed1f556f
commit f2d88437e6
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
5 changed files with 1265 additions and 26 deletions

View File

@ -0,0 +1,541 @@
# 创建新的作业编辑对话框组件
<template>
<v-dialog v-model="dialogVisible" width="auto" max-width="900" @click:outside="handleClose">
<v-card border>
<v-card-title>{{ title }}</v-card-title>
<v-card-subtitle>
{{ autoSave ? "喵?喵呜!" : "写完后点击上传谢谢喵" }}
</v-card-subtitle>
<v-card-text>
<div class="d-flex">
<div class="flex-grow-1">
<v-textarea
ref="inputRef"
v-model="content"
auto-grow
placeholder="使用换行表示分条"
rows="5"
@click="updateCurrentLine"
@keyup="updateCurrentLine"
width="480"
/>
<!-- Template Buttons Section -->
<div v-if="templateData" class="mt-4">
<div v-if="hasTemplates" class="template-buttons">
<!-- Subject specific books -->
<template v-if="subjectBooks">
<div v-for="(pages, book) in subjectBooks" :key="book" class="button-group">
<v-chip
class="ma-1 book-chip"
:color="isBookSelected(book) ? 'success' : 'default'"
:variant="isBookSelected(book) ? 'elevated' : 'flat'"
@click="handleBookClick(book)"
>
{{ book }}
</v-chip>
<!-- Show pages only if book is selected -->
<div v-if="isBookSelected(book)" class="pages-container mt-2">
<v-chip
v-for="page in pages"
:key="page"
class="ma-1"
:color="isPageSelected(book, page) ? 'info' : 'default'"
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
@click="handlePageClick(book, page)"
>
{{ page }}
</v-chip>
</div>
</div>
</template>
<!-- Common books -->
<template v-if="commonBooks">
<div v-for="(pages, book) in commonBooks" :key="book" class="button-group">
<v-chip
class="ma-1 book-chip"
:color="isBookSelected(book) ? 'success' : 'default'"
:variant="isBookSelected(book) ? 'elevated' : 'flat'"
@click="handleBookClick(book)"
>
{{ book }}
</v-chip>
<!-- Show pages only if book is selected -->
<div v-if="isBookSelected(book)" class="pages-container mt-2">
<v-chip
v-for="page in pages"
:key="page"
class="ma-1"
:color="isPageSelected(book, page) ? 'info' : 'default'"
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
@click="handlePageClick(book, page)"
>
{{ page }}
</v-chip>
</div>
</div>
</template>
<!-- Actions -->
<div v-if="templateData.actions?.length" class="button-group">
<v-chip
v-for="action in templateData.actions"
:key="action"
class="ma-1"
color="primary"
variant="flat"
@click="insertTemplate(action)"
>
{{ action }}
</v-chip>
</div>
</div>
<div v-else class="text-center text-body-2 text-disabled mt-2">
暂无可用的模板
</div>
</div>
</div>
<!-- Quick Tools Section -->
<div class="quick-tools ml-4" style="min-width: 180px;">
<!-- Numeric Keypad -->
<div class="numeric-keypad mb-4">
<div class="keypad-row">
<v-btn
v-for="n in 3"
:key="n"
size="small"
variant="tonal"
class="keypad-btn"
@click="insertAtCursor(String(n))"
>
{{ n }}
</v-btn>
</div>
<div class="keypad-row">
<v-btn
v-for="n in 3"
:key="n"
size="small"
variant="tonal"
class="keypad-btn"
@click="insertAtCursor(String(n + 3))"
>
{{ n + 3 }}
</v-btn>
</div>
<div class="keypad-row">
<v-btn
v-for="n in 3"
:key="n"
size="small"
variant="tonal"
class="keypad-btn"
@click="insertAtCursor(String(n + 6))"
>
{{ n + 6 }}
</v-btn>
</div>
<div class="keypad-row">
<v-btn
size="small"
variant="tonal"
class="keypad-btn"
@click="insertAtCursor('-')"
>
-
</v-btn>
<v-btn
size="small"
variant="tonal"
class="keypad-btn"
@click="insertAtCursor('0')"
>
0
</v-btn>
<v-btn
size="small"
variant="tonal"
class="keypad-btn"
color="error"
@click="deleteLastChar"
>
</v-btn>
</div>
<div class="keypad-row">
<v-btn
size="small"
variant="tonal"
class="keypad-btn space-btn"
@click="insertAtCursor(' ')"
>
空格
</v-btn><v-btn
size="small"
variant="tonal"
class="keypad-btn space-btn"
@click="insertAtCursor('\n')"
>
换行
</v-btn>
</div>
</div>
<div class="d-flex flex-wrap gap-1">
<v-btn
v-for="text in quickTexts"
:key="text"
size="small"
variant="flat"
@click="insertAtCursor(text)"
>
{{ text }}
</v-btn>
</div>
</div>
</div>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import dataProvider from "@/utils/dataProvider";
export default {
name: "HomeworkEditDialog",
props: {
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
required: true
},
initialContent: {
type: String,
default: ""
},
autoSave: {
type: Boolean,
default: false
}
},
emits: ["update:modelValue", "save"],
data() {
return {
content: "",
templateData: null,
currentLine: "",
currentLineStart: 0,
currentLineEnd: 0,
quickTexts: ["课", "题", "T", "P"]
};
},
computed: {
dialogVisible: {
get() {
return this.modelValue;
},
set(value) {
this.$emit("update:modelValue", value);
}
},
subject() {
//
return this.title;
},
hasTemplates() {
return !!(
(this.templateData?.actions?.length) ||
this.subjectBooks ||
this.commonBooks
);
},
subjectBooks() {
if (!this.subject || !this.templateData?.subjects?.[this.subject]?.books) {
return null;
}
return this.templateData.subjects[this.subject].books;
},
commonBooks() {
if (!this.templateData?.commonSubject?.books) {
return null;
}
return this.templateData.commonSubject.books;
}
},
watch: {
async modelValue(newValue) {
if (newValue) {
//
this.content = this.initialContent;
//
try {
this.templateData = await dataProvider.loadData("classworks-config-homework-template");
} catch (error) {
console.error("Failed to load homework templates:", error);
this.templateData = null;
}
this.$nextTick(() => {
if (this.$refs.inputRef) {
this.$refs.inputRef.focus();
this.updateCurrentLine();
}
});
}
}
},
methods: {
handleClose() {
const trimmedContent = this.content.trim();
if (trimmedContent !== this.initialContent.trim()) {
this.$emit("save", trimmedContent);
}
this.dialogVisible = false;
},
updateCurrentLine() {
const textarea = this.$refs.inputRef.$el.querySelector('textarea');
const cursorPosition = textarea.selectionStart;
const content = this.content;
let currentPos = 0;
const lines = content.split('\n');
for (let i = 0; i < lines.length; i++) {
const lineLength = lines[i].length;
const totalLength = currentPos + lineLength;
if (cursorPosition <= totalLength || i === lines.length - 1) {
this.currentLine = lines[i];
this.currentLineStart = currentPos;
this.currentLineEnd = totalLength;
break;
}
currentPos = totalLength + 1; // +1 for the newline character
}
//
if (!this.currentLine) {
this.currentLine = "";
this.currentLineStart = content.length;
this.currentLineEnd = content.length;
}
},
isBookSelected(book) {
return this.currentLine.includes(book);
},
isPageSelected(book, page) {
return this.currentLine.includes(page);
},
handleBookClick(book) {
if (this.isBookSelected(book)) {
//
const lines = this.content.split('\n');
const lineToDelete = lines.findIndex(line => line.includes(book));
if (lineToDelete !== -1) {
lines.splice(lineToDelete, 1);
this.content = lines.join('\n');
}
} else {
//
const hasContent = this.content.trim().length > 0;
this.content = (hasContent ? this.content.trim() + '\n' : '') + book;
}
this.$nextTick(() => {
const textarea = this.$refs.inputRef.$el.querySelector('textarea');
textarea.focus();
if (!this.isBookSelected(book)) {
//
const lines = this.content.split('\n');
let position = 0;
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes(book)) {
position += lines[i].length;
break;
}
position += lines[i].length + 1; // +1 for newline
}
textarea.setSelectionRange(position, position);
}
this.updateCurrentLine();
});
},
handlePageClick(book, page) {
if (this.isPageSelected(book, page)) {
//
const start = this.currentLineStart;
const end = this.currentLineEnd;
const currentLineContent = this.content.slice(start, end);
const lastIndex = currentLineContent.lastIndexOf(page);
if (lastIndex !== -1) {
const newLineContent =
currentLineContent.slice(0, lastIndex) +
currentLineContent.slice(lastIndex + page.length);
this.content = this.content.slice(0, start) +
newLineContent.trim() +
this.content.slice(end);
}
} else {
//
const start = this.currentLineStart;
const end = this.currentLineEnd;
const currentLineContent = this.content.slice(start, end);
this.content = this.content.slice(0, start) +
currentLineContent.trim() +
(currentLineContent.trim().length > 0 ? ' ' : '') +
page +
this.content.slice(end);
}
this.$nextTick(() => {
const textarea = this.$refs.inputRef.$el.querySelector('textarea');
textarea.focus();
//
const lines = this.content.split('\n');
let position = 0;
for (let i = 0; i < lines.length; i++) {
position += lines[i].length;
if (position > this.currentLineStart) {
break;
}
position += 1; // +1 for newline
}
textarea.setSelectionRange(position, position);
this.updateCurrentLine();
});
},
insertTemplate(text) {
const textarea = this.$refs.inputRef.$el.querySelector('textarea');
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
//
const needsSpace = start > 0 && this.content[start - 1] !== ' ' && this.content[start - 1] !== '\n';
this.content = this.content.slice(0, start) + (needsSpace ? ' ' : '') + text + this.content.slice(end);
this.$nextTick(() => {
textarea.focus();
const newPosition = start + text.length + (needsSpace ? 1 : 0);
textarea.setSelectionRange(newPosition, newPosition);
this.updateCurrentLine();
});
},
insertAtCursor(text) {
if (!text) return;
const textarea = this.$refs.inputRef.$el.querySelector('textarea');
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
this.content = this.content.slice(0, start) + text + this.content.slice(end);
this.$nextTick(() => {
textarea.focus();
const newPosition = start + text.length;
textarea.setSelectionRange(newPosition, newPosition);
this.updateCurrentLine();
});
},
deleteLastChar() {
const textarea = this.$refs.inputRef.$el.querySelector('textarea');
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
if (start === end) {
//
if (start > 0) {
this.content = this.content.slice(0, start - 1) + this.content.slice(start);
this.$nextTick(() => {
textarea.focus();
textarea.setSelectionRange(start - 1, start - 1);
this.updateCurrentLine();
});
}
} else {
//
this.content = this.content.slice(0, start) + this.content.slice(end);
this.$nextTick(() => {
textarea.focus();
textarea.setSelectionRange(start, start);
this.updateCurrentLine();
});
}
}
}
};
</script>
<style scoped>
.template-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
.book-chip {
align-self: flex-start;
}
.pages-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding-left: 16px;
}
.group-label {
font-size: 0.875rem;
color: rgba(0, 0, 0, 0.6);
margin-right: 8px;
white-space: nowrap;
}
:deep(.v-chip) {
cursor: pointer;
user-select: none;
}
.quick-tools {
border-left: 1px solid rgba(0, 0, 0, 0.12);
padding-left: 16px;
}
.gap-1 {
gap: 4px;
}
.numeric-keypad {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 4px;
}
.keypad-row {
display: flex;
gap: 4px;
}
.keypad-btn {
flex: 1;
min-width: 36px !important;
}
.space-btn {
width: 100% !important;
}
</style>

View File

@ -0,0 +1,690 @@
<template>
<settings-card
title="作业模板配置"
icon="mdi-book-edit"
:loading="loading"
border
>
<!-- 顶部操作按钮 -->
<v-alert
v-if="error"
type="error"
variant="tonal"
closable
class="mb-4"
>
{{ error }}
</v-alert>
<div class="d-flex justify-space-between align-center mb-6">
<div>
<v-btn
color="primary"
size="large"
prepend-icon="mdi-refresh"
:loading="loading"
@click="loadConfig"
class="mr-2"
>
重新加载配置
</v-btn>
<v-btn
color="success"
size="large"
prepend-icon="mdi-content-save"
:loading="loading"
@click="saveConfig"
>
保存所有更改
</v-btn>
</div>
<v-chip
v-if="hasChanges"
color="warning"
variant="elevated"
>
有未保存的更改
</v-chip>
</div>
<v-row>
<v-col cols="12" md="6">
<setting-group title="科目配置" icon="mdi-book" border>
<v-list>
<v-list-item>
<v-text-field
v-model="newSubject"
label="添加新科目"
variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addSubject"
@keyup.enter="addSubject"
/>
</v-list-item>
<v-list-item v-for="subject in subjectList" :key="subject">
<v-card border class="w-100 mb-2">
<v-card-title class="d-flex align-center">
<v-text-field
v-model="editedSubjects[subject]"
:placeholder="subject"
density="comfortable"
variant="plain"
hide-details
@blur="updateSubject(subject)"
/>
<v-spacer />
<v-btn
icon="mdi-delete"
variant="text"
color="error"
size="small"
@click="deleteSubject(subject)"
/>
</v-card-title>
<v-card-text>
<v-text-field
v-model="newBookTypes[subject]"
label="添加作业本名称"
variant="outlined"
density="comfortable"
class="mb-2"
append-inner-icon="mdi-plus"
@click:append-inner="() => addBookType(subject)"
@keyup.enter="() => addBookType(subject)"
/>
<v-list density="compact" border rounded>
<v-list-item
v-for="(books, bookType) in config.subjects[subject].books"
:key="bookType"
:title="bookType"
@click="openSubjectBookDialog(subject, bookType, books)"
>
<template v-slot:prepend>
<v-icon icon="mdi-book-open-variant" class="mr-2" />
</template>
<template v-slot:append>
<v-chip
size="small"
class="mr-2"
color="info"
>
{{ books.length }}个部分
</v-chip>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
size="small"
@click.stop="() => deleteBookType(subject, bookType)"
/>
</template>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-list-item>
</v-list>
</setting-group>
</v-col>
<v-col cols="12" md="6">
<setting-group title="通用配置" icon="mdi-cog" border>
<v-list>
<v-list-item>
<v-text-field
v-model="newCommonBook"
label="添加作业本名称"
variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addCommonBook"
@keyup.enter="addCommonBook"
/>
</v-list-item>
<v-list-item>
<v-list density="compact" border rounded>
<v-list-item
v-for="(books, bookType) in config.commonSubject.books"
:key="bookType"
:title="bookType"
@click="openSubjectBookDialog('common', bookType, books)"
>
<template v-slot:prepend>
<v-icon icon="mdi-book-multiple" class="mr-2" />
</template>
<template v-slot:append>
<v-chip
size="small"
class="mr-2"
color="info"
>
{{ books.length }}个部分
</v-chip>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
size="small"
@click.stop="() => deleteBookType('common', bookType)"
/>
</template>
</v-list-item>
</v-list>
</v-list-item>
<v-divider class="my-2" />
<v-list-item>
<v-text-field
v-model="newAction"
label="添加操作"
variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addAction"
@keyup.enter="addAction"
/>
</v-list-item>
<v-list-item>
<v-list density="compact" border rounded>
<v-list-item
v-for="action in config.actions"
:key="action"
:title="action"
@click="openActionDialog(action)"
>
<template v-slot:append>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
size="small"
@click.stop="removeAction(action)"
/>
</template>
</v-list-item>
</v-list>
</v-list-item>
</v-list>
</setting-group>
</v-col>
</v-row>
<!-- 编辑弹框 -->
<v-dialog v-model="dialog.show" max-width="600px">
<v-card>
<v-card-title class="text-h5 pa-4">
{{ dialog.title }}
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12">
<v-text-field
v-model="dialog.editedItem.name"
:label="dialog.nameLabel"
variant="outlined"
density="comfortable"
:rules="[v => !!v || '名称不能为空']"
/>
</v-col>
<v-col cols="12" v-if="dialog.editedItem.type === 'subjectBook'">
<div class="text-subtitle-2 mb-2">所属科目</div>
<v-chip color="primary">{{ dialog.editedItem.subject }}</v-chip>
</v-col>
<v-col cols="12" v-if="['subjectBook', 'commonBook'].includes(dialog.editedItem.type)">
<v-card variant="outlined">
<v-card-title class="text-subtitle-1 py-2">需完成部分</v-card-title>
<v-card-text class="pt-0">
<v-list density="compact" border rounded class="mb-2">
<v-list-item
v-for="(task, index) in dialog.editedItem.tasks"
:key="index"
>
<template v-slot:prepend>
<v-icon icon="mdi-checkbox-blank-circle-outline" size="small" class="mr-2" />
</template>
<v-text-field
v-model="dialog.editedItem.tasks[index]"
variant="plain"
density="compact"
hide-details
/>
<template v-slot:append>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
size="small"
@click="removeTask(index)"
/>
</template>
</v-list-item>
</v-list>
<v-text-field
v-model="newTask"
label="添加需完成部分"
variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addTask"
@keyup.enter="addTask"
class="mt-2"
/>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
color="primary"
variant="elevated"
@click="saveDialog"
>
关闭
</v-btn>
<v-btn
color="error"
variant="text"
@click="closeDialog"
>
取消
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 底部保存提示 -->
<v-snackbar
v-model="showSnackbar"
:color="snackbarColor"
:timeout="3000"
>
{{ snackbarText }}
</v-snackbar>
</settings-card>
</template>
<script>
import { reactive } from 'vue';
import SettingsCard from '@/components/SettingsCard.vue';
import SettingGroup from '@/components/settings/SettingGroup.vue';
import dataProvider from "@/utils/dataProvider.js";
const DEFAULT_CONFIG = {
subjects: {
"语文": {
books: {
"课本": ["第一单元", "第二单元"],
"练习册": ["第一章", "第二章"]
}
},
"数学": {
books: {
"课本": ["第一章", "第二章"],
"习题册": ["基础练习", "提高练习"]
}
},
"英语": {
books: {
"课本": ["Unit 1", "Unit 2"],
"练习册": ["Chapter 1", "Chapter 2"]
}
}
},
commonSubject: {
books: {
"试卷": ["单元测试", "期中测试", "期末测试"],
"假期作业": ["必做题", "选做题"]
}
},
actions: ["写完", "下一课", "不交", "明天交"]
};
export default {
name: 'HomeworkTemplateCard',
components: {
SettingsCard,
SettingGroup
},
data() {
return {
loading: false,
error: null,
config: reactive(JSON.parse(JSON.stringify(DEFAULT_CONFIG))),
originalConfig: null,
newSubject: '',
newCommonBook: '',
newAction: '',
newTask: '',
editedSubjects: {},
editedBookTypes: {},
newBookTypes: {},
newBooks: {},
showSnackbar: false,
snackbarText: '',
snackbarColor: 'success',
isNewConfig: true,
dialog: {
show: false,
title: '',
nameLabel: '',
editedItem: {
name: '',
type: '', // 'book', 'commonBook', 'action'
subject: '',
bookType: '',
originalName: '',
tasks: []
}
}
};
},
computed: {
subjectList() {
return Object.keys(this.config.subjects);
},
hasChanges() {
if (this.isNewConfig) return true;
return this.originalConfig &&
JSON.stringify(this.config) !== JSON.stringify(this.originalConfig);
}
},
created() {
this.loadConfig();
},
methods: {
async loadConfig() {
this.loading = true;
try {
const response = await dataProvider.loadData("classworks-config-homework-template");
if (response) {
//
const config = response;
Object.assign(this.config, config);
this.originalConfig = JSON.parse(JSON.stringify(config));
this.isNewConfig = false;
this.showMessage('配置已加载', 'success');
} else if (response.error?.code === 'NOT_FOUND') {
// 使
this.showMessage('使用默认配置', 'info');
this.isNewConfig = true;
} else {
// 使
const errorMsg = response.error?.message || '加载失败';
this.showMessage(`加载失败: ${errorMsg},可继续编辑当前配置`, 'warning');
}
} catch (error) {
// 使
console.error('Failed to load config:', error);
this.showMessage('加载失败,可继续编辑当前配置', 'warning');
}
this.loading = false;
},
async saveConfig() {
this.loading = true;
try {
const response = await dataProvider.saveData("classworks-config-homework-template", this.config);
if (response) {
this.originalConfig = JSON.parse(JSON.stringify(this.config));
this.isNewConfig = false;
this.showMessage('配置已保存', 'success');
} else {
throw new Error(response || '保存失败');
}
} catch (error) {
console.error('Failed to save config:', error);
this.showMessage(`保存失败: ${error.message},请稍后重试`, 'error');
}
this.loading = false;
},
showMessage(text, color = 'success') {
this.snackbarText = text;
this.snackbarColor = color;
this.showSnackbar = true;
},
addSubject() {
if (!this.newSubject) return;
if (!this.config.subjects[this.newSubject]) {
this.config.subjects[this.newSubject] = { books: {} };
}
this.newSubject = '';
},
updateSubject(oldSubject) {
const newSubject = this.editedSubjects[oldSubject];
if (newSubject && newSubject !== oldSubject) {
const subjectData = this.config.subjects[oldSubject];
this.config.subjects[newSubject] = subjectData;
delete this.config.subjects[oldSubject];
}
delete this.editedSubjects[oldSubject];
},
deleteSubject(subject) {
delete this.config.subjects[subject];
},
addBookType(subject) {
const newType = this.newBookTypes[subject];
if (!newType) return;
if (!this.config.subjects[subject].books[newType]) {
this.config.subjects[subject].books[newType] = [];
}
this.newBookTypes[subject] = '';
},
updateBookType(subject, oldType) {
const key = `${subject}-${oldType}`;
const newType = this.editedBookTypes[key];
if (newType && newType !== oldType) {
const books = this.config.subjects[subject].books[oldType];
this.config.subjects[subject].books[newType] = books;
delete this.config.subjects[subject].books[oldType];
}
delete this.editedBookTypes[key];
},
deleteBookType(subject, bookType) {
if (subject === 'common') {
delete this.config.commonSubject.books[bookType];
} else {
delete this.config.subjects[subject].books[bookType];
}
},
addBook(subject, bookType) {
const key = `${subject}-${bookType}`;
const newBook = this.newBooks[key];
if (!newBook) return;
if (!this.config.subjects[subject].books[bookType].includes(newBook)) {
this.config.subjects[subject].books[bookType].push(newBook);
}
this.newBooks[key] = '';
},
removeBook(subject, bookType, book) {
const books = this.config.subjects[subject].books[bookType];
const index = books.indexOf(book);
if (index > -1) {
books.splice(index, 1);
}
},
addCommonBook() {
if (!this.newCommonBook) return;
if (!this.config.commonSubject.books[this.newCommonBook]) {
this.config.commonSubject.books[this.newCommonBook] = [];
}
this.newCommonBook = '';
},
removeCommonBook(book) {
delete this.config.commonSubject.books[book];
},
addAction() {
if (!this.newAction) return;
if (!this.config.actions.includes(this.newAction)) {
this.config.actions.push(this.newAction);
}
this.newAction = '';
},
removeAction(action) {
const index = this.config.actions.indexOf(action);
if (index > -1) {
this.config.actions.splice(index, 1);
}
},
openBookDialog(subject, bookType, book) {
this.dialog.show = true;
this.dialog.title = '编辑需完成部分';
this.dialog.nameLabel = '部分名称';
this.dialog.editedItem = {
name: book,
type: 'book',
subject,
bookType,
originalName: book,
tasks: this.config.subjects[subject].books[bookType]
};
},
openCommonBookDialog(book) {
this.dialog.show = true;
this.dialog.title = '编辑通用作业本';
this.dialog.nameLabel = '作业本名称';
this.dialog.editedItem = {
name: book,
type: 'commonBook',
originalName: book,
tasks: Array.isArray(this.config.commonSubject.books[book]) ? [...this.config.commonSubject.books[book]] : []
};
},
openActionDialog(action) {
this.dialog = {
show: true,
title: '编辑操作',
nameLabel: '操作名称',
editedItem: {
name: action,
type: 'action',
originalName: action,
tasks: []
}
};
},
addTask() {
if (!this.newTask) return;
if (!this.dialog.editedItem.tasks) {
this.dialog.editedItem.tasks = [];
}
this.dialog.editedItem.tasks.push(this.newTask);
this.newTask = '';
},
removeTask(index) {
this.dialog.editedItem.tasks.splice(index, 1);
},
openSubjectBookDialog(subject, bookType, books) {
this.dialog.show = true;
this.dialog.title = subject === 'common' ? '编辑通用作业本' : '编辑作业本';
this.dialog.nameLabel = '作业本名称';
this.dialog.editedItem = {
name: bookType,
type: 'subjectBook',
subject,
originalName: bookType,
tasks: Array.isArray(books) ? [...books] : []
};
},
saveDialog() {
const { type, name, subject, originalName, tasks } = this.dialog.editedItem;
if (!name) {
this.showMessage('名称不能为空', 'error');
return;
}
let actionIndex;
const targetBooks = subject === 'common'
? this.config.commonSubject.books
: subject ? this.config.subjects[subject].books : null;
switch (type) {
case 'subjectBook':
if (targetBooks) {
if (originalName !== name) {
//
targetBooks[name] = tasks || [];
delete targetBooks[originalName];
} else {
//
targetBooks[name] = tasks || [];
}
}
break;
case 'action':
actionIndex = this.config.actions.indexOf(originalName);
if (actionIndex > -1) {
this.config.actions[actionIndex] = name;
}
break;
}
this.closeDialog();
//this.showMessage('', 'success');
},
closeDialog() {
this.dialog = {
show: false,
title: '',
nameLabel: '',
editedItem: {
name: '',
type: '',
subject: '',
originalName: '',
tasks: []
}
};
this.newTask = '';
}
}
};
</script>
<style scoped>
.v-card-text {
padding-top: 0;
}
</style>

View File

@ -241,27 +241,13 @@
</v-col>
</div>
<v-dialog
<homework-edit-dialog
v-model="state.dialogVisible"
width="500"
@click:outside="handleClose"
>
<v-card border>
<v-card-title>{{ state.dialogTitle }}</v-card-title>
<v-card-subtitle>
{{ autoSave ? "喵?喵呜!" : "写完后点击上传谢谢喵" }}
</v-card-subtitle>
<v-card-text>
<v-textarea
ref="inputRef"
v-model="state.textarea"
auto-grow
placeholder="使用换行表示分条"
rows="5"
/>
</v-card-text>
</v-card>
</v-dialog>
:title="state.dialogTitle"
:initial-content="state.textarea"
:auto-save="autoSave"
@save="handleHomeworkSave"
/>
<v-snackbar v-model="state.snackbar" :timeout="2000">
{{ state.snackbarText }}
@ -622,6 +608,7 @@ import RandomPicker from "@/components/RandomPicker.vue";
import NamespaceAccess from "@/components/NamespaceAccess.vue";
import FloatingToolbar from "@/components/FloatingToolbar.vue";
import FloatingICP from "@/components/FloatingICP.vue";
import HomeworkEditDialog from "@/components/HomeworkEditDialog.vue";
import dataProvider from "@/utils/dataProvider";
import {
getSetting,
@ -645,6 +632,7 @@ export default {
NamespaceAccess,
FloatingToolbar,
FloatingICP,
HomeworkEditDialog,
},
data() {
return {
@ -1241,11 +1229,20 @@ export default {
subject;
this.state.textarea = this.state.boardData.homework[subject].content;
this.state.dialogVisible = true;
this.$nextTick(() => {
if (this.$refs.inputRef) {
this.$refs.inputRef.focus();
}
});
},
async handleHomeworkSave(content) {
if (!this.currentEditSubject) return;
this.state.boardData.homework[this.currentEditSubject] = {
content: content,
};
this.state.synced = false;
if (this.autoSave) {
await this.trySave(true);
}
},
splitPoint(content) {

View File

@ -156,6 +156,9 @@
<v-tabs-window-item value="randomPicker">
<random-picker-card border :is-mobile="isMobile" />
</v-tabs-window-item>
<v-tabs-window-item value="homework">
<homework-template-card border />
</v-tabs-window-item>
<v-tabs-window-item value="developer"
><settings-card border title="开发者选项" icon="mdi-developer-board">
<v-list>
@ -234,6 +237,7 @@ import SettingsExplorer from "@/components/settings/SettingsExplorer.vue";
import SettingsLinkGenerator from "@/components/SettingsLinkGenerator.vue";
import NamespaceSettingsCard from "@/components/settings/cards/NamespaceSettingsCard.vue";
import RandomPickerCard from "@/components/settings/cards/RandomPickerCard.vue";
import HomeworkTemplateCard from '@/components/settings/cards/HomeworkTemplateCard.vue';
export default {
name: "Settings",
components: {
@ -252,6 +256,7 @@ export default {
SettingsLinkGenerator,
NamespaceSettingsCard,
RandomPickerCard,
HomeworkTemplateCard,
},
setup() {
const { mobile } = useDisplay();
@ -389,6 +394,11 @@ export default {
icon: "mdi-dice-multiple",
value: "randomPicker",
},
{
title: "作业模板",
icon: "mdi-book-edit",
value: "homework",
},
{
title: "开发者",
icon: "mdi-developer-board",

View File

@ -133,7 +133,7 @@ export const kvServerProvider = {
if (error.response?.status === 404) {
return formatError("数据不存在", "NOT_FOUND");
}
console.log(error);
return formatError(
error.response?.data?.message || "服务器连接失败",
"NETWORK_ERROR"
@ -150,6 +150,7 @@ export const kvServerProvider = {
});
return formatResponse(true);
} catch (error) {
console.log(error);
return formatError(
error.response?.data?.message || "保存失败",
"SAVE_ERROR"