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

Add vuedraggable dependency for improved drag-and-drop functionality. Refactor index.vue to use subject names as keys and streamline subject management. Update settings.vue to include SubjectManagementCard for better subject configuration.

This commit is contained in:
SunWuyuan 2025-07-05 14:18:22 +08:00
parent f2d88437e6
commit f5dab48276
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
5 changed files with 571 additions and 93 deletions

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- esbuild
- sharp

View File

@ -0,0 +1,297 @@
<template>
<settings-card
title="科目管理"
icon="mdi-book-multiple"
: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
variant="text"
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>
<v-btn
variant="text"
prepend-icon="mdi-restore"
:loading="loading"
@click="resetToDefault"
class="mr-2"
>
重置为默认
</v-btn>
</div>
<v-chip
v-if="hasChanges"
color="warning"
variant="elevated"
>
有未保存的更改
</v-chip>
</div>
<!-- 添加新科目 -->
<v-card class="mb-4" variant="outlined">
<v-card-text>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
v-model="newSubjectName"
label="科目名称"
variant="outlined"
density="comfortable"
:rules="[v => !!v || '科目名称不能为空']"
@keyup.enter="addSubject"
append-inner-icon="mdi-plus"
@click:append-inner="addSubject"
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- 科目列表 -->
<v-card variant="outlined">
<v-card-text class="pa-0">
<v-list lines="one">
<v-list-item
v-for="(subject, index) in subjects"
:key="subject.order"
>
<template v-slot:prepend>
<div class="d-flex flex-column align-center mr-2">
<v-btn
icon="mdi-chevron-up"
variant="text"
size="small"
:disabled="index === 0"
@click="moveSubject(index, -1)"
/>
<v-btn
icon="mdi-chevron-down"
variant="text"
size="small"
:disabled="index === subjects.length - 1"
@click="moveSubject(index, 1)"
/>
</div>
</template>
<v-list-item-title>
<v-text-field
v-model="subject.name"
variant="plain"
density="compact"
hide-details
@blur="updateSubject(subject)"
/>
</v-list-item-title>
<template v-slot:append>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
size="small"
@click="deleteSubject(subject)"
/>
</template>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<!-- 底部提示 -->
<v-snackbar
v-model="showSnackbar"
:color="snackbarColor"
:timeout="3000"
>
{{ snackbarText }}
</v-snackbar>
</settings-card>
</template>
<script>
import SettingsCard from '@/components/SettingsCard.vue';
import dataProvider from "@/utils/dataProvider.js";
export default {
name: 'SubjectManagementCard',
components: {
SettingsCard
},
data() {
return {
loading: false,
error: null,
subjects: [],
originalSubjects: null,
newSubjectName: '',
showSnackbar: false,
snackbarText: '',
snackbarColor: 'success',
defaultSubjects: [
{ name: '语文', order: 0 },
{ name: '数学', order: 1 },
{ name: '英语', order: 2 },
{ name: '物理', order: 3 },
{ name: '化学', order: 4 },
{ name: '生物', order: 5 },
{ name: '政治', order: 6 },
{ name: '历史', order: 7 },
{ name: '地理', order: 8 },
{ name: '其他', order: 9 }
]
};
},
computed: {
hasChanges() {
return this.originalSubjects &&
JSON.stringify(this.subjects) !== JSON.stringify(this.originalSubjects);
}
},
created() {
this.loadConfig();
},
methods: {
async loadConfig() {
this.loading = true;
try {
const response = await dataProvider.loadData("classworks-config-subject");
if (response) {
//
this.subjects = response.map((subject, index) => ({
name: subject.name,
order: subject.order ?? index
})).sort((a, b) => a.order - b.order);
this.originalSubjects = JSON.parse(JSON.stringify(this.subjects));
this.showMessage('配置已加载', 'success');
} else {
// 使
this.subjects = [];
this.originalSubjects = [];
this.showMessage('使用默认配置', 'info');
}
} 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-subject", this.subjects);
if (response) {
this.originalSubjects = JSON.parse(JSON.stringify(this.subjects));
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.newSubjectName) return;
const subject = {
name: this.newSubjectName,
order: this.subjects.length
};
this.subjects.push(subject);
this.newSubjectName = '';
},
updateSubject(subject) {
const index = this.subjects.findIndex(s => s.order === subject.order);
if (index > -1) {
this.subjects[index] = { ...subject };
}
},
deleteSubject(subject) {
const index = this.subjects.findIndex(s => s.order === subject.order);
if (index > -1) {
this.subjects.splice(index, 1);
//
this.subjects.forEach((s, i) => {
s.order = i;
});
}
},
moveSubject(index, direction) {
const newIndex = index + direction;
if (newIndex >= 0 && newIndex < this.subjects.length) {
//
const temp = this.subjects[index];
this.subjects[index] = this.subjects[newIndex];
this.subjects[newIndex] = temp;
//
this.subjects.forEach((subject, i) => {
subject.order = i;
});
}
},
resetToDefault() {
this.subjects = JSON.parse(JSON.stringify(this.defaultSubjects));
this.showMessage('已重置为默认科目列表', 'info');
}
}
};
</script>
<style scoped>
.v-list-item {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.v-list-item:last-child {
border-bottom: none;
}
</style>

View File

@ -62,9 +62,9 @@
<v-btn-group divided variant="outlined"> <v-btn-group divided variant="outlined">
<v-btn <v-btn
v-for="subject in unusedSubjects" v-for="subject in unusedSubjects"
:key="subject.key" :key="subject.name"
:disabled="isEditingDisabled" :disabled="isEditingDisabled"
@click="openDialog(subject.key)" @click="openDialog(subject.name)"
> >
<v-icon start> mdi-plus </v-icon> <v-icon start> mdi-plus </v-icon>
{{ subject.name }} {{ subject.name }}
@ -75,11 +75,11 @@
<TransitionGroup name="v-list"> <TransitionGroup name="v-list">
<v-card <v-card
v-for="subject in unusedSubjects" v-for="subject in unusedSubjects"
:key="subject.key" :key="subject.name"
border border
class="empty-subject-card" class="empty-subject-card"
:disabled="isEditingDisabled" :disabled="isEditingDisabled"
@click="openDialog(subject.key)" @click="openDialog(subject.name)"
> >
<v-card-title class="text-subtitle-1"> <v-card-title class="text-subtitle-1">
{{ subject.name }} {{ subject.name }}
@ -635,6 +635,19 @@ export default {
HomeworkEditDialog, HomeworkEditDialog,
}, },
data() { data() {
const defaultSubjects = [
{ name: "语文", order: 0 },
{ name: "数学", order: 1 },
{ name: "英语", order: 2 },
{ name: "物理", order: 3 },
{ name: "化学", order: 4 },
{ name: "生物", order: 5 },
{ name: "政治", order: 6 },
{ name: "历史", order: 7 },
{ name: "地理", order: 8 },
{ name: "其他", order: 9 }
];
return { return {
dataKey: "", dataKey: "",
provider: "", provider: "",
@ -666,34 +679,11 @@ export default {
selectedDate: new Date().toISOString().split("T")[0].replace(/-/g, ''), selectedDate: new Date().toISOString().split("T")[0].replace(/-/g, ''),
selectedDateObj: new Date(), selectedDateObj: new Date(),
refreshInterval: null, refreshInterval: null,
subjectOrder: [
"语文",
"数学",
"英语",
"物理",
"化学",
"生物",
"政治",
"历史",
"地理",
"其他",
],
showNoDataMessage: false, showNoDataMessage: false,
noDataMessage: "", noDataMessage: "",
isToday: false, isToday: false,
attendanceDialog: false, attendanceDialog: false,
availableSubjects: [ availableSubjects: defaultSubjects,
{ key: "语文", name: "语文" },
{ key: "数学", name: "数学" },
{ key: "英语", name: "英语" },
{ key: "物理", name: "物理" },
{ key: "化学", name: "化学" },
{ key: "生物", name: "生物" },
{ key: "政治", name: "政治" },
{ key: "历史", name: "历史" },
{ key: "地理", name: "地理" },
{ key: "其他", name: "其他" },
],
isFullscreen: false, isFullscreen: false,
}, },
loading: { loading: {
@ -750,7 +740,7 @@ export default {
sortedItems() { sortedItems() {
const key = `${JSON.stringify( const key = `${JSON.stringify(
this.state.boardData.homework this.state.boardData.homework
)}_${this.state.subjectOrder.join()}_${this.dynamicSort}`; )}_${this.subjectOrder.join()}_${this.dynamicSort}`;
if (this.sortedItemsCache.key === key) { if (this.sortedItemsCache.key === key) {
return this.sortedItemsCache.value; return this.sortedItemsCache.value;
} }
@ -760,10 +750,10 @@ export default {
.map(([key, value]) => ({ .map(([key, value]) => ({
key, key,
name: name:
this.state.availableSubjects.find((s) => s.key === key)?.name || this.state.availableSubjects.find((s) => s.name === key)?.name ||
key, key,
content: value.content, content: value.content,
order: this.state.subjectOrder.indexOf(key), order: this.subjectOrder.indexOf(key),
rowSpan: Math.ceil( rowSpan: Math.ceil(
(value.content.split("\n").filter((line) => line.trim()).length + (value.content.split("\n").filter((line) => line.trim()).length +
1) * 1) *
@ -783,9 +773,9 @@ export default {
const usedKeys = Object.keys(this.state.boardData.homework).filter( const usedKeys = Object.keys(this.state.boardData.homework).filter(
(key) => this.state.boardData.homework[key].content?.trim() (key) => this.state.boardData.homework[key].content?.trim()
); );
return this.state.availableSubjects.filter( return this.state.availableSubjects
(subject) => !usedKeys.includes(subject.key) .filter(subject => !usedKeys.includes(subject.name))
); .sort((a, b) => a.order - b.order);
}, },
emptySubjects() { emptySubjects() {
if (this.emptySubjectDisplay !== "button") return []; if (this.emptySubjectDisplay !== "button") return [];
@ -911,6 +901,11 @@ export default {
return pinyinA.localeCompare(pinyinB); return pinyinA.localeCompare(pinyinB);
}); });
}, },
subjectOrder() {
return [...this.state.availableSubjects]
.sort((a, b) => a.order - b.order)
.map(subject => subject.name);
},
}, },
watch: { watch: {
@ -1183,6 +1178,7 @@ export default {
async loadConfig() { async loadConfig() {
try { try {
//
try { try {
const response = await dataProvider.loadData("classworks-list-main"); const response = await dataProvider.loadData("classworks-list-main");
@ -1190,7 +1186,6 @@ export default {
this.state.studentList = response.map( this.state.studentList = response.map(
(student) => student.name (student) => student.name
); );
return;
} }
} catch (error) { } catch (error) {
console.warn( console.warn(
@ -1198,6 +1193,18 @@ export default {
error error
); );
} }
//
try {
const subjectsResponse = await dataProvider.loadData("classworks-config-subject");
if (subjectsResponse && Array.isArray(subjectsResponse)) {
//
this.state.availableSubjects = subjectsResponse;
}
} catch (error) {
console.warn("Failed to load subject configuration:", error);
//
}
} catch (error) { } catch (error) {
console.error("加载配置失败:", error); console.error("加载配置失败:", error);
this.$message.error("加载配置失败", error.message); this.$message.error("加载配置失败", error.message);
@ -1225,7 +1232,7 @@ export default {
}; };
} }
this.state.dialogTitle = this.state.dialogTitle =
this.state.availableSubjects.find((s) => s.key === subject)?.name || this.state.availableSubjects.find((s) => s.name === subject)?.name ||
subject; subject;
this.state.textarea = this.state.boardData.homework[subject].content; this.state.textarea = this.state.boardData.homework[subject].content;
this.state.dialogVisible = true; this.state.dialogVisible = true;
@ -1404,44 +1411,6 @@ export default {
})); }));
}, },
fixedGridLayout(items) {
const rowSubjects = [
["语文", "数学", "英语"],
["物理", "化学", "生物"],
["政治", "历史", "地理", "其他"],
];
return items
.sort((a, b) => {
const getRowIndex = (subject) => {
for (let i = 0; i < rowSubjects.length; i++) {
if (rowSubjects[i].includes(subject)) {
return i;
}
}
return rowSubjects.length;
};
const getColumnIndex = (subject) => {
for (const row of rowSubjects) {
const index = row.indexOf(subject);
if (index !== -1) return index;
}
return 999;
};
const rowA = getRowIndex(a.key);
const rowB = getRowIndex(b.key);
if (rowA !== rowB) {
return rowA - rowB;
}
const colA = getColumnIndex(a.key);
const colB = getColumnIndex(b.key);
return colA - colB;
})
.map((item, index) => ({
...item,
order: index,
rowSpan: item.content ? 2 : 1,
}));
},
setAllPresent() { setAllPresent() {
this.state.boardData.attendance = { this.state.boardData.attendance = {

View File

@ -2,12 +2,12 @@
<div class="settings-page"> <div class="settings-page">
<v-app-bar elevation="1"> <v-app-bar elevation="1">
<template #prepend> <template #prepend>
<v-btn <v-btn
icon="mdi-arrow-left" icon="mdi-arrow-left"
variant="text" variant="text"
@click="$router.push('/')" @click="$router.push('/')"
/> <v-btn />
<v-btn
icon="mdi-menu" icon="mdi-menu"
variant="text" variant="text"
@click="drawer = !drawer" @click="drawer = !drawer"
@ -113,7 +113,9 @@
@saved="onSettingsSaved" @saved="onSettingsSaved"
/> />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="student">
<student-list-card border :is-mobile="isMobile" />
</v-tabs-window-item>
<v-tabs-window-item value="share"> <v-tabs-window-item value="share">
<settings-link-generator border class="mt-4" /> <settings-link-generator border class="mt-4" />
</v-tabs-window-item> </v-tabs-window-item>
@ -150,15 +152,15 @@
/> />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="student">
<student-list-card border :is-mobile="isMobile" />
</v-tabs-window-item>
<v-tabs-window-item value="randomPicker"> <v-tabs-window-item value="randomPicker">
<random-picker-card border :is-mobile="isMobile" /> <random-picker-card border :is-mobile="isMobile" />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="homework"> <v-tabs-window-item value="subject">
<subject-management-card border /> <br />
<homework-template-card border /> <homework-template-card border />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="developer" <v-tabs-window-item value="developer"
><settings-card border title="开发者选项" icon="mdi-developer-board"> ><settings-card border title="开发者选项" icon="mdi-developer-board">
<v-list> <v-list>
@ -237,7 +239,8 @@ import SettingsExplorer from "@/components/settings/SettingsExplorer.vue";
import SettingsLinkGenerator from "@/components/SettingsLinkGenerator.vue"; import SettingsLinkGenerator from "@/components/SettingsLinkGenerator.vue";
import NamespaceSettingsCard from "@/components/settings/cards/NamespaceSettingsCard.vue"; import NamespaceSettingsCard from "@/components/settings/cards/NamespaceSettingsCard.vue";
import RandomPickerCard from "@/components/settings/cards/RandomPickerCard.vue"; import RandomPickerCard from "@/components/settings/cards/RandomPickerCard.vue";
import HomeworkTemplateCard from '@/components/settings/cards/HomeworkTemplateCard.vue'; import HomeworkTemplateCard from "@/components/settings/cards/HomeworkTemplateCard.vue";
import SubjectManagementCard from "@/components/settings/cards/SubjectManagementCard.vue";
export default { export default {
name: "Settings", name: "Settings",
components: { components: {
@ -257,6 +260,7 @@ export default {
NamespaceSettingsCard, NamespaceSettingsCard,
RandomPickerCard, RandomPickerCard,
HomeworkTemplateCard, HomeworkTemplateCard,
SubjectManagementCard,
}, },
setup() { setup() {
const { mobile } = useDisplay(); const { mobile } = useDisplay();
@ -359,6 +363,16 @@ export default {
}, },
] ]
: []), : []),
{
title: "科目",
icon: "mdi-book-edit",
value: "subject",
},
{
title: "学生列表",
icon: "mdi-account-group",
value: "student",
},
{ {
title: "分享设置", title: "分享设置",
icon: "mdi-share", icon: "mdi-share",
@ -384,21 +398,13 @@ export default {
icon: "mdi-theme-light-dark", icon: "mdi-theme-light-dark",
value: "theme", value: "theme",
}, },
{
title: "学生列表",
icon: "mdi-account-group",
value: "student",
},
{ {
title: "随机点名", title: "随机点名",
icon: "mdi-dice-multiple", icon: "mdi-dice-multiple",
value: "randomPicker", value: "randomPicker",
}, },
{
title: "作业模板",
icon: "mdi-book-edit",
value: "homework",
},
{ {
title: "开发者", title: "开发者",
icon: "mdi-developer-board", icon: "mdi-developer-board",
@ -425,7 +431,7 @@ export default {
handler(newValue) { handler(newValue) {
this.drawer = !newValue; this.drawer = !newValue;
}, },
immediate: true immediate: true,
}, },
studentData: { studentData: {
handler(newData) { handler(newData) {

File diff suppressed because one or more lines are too long