1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-10-22 02:03:10 +00:00
Classworks/src/components/HomeworkEditDialog.vue
copilot-swe-agent[bot] 92a2019566 Use kvToken instead of siteKey in examschedule button cloud URL
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2025-10-14 10:21:58 +00:00

582 lines
17 KiB
Vue

# 创建新的作业编辑对话框组件
<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"
width="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
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
v-if="showQuickTools"
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>
<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";
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;
},
showQuickTools() {
return getSetting("display.showQuickTools");
}
},
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>