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:
parent
53ed1f556f
commit
f2d88437e6
541
src/components/HomeworkEditDialog.vue
Normal file
541
src/components/HomeworkEditDialog.vue
Normal 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>
|
690
src/components/settings/cards/HomeworkTemplateCard.vue
Normal file
690
src/components/settings/cards/HomeworkTemplateCard.vue
Normal 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>
|
@ -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"
|
||||
:title="state.dialogTitle"
|
||||
:initial-content="state.textarea"
|
||||
:auto-save="autoSave"
|
||||
@save="handleHomeworkSave"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<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) {
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user