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:
parent
f2d88437e6
commit
f5dab48276
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@parcel/watcher'
|
||||
- esbuild
|
||||
- sharp
|
297
src/components/settings/cards/SubjectManagementCard.vue
Normal file
297
src/components/settings/cards/SubjectManagementCard.vue
Normal 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>
|
@ -62,9 +62,9 @@
|
||||
<v-btn-group divided variant="outlined">
|
||||
<v-btn
|
||||
v-for="subject in unusedSubjects"
|
||||
:key="subject.key"
|
||||
:key="subject.name"
|
||||
:disabled="isEditingDisabled"
|
||||
@click="openDialog(subject.key)"
|
||||
@click="openDialog(subject.name)"
|
||||
>
|
||||
<v-icon start> mdi-plus </v-icon>
|
||||
{{ subject.name }}
|
||||
@ -75,11 +75,11 @@
|
||||
<TransitionGroup name="v-list">
|
||||
<v-card
|
||||
v-for="subject in unusedSubjects"
|
||||
:key="subject.key"
|
||||
:key="subject.name"
|
||||
border
|
||||
class="empty-subject-card"
|
||||
:disabled="isEditingDisabled"
|
||||
@click="openDialog(subject.key)"
|
||||
@click="openDialog(subject.name)"
|
||||
>
|
||||
<v-card-title class="text-subtitle-1">
|
||||
{{ subject.name }}
|
||||
@ -635,6 +635,19 @@ export default {
|
||||
HomeworkEditDialog,
|
||||
},
|
||||
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 {
|
||||
dataKey: "",
|
||||
provider: "",
|
||||
@ -666,34 +679,11 @@ export default {
|
||||
selectedDate: new Date().toISOString().split("T")[0].replace(/-/g, ''),
|
||||
selectedDateObj: new Date(),
|
||||
refreshInterval: null,
|
||||
subjectOrder: [
|
||||
"语文",
|
||||
"数学",
|
||||
"英语",
|
||||
"物理",
|
||||
"化学",
|
||||
"生物",
|
||||
"政治",
|
||||
"历史",
|
||||
"地理",
|
||||
"其他",
|
||||
],
|
||||
showNoDataMessage: false,
|
||||
noDataMessage: "",
|
||||
isToday: false,
|
||||
attendanceDialog: false,
|
||||
availableSubjects: [
|
||||
{ key: "语文", name: "语文" },
|
||||
{ key: "数学", name: "数学" },
|
||||
{ key: "英语", name: "英语" },
|
||||
{ key: "物理", name: "物理" },
|
||||
{ key: "化学", name: "化学" },
|
||||
{ key: "生物", name: "生物" },
|
||||
{ key: "政治", name: "政治" },
|
||||
{ key: "历史", name: "历史" },
|
||||
{ key: "地理", name: "地理" },
|
||||
{ key: "其他", name: "其他" },
|
||||
],
|
||||
availableSubjects: defaultSubjects,
|
||||
isFullscreen: false,
|
||||
},
|
||||
loading: {
|
||||
@ -750,7 +740,7 @@ export default {
|
||||
sortedItems() {
|
||||
const key = `${JSON.stringify(
|
||||
this.state.boardData.homework
|
||||
)}_${this.state.subjectOrder.join()}_${this.dynamicSort}`;
|
||||
)}_${this.subjectOrder.join()}_${this.dynamicSort}`;
|
||||
if (this.sortedItemsCache.key === key) {
|
||||
return this.sortedItemsCache.value;
|
||||
}
|
||||
@ -760,10 +750,10 @@ export default {
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
name:
|
||||
this.state.availableSubjects.find((s) => s.key === key)?.name ||
|
||||
this.state.availableSubjects.find((s) => s.name === key)?.name ||
|
||||
key,
|
||||
content: value.content,
|
||||
order: this.state.subjectOrder.indexOf(key),
|
||||
order: this.subjectOrder.indexOf(key),
|
||||
rowSpan: Math.ceil(
|
||||
(value.content.split("\n").filter((line) => line.trim()).length +
|
||||
1) *
|
||||
@ -783,9 +773,9 @@ export default {
|
||||
const usedKeys = Object.keys(this.state.boardData.homework).filter(
|
||||
(key) => this.state.boardData.homework[key].content?.trim()
|
||||
);
|
||||
return this.state.availableSubjects.filter(
|
||||
(subject) => !usedKeys.includes(subject.key)
|
||||
);
|
||||
return this.state.availableSubjects
|
||||
.filter(subject => !usedKeys.includes(subject.name))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
},
|
||||
emptySubjects() {
|
||||
if (this.emptySubjectDisplay !== "button") return [];
|
||||
@ -911,6 +901,11 @@ export default {
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
},
|
||||
subjectOrder() {
|
||||
return [...this.state.availableSubjects]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(subject => subject.name);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -1183,6 +1178,7 @@ export default {
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
// 加载学生列表
|
||||
try {
|
||||
const response = await dataProvider.loadData("classworks-list-main");
|
||||
|
||||
@ -1190,7 +1186,6 @@ export default {
|
||||
this.state.studentList = response.map(
|
||||
(student) => student.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
@ -1198,6 +1193,18 @@ export default {
|
||||
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) {
|
||||
console.error("加载配置失败:", error);
|
||||
this.$message.error("加载配置失败", error.message);
|
||||
@ -1225,7 +1232,7 @@ export default {
|
||||
};
|
||||
}
|
||||
this.state.dialogTitle =
|
||||
this.state.availableSubjects.find((s) => s.key === subject)?.name ||
|
||||
this.state.availableSubjects.find((s) => s.name === subject)?.name ||
|
||||
subject;
|
||||
this.state.textarea = this.state.boardData.homework[subject].content;
|
||||
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() {
|
||||
this.state.boardData.attendance = {
|
||||
|
@ -2,12 +2,12 @@
|
||||
<div class="settings-page">
|
||||
<v-app-bar elevation="1">
|
||||
<template #prepend>
|
||||
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.push('/')"
|
||||
/> <v-btn
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-menu"
|
||||
variant="text"
|
||||
@click="drawer = !drawer"
|
||||
@ -113,7 +113,9 @@
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
</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">
|
||||
<settings-link-generator border class="mt-4" />
|
||||
</v-tabs-window-item>
|
||||
@ -150,15 +152,15 @@
|
||||
/>
|
||||
</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">
|
||||
<random-picker-card border :is-mobile="isMobile" />
|
||||
</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 />
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="developer"
|
||||
><settings-card border title="开发者选项" icon="mdi-developer-board">
|
||||
<v-list>
|
||||
@ -237,7 +239,8 @@ 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';
|
||||
import HomeworkTemplateCard from "@/components/settings/cards/HomeworkTemplateCard.vue";
|
||||
import SubjectManagementCard from "@/components/settings/cards/SubjectManagementCard.vue";
|
||||
export default {
|
||||
name: "Settings",
|
||||
components: {
|
||||
@ -257,6 +260,7 @@ export default {
|
||||
NamespaceSettingsCard,
|
||||
RandomPickerCard,
|
||||
HomeworkTemplateCard,
|
||||
SubjectManagementCard,
|
||||
},
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
@ -359,6 +363,16 @@ export default {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "科目",
|
||||
icon: "mdi-book-edit",
|
||||
value: "subject",
|
||||
},
|
||||
{
|
||||
title: "学生列表",
|
||||
icon: "mdi-account-group",
|
||||
value: "student",
|
||||
},
|
||||
{
|
||||
title: "分享设置",
|
||||
icon: "mdi-share",
|
||||
@ -384,21 +398,13 @@ export default {
|
||||
icon: "mdi-theme-light-dark",
|
||||
value: "theme",
|
||||
},
|
||||
{
|
||||
title: "学生列表",
|
||||
icon: "mdi-account-group",
|
||||
value: "student",
|
||||
},
|
||||
|
||||
{
|
||||
title: "随机点名",
|
||||
icon: "mdi-dice-multiple",
|
||||
value: "randomPicker",
|
||||
},
|
||||
{
|
||||
title: "作业模板",
|
||||
icon: "mdi-book-edit",
|
||||
value: "homework",
|
||||
},
|
||||
|
||||
{
|
||||
title: "开发者",
|
||||
icon: "mdi-developer-board",
|
||||
@ -425,7 +431,7 @@ export default {
|
||||
handler(newValue) {
|
||||
this.drawer = !newValue;
|
||||
},
|
||||
immediate: true
|
||||
immediate: true,
|
||||
},
|
||||
studentData: {
|
||||
handler(newData) {
|
||||
|
202
vite.config.mjs.timestamp-1751696302207-c76e023be42d5.mjs
Normal file
202
vite.config.mjs.timestamp-1751696302207-c76e023be42d5.mjs
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user