1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-07 21:13:11 +00:00
Classworks/src/components/HomeworkEditDialog.vue
copilot-swe-agent[bot] 056225b6b3 Fix notification deletion bug: save {} instead of [] when list is empty
When all notifications are deleted, the persistentNotifications array
becomes empty ([]). The backend doesn't accept an empty array as a
valid value, requiring an empty object ({}) instead. This fix modifies
both index.vue and UrgentTestDialog.vue to save {} when the
notification list is empty.

Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2025-12-01 10:16:47 +00:00

632 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 创建新的作业编辑对话框组件
<template>
<v-dialog
v-model="dialogVisible"
:fullscreen="isMobile"
max-width="900"
width="auto"
@click:outside="handleClose"
>
<v-card border>
<v-card-title class="d-flex align-center">
{{ title }}
<v-spacer />
<v-btn
icon="mdi-close"
variant="text"
@click="handleClose"
/>
</v-card-title>
<v-card-subtitle>
{{ autoSave ? autoSavePromptText : manualSavePromptText }}
</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"
:width="isMobile ? '100%' : '480'"
@click="updateCurrentLine"
@keyup="updateCurrentLine"
/>
<!-- 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
:color="isBookSelected(book) ? 'success' : 'default'"
:variant="isBookSelected(book) ? 'elevated' : 'flat'"
class="ma-1 book-chip"
@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"
:color="isPageSelected(book, page) ? 'info' : 'default'"
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
class="ma-1"
@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
:color="isBookSelected(book) ? 'success' : 'default'"
:variant="isBookSelected(book) ? 'elevated' : 'flat'"
class="ma-1 book-chip"
@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"
:color="isPageSelected(book, page) ? 'info' : 'default'"
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
class="ma-1"
@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
v-if="showQuickTools && !isMobile"
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"
class="keypad-btn"
size="small"
variant="tonal"
@click="insertAtCursor(String(n))"
>
{{ n }}
</v-btn>
</div>
<div class="keypad-row">
<v-btn
v-for="n in 3"
:key="n"
class="keypad-btn"
size="small"
variant="tonal"
@click="insertAtCursor(String(n + 3))"
>
{{ n + 3 }}
</v-btn>
</div>
<div class="keypad-row">
<v-btn
v-for="n in 3"
:key="n"
class="keypad-btn"
size="small"
variant="tonal"
@click="insertAtCursor(String(n + 6))"
>
{{ n + 6 }}
</v-btn>
</div>
<div class="keypad-row">
<v-btn
class="keypad-btn"
size="small"
variant="tonal"
@click="insertAtCursor('-')"
>
-
</v-btn>
<v-btn
class="keypad-btn"
size="small"
variant="tonal"
@click="insertAtCursor('0')"
>
0
</v-btn>
<v-btn
class="keypad-btn"
color="error"
size="small"
variant="tonal"
@click="deleteLastChar"
>
</v-btn>
</div>
<div class="keypad-row">
<v-btn
class="keypad-btn space-btn"
size="small"
variant="tonal"
@click="insertAtCursor(' ')"
>
空格
</v-btn>
<v-btn
class="keypad-btn space-btn"
size="small"
variant="tonal"
@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-alert
v-if="isEditingPastData"
type="warning"
variant="tonal"
class="mx-4 mb-4"
border="start"
border-color="warning"
prominent
>
<template #prepend />
<div class="d-flex flex-column">
<div class="text-h6 mb-1">
你打算修改历史
</div>
<div class="text-body-2">
这是 {{ new Date(currentDateString.slice(0,4), currentDateString.slice(4,6)-1, currentDateString.slice(6,8)).toLocaleDateString() }} 的作业 请谨慎操作确保不会覆盖重要数据
</div>
</div>
</v-alert>
<div class="text-center text-body-2 text-disabled mb-5">
点击空白处完成编辑
</div>
</v-card>
</v-dialog>
</template>
<script>
import dataProvider from "@/utils/dataProvider";
import {getSetting} from "@/utils/settings";
import { useDisplay } from "vuetify";
export default {
name: "HomeworkEditDialog",
props: {
modelValue: {
type: Boolean,
required: true
},
title: {
type: String,
required: true
},
initialContent: {
type: String,
default: ""
},
autoSave: {
type: Boolean,
default: false
},
isEditingPastData: {
type: Boolean,
default: false
},
currentDateString: {
type: String,
default: ""
}
},
emits: ["update:modelValue", "save"],
setup() {
const { mobile } = useDisplay();
return { isMobile: mobile };
},
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;
},
showQuickTools() {
return getSetting("display.showQuickTools");
},
autoSavePromptText() {
return getSetting("edit.autoSavePromptText");
},
manualSavePromptText() {
return getSetting("edit.manualSavePromptText");
}
},
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>