Commit fd870ca4 authored by Administrator's avatar Administrator
Browse files

added multiple feat

UI: add Settings skeleton page with v-tabs + 6 categories
UI: enlarge TaskView event fonts and replace progress bar with SVG ring
UI: add native title tooltip on TaskView events
UI: convert filter panel toggle to floating icon button with slide animation
UI: set browser tab title to "学生管理系统"
parent 1e2ed63c
......@@ -40,3 +40,4 @@ clipboard*
*.yaml
暑假日程表.txt
import-assignments.cjs
to-do-feature.md
\ No newline at end of file
......@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>学生管理系统</title>
</head>
<body>
<div id="app"></div>
......
......@@ -49,5 +49,11 @@ export const GLOBAL_MENU_ITEMS = [
mdiIcon: 'mdi-clipboard-check-multiple-outline',
hasRouter: true
},
{
nameKey: 'navigation.menu.settings',
path: '/settings',
mdiIcon: 'mdi-cog-outline',
hasRouter: true
},
// 可以继续添加更多菜单项
];
......@@ -11,6 +11,7 @@ import task from './modules/task'
import story from './modules/story'
import auth from './modules/auth'
import navigation from './modules/navigation'
import settings from './modules/settings'
import appHeader from './components/app-header'
// 组合所有翻译
......@@ -29,6 +30,7 @@ export default {
story,
auth,
navigation,
settings,
components: {
appHeader
}
......
......@@ -27,7 +27,8 @@ export default {
terms: 'Terms',
stories: 'Story',
tasks: 'Task Management',
profile: 'Profile'
profile: 'Profile',
settings: 'Settings'
},
drawer: {
......
export default {
title: 'Settings',
subtitle: 'Manage your preferences and account',
// 分类 tab 标签
categories: {
general: { label: 'General', description: 'Language, region, basic preferences' },
appearance: { label: 'Appearance', description: 'Theme, density, color scheme' },
notifications: { label: 'Notifications', description: 'Reminders, alerts, sound' },
data: { label: 'Data', description: 'Import, export, cache, reset' },
account: { label: 'Account', description: 'Profile, password, devices' },
about: { label: 'About', description: 'Version, licenses, acknowledgements' }
},
// 顶部动作
actions: {
reset: 'Reset to defaults',
save: 'Save changes'
},
// 占位说明(各 tab 卡片复用)
placeholders: {
comingSoon: 'This section is not implemented yet (phase-1 skeleton).'
},
// 各分类的字段(phase-1 仅占位)
fields: {
language: 'Display language',
timezone: 'Time zone',
theme: 'Theme',
density: 'Interface density',
weekStart: 'Week starts on',
reminderLeadTime: 'Default reminder lead time',
exportFormat: 'Default export format',
autoBackup: 'Auto-backup enabled'
},
messages: {
phase1Hint: 'Skeleton only — values are not persisted yet.',
resetConfirm: 'Reset all settings to defaults?'
}
}
\ No newline at end of file
......@@ -14,7 +14,16 @@ export default {
},
noEvents: 'No tasks this month',
enterFullscreen: 'Enter Fullscreen',
exitFullscreen: 'Exit Fullscreen'
exitFullscreen: 'Exit Fullscreen',
tooltip: {
progress: 'Progress: {percent}%'
},
buttonText: {
today: 'Today',
month: 'Month',
week: 'Week',
day: 'Day'
}
},
task: {
name: 'Task Name',
......@@ -57,6 +66,13 @@ export default {
title: 'Edit Task',
createTitle: 'Create New Task'
},
dialog: {
delete: 'Delete Confirmation'
},
deleteConfirmation: {
message: 'Are you sure you want to delete this task? This action cannot be undone.',
warning: 'This action is irreversible.'
},
messages: {
loadError: 'Failed to load task data',
noData: 'No task data available',
......
......@@ -11,6 +11,7 @@ import task from './modules/task'
import story from './modules/story'
import auth from './modules/auth'
import navigation from './modules/navigation'
import settings from './modules/settings'
import appHeader from './components/app-header'
// 组合所有翻译
......@@ -29,6 +30,7 @@ export default {
story,
auth,
navigation,
settings,
components: {
appHeader
}
......
......@@ -27,7 +27,8 @@ export default {
terms: '学期',
stories: '系列任务',
tasks: '作业管理',
profile: '个人资料'
profile: '个人资料',
settings: '设置'
},
drawer: {
......
export default {
title: '设置',
subtitle: '管理您的偏好与账户',
// 分类 tab 标签
categories: {
general: { label: '通用', description: '语言、区域、基本偏好' },
appearance: { label: '外观', description: '主题、密度、配色' },
notifications: { label: '通知', description: '提醒、弹窗、声音' },
data: { label: '数据', description: '导入、导出、缓存、重置' },
account: { label: '账户', description: '资料、密码、设备' },
about: { label: '关于', description: '版本、许可、致谢' }
},
// 顶部动作
actions: {
reset: '恢复默认',
save: '保存修改'
},
// 占位说明
placeholders: {
comingSoon: '本分区尚未实现(phase-1 骨架)。'
},
// 各分类的字段
fields: {
language: '显示语言',
timezone: '时区',
theme: '主题',
density: '界面密度',
weekStart: '一周起始日',
reminderLeadTime: '默认提醒提前时间',
exportFormat: '默认导出格式',
autoBackup: '启用自动备份'
},
messages: {
phase1Hint: '当前仅为骨架,暂不会持久化。',
resetConfirm: '确定要把所有设置恢复为默认值吗?'
}
}
\ No newline at end of file
......@@ -14,7 +14,16 @@ export default {
},
noEvents: '本月没有作业',
enterFullscreen: '全屏显示',
exitFullscreen: '退出全屏'
exitFullscreen: '退出全屏',
tooltip: {
progress: '进度: {percent}%'
},
buttonText: {
today: '今天',
month: '',
week: '',
day: ''
}
},
task: {
name: '作业名称',
......@@ -57,6 +66,13 @@ export default {
title: '编辑作业',
createTitle: '新增作业'
},
dialog: {
delete: '删除确认'
},
deleteConfirmation: {
message: '您确定要删除此作业吗?此操作无法撤销。',
warning: '此操作不可撤销,请谨慎。'
},
messages: {
loadError: '加载作业数据失败',
noData: '暂无作业数据',
......
......@@ -9,6 +9,7 @@ import SubjectView from '../views/SubjectView.vue'
import TermView from '../views/TermView.vue'
import StoryView from '../views/StoryView.vue'
import TaskView from '../views/TaskView.vue'
import SettingsView from '../views/SettingsView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
......@@ -138,6 +139,23 @@ const router = createRouter({
]
},
},
{
path: '/settings',
name: 'settings',
component: SettingsView,
meta: {
requiresAuth: true,
layout: 'default',
title: 'Settings',
breadcrumb: {
key: 'navigation.breadcrumb.settings'
},
breadcrumbPath: [
{ key: 'navigation.breadcrumb.home', to: '/', disabled: false },
{ key: 'navigation.breadcrumb.settings', to: '/settings', disabled: true }
]
},
},
{
path: '/:pathMatch(.*)*',
redirect: '/',
......
<script setup>
import { ref } from 'vue'
// 分类配置(phase-1: 数组驱动 tabs; phase-2: 改为 { key, icon, component })
const categories = [
{ key: 'general', icon: 'mdi-tune-variant' },
{ key: 'appearance', icon: 'mdi-palette-outline' },
{ key: 'notifications', icon: 'mdi-bell-outline' },
{ key: 'data', icon: 'mdi-database-outline' },
{ key: 'account', icon: 'mdi-account-cog-outline' },
{ key: 'about', icon: 'mdi-information-outline' }
]
const activeTab = ref('general')
// phase-1 占位状态; phase-2 替换为 usePreferencesStore()
const general = ref({ language: 'zh-CN', timezone: 'Asia/Shanghai' })
const appearance = ref({ theme: 'system', density: 'comfortable' })
const notifications = ref({ weekStart: 1, reminderLeadTime: 15 })
const data = ref({ exportFormat: 'xlsx', autoBackup: true })
</script>
<template>
<v-container fluid>
<!-- 页面标题区(与 HomeView/TaskView 一致) -->
<v-row class="mb-4">
<v-col cols="12">
<h2>{{ $t('settings.title') }}</h2>
<p class="text-body-2 text-medium-emphasis mt-1">
{{ $t('settings.subtitle') }}
</p>
</v-col>
</v-row>
<v-card>
<!-- 分类 tabs -->
<v-tabs v-model="activeTab" show-arrows density="comfortable">
<v-tab v-for="cat in categories" :key="cat.key" :value="cat.key">
<v-icon class="me-2" :icon="cat.icon" />
{{ $t(`settings.categories.${cat.key}.label`) }}
</v-tab>
</v-tabs>
<v-divider />
<!-- 分类内容 -->
<v-window v-model="activeTab">
<v-window-item v-for="cat in categories" :key="cat.key" :value="cat.key">
<v-card flat>
<v-card-title>
{{ $t(`settings.categories.${cat.key}.label`) }}
</v-card-title>
<v-card-subtitle>
{{ $t(`settings.categories.${cat.key}.description`) }}
</v-card-subtitle>
<v-card-text>
<p class="text-body-2 text-medium-emphasis mb-4">
{{ $t('settings.placeholders.comingSoon') }}
</p>
<v-row dense>
<!-- general -->
<template v-if="cat.key === 'general'">
<v-col cols="12" md="6">
<v-select
v-model="general.language"
:items="[
{ title: '中文', value: 'zh-CN' },
{ title: 'English', value: 'en' }
]"
:label="$t('settings.fields.language')"
variant="outlined"
density="comfortable"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="general.timezone"
:label="$t('settings.fields.timezone')"
variant="outlined"
density="comfortable"
/>
</v-col>
</template>
<!-- appearance -->
<template v-else-if="cat.key === 'appearance'">
<v-col cols="12" md="6">
<v-select
v-model="appearance.theme"
:items="['system', 'light', 'dark']"
:label="$t('settings.fields.theme')"
variant="outlined"
density="comfortable"
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="appearance.density"
:items="['comfortable', 'compact', 'default']"
:label="$t('settings.fields.density')"
variant="outlined"
density="comfortable"
/>
</v-col>
</template>
<!-- notifications -->
<template v-else-if="cat.key === 'notifications'">
<v-col cols="12" md="6">
<v-select
v-model="notifications.weekStart"
:items="[0, 1, 6]"
:label="$t('settings.fields.weekStart')"
variant="outlined"
density="comfortable"
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model.number="notifications.reminderLeadTime"
type="number"
:label="$t('settings.fields.reminderLeadTime')"
variant="outlined"
density="comfortable"
/>
</v-col>
</template>
<!-- data -->
<template v-else-if="cat.key === 'data'">
<v-col cols="12" md="6">
<v-select
v-model="data.exportFormat"
:items="['xlsx', 'csv', 'json']"
:label="$t('settings.fields.exportFormat')"
variant="outlined"
density="comfortable"
/>
</v-col>
<v-col cols="12" md="6" class="d-flex align-center">
<v-switch
v-model="data.autoBackup"
:label="$t('settings.fields.autoBackup')"
color="primary"
hide-details
/>
</v-col>
</template>
<!-- account: 占位 -->
<template v-else-if="cat.key === 'account'">
<v-col cols="12">
<v-alert type="info" variant="tonal">
{{ $t('settings.placeholders.comingSoon') }}
</v-alert>
</v-col>
</template>
<!-- about -->
<template v-else-if="cat.key === 'about'">
<v-col cols="12">
<v-list>
<v-list-item title="Version" subtitle="0.1.0 (phase-1)" />
<v-list-item title="Vue" subtitle="3.x" />
<v-list-item title="Vuetify" subtitle="3.x" />
</v-list>
</v-col>
</template>
</v-row>
</v-card-text>
</v-card>
</v-window-item>
</v-window>
<!-- 底部共享按钮: 跨所有 tab, 只显示一对 Reset / Save -->
<v-divider />
<v-card-actions>
<v-spacer />
<v-btn variant="text" color="error">
{{ $t('settings.actions.reset') }}
</v-btn>
<v-btn color="primary" variant="elevated">
{{ $t('settings.actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
\ No newline at end of file
......@@ -9,6 +9,17 @@
<!-- 筛选条件和日历的左右布局 -->
<div class="d-flex task-layout">
<!-- 浮动折叠按钮:贴在筛选面板右边缘中段, 折叠时贴左边缘 -->
<v-btn
class="filter-panel-toggle"
:class="{ 'filter-panel-toggle-collapsed': !isFilterExpanded }"
:icon="isFilterExpanded ? 'mdi-chevron-left' : 'mdi-chevron-right'"
variant="elevated"
color="primary"
size="small"
@click="toggleFilterPanel"
:title="isFilterExpanded ? $t('task.filters.collapse') : $t('task.filters.expand')"
/>
<!-- 左侧:筛选条件面板 -->
<div
class="filter-panel-container"
......@@ -127,16 +138,6 @@
<v-card-title class="d-flex justify-space-between align-center">
<span>{{ $t('task.calendar.title') }}</span>
<div class="d-flex gap-1">
<!-- 筛选面板切换按钮 -->
<v-btn
:icon="isFilterExpanded ? 'mdi-filter-minus' : 'mdi-filter-plus'"
variant="outlined"
color="primary"
size="small"
@click="toggleFilterPanel"
:title="isFilterExpanded ? $t('task.filters.collapse') : $t('task.filters.expand')"
:disabled="isLoading"
/>
<!-- 全屏切换按钮 -->
<v-btn
:icon="isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
......@@ -184,72 +185,6 @@
</div>
</div>
<!-- 任务详情对话框 -->
<v-dialog v-model="taskDetailDialog" max-width="600">
<v-card v-if="selectedTask">
<v-card-title>
<span>{{ selectedTask.task_name }}</span>
<v-spacer />
<v-btn
icon="mdi-close"
variant="text"
@click="taskDetailDialog = false"
/>
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="6">
<v-list-item>
<v-list-item-title>{{ $t('task.task.subject') }}</v-list-item-title>
<v-list-item-subtitle>{{ getSubjectName(selectedTask.subject_id) }}</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="6">
<v-list-item>
<v-list-item-title>{{ $t('task.task.student') }}</v-list-item-title>
<v-list-item-subtitle>{{ getStudentName(selectedTask.student_id) }}</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="6">
<v-list-item>
<v-list-item-title>{{ $t('task.task.startDate') }}</v-list-item-title>
<v-list-item-subtitle>{{ formatDate(selectedTask.start_date) }}</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="6">
<v-list-item>
<v-list-item-title>{{ $t('task.task.endDate') }}</v-list-item-title>
<v-list-item-subtitle>{{ formatDate(selectedTask.end_date) }}</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12">
<v-list-item>
<v-list-item-title>{{ $t('task.task.completionPercent') }}</v-list-item-title>
<v-list-item-subtitle>
<v-progress-linear
:model-value="selectedTask.completion_percent"
:color="getCompletionColor(selectedTask.completion_percent)"
height="20"
rounded
>
<template #default="{ value }">
<strong>{{ Math.ceil(value) }}%</strong>
</template>
</v-progress-linear>
</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col cols="12" v-if="selectedTask.task_description">
<v-list-item>
<v-list-item-title>{{ $t('task.task.description') }}</v-list-item-title>
<v-list-item-subtitle>{{ selectedTask.task_description }}</v-list-item-subtitle>
</v-list-item>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<!-- 任务编辑对话框 -->
<v-dialog v-model="editDialog" max-width="600">
<v-card class="edit-task-card">
......@@ -588,65 +523,6 @@
</v-card-text>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog
v-model="deleteDialog"
max-width="500px"
persistent
>
<v-card>
<v-card-title class="text-h5 bg-error text-white">
{{ $t('common.messages.dialogs.deleteConfirmation') }}
</v-card-title>
<v-card-text class="pt-4">
<div v-if="isError" class="mb-4">
<v-alert
type="error"
variant="tonal"
closable
class="mb-2"
>
{{ errorMessage }}
</v-alert>
</div>
<p class="text-body-1 mb-4">{{ $t('task.messages.deleteConfirmation') }}</p>
<v-card variant="outlined" class="mb-4">
<v-card-item>
<v-card-title>{{ taskToDelete?.task_name }}</v-card-title>
</v-card-item>
</v-card>
<v-alert
type="warning"
variant="tonal"
icon="mdi-alert-circle"
class="mb-0"
>
{{ $t('common.messages.deleteWarning') }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
@click="closeDeleteDialog"
variant="outlined"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="error"
variant="flat"
:loading="isDeleting"
@click="confirmDeleteTask"
>
{{ $t('common.buttons.delete') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
......@@ -654,7 +530,7 @@
import { onMounted, ref, computed, watch, onUnmounted, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { getTasks, getTaskById, getTaskImage, updateTask as updateTaskAPI, deleteTask } from '@/api/taskService.js'
import { getTasks, getTaskImage, updateTask as updateTaskAPI, deleteTask } from '@/api/taskService.js'
import { getStudents } from '@/api/studentService.js'
import { getSubjects } from '@/api/subjectService.js'
import { getTerms } from '@/api/termService.js'
......@@ -671,14 +547,16 @@ defineOptions({
}
})
const { t } = useI18n()
const { t, locale: i18nLocale } = useI18n()
const authStore = useAuthStore()
// FullCalendar 使用 'en' / 'zh-cn'(小写),与项目内部 'zh-CN' 略有差异
const fcLocale = computed(() => (i18nLocale.value === 'zh-CN' ? 'zh-cn' : 'en'))
// 基础状态
const isLoading = ref(false)
const isError = ref(false)
const errorMessage = ref('')
const successMessage = ref('')
// 删除功能相关状态
const deleteDialog = ref(false)
......@@ -687,10 +565,6 @@ const isDeleting = ref(false)
const tasks = ref([])
// 日历相关状态
const calendar = ref(null)
const selectedDate = ref(new Date())
const taskDetailDialog = ref(false)
const selectedTask = ref(null)
const isFullscreen = ref(false)
// 筛选相关状态
......@@ -774,18 +648,17 @@ const calendarOptions = computed(() => ({
height: 'parent',
expandRows: true,
firstDay: 0,
locale: 'zh-cn',
buttonText: { today: '今天' },
locale: fcLocale.value,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,dayGridWeek,dayGridDay'
},
buttonText: {
today: '今天',
month: '',
week: '',
day: ''
today: t('task.calendar.buttonText.today'),
month: t('task.calendar.buttonText.month'),
week: t('task.calendar.buttonText.week'),
day: t('task.calendar.buttonText.day')
},
editable: true,
eventDurationEditable: true,
......@@ -822,6 +695,15 @@ const calendarOptions = computed(() => ({
}
return result
})(),
// hover tooltip: 作业名 + 描述 + 进度。原生 title 属性, 浏览器自动渲染气泡。
eventDidMount: (arg) => {
const taskData = arg.event.extendedProps
const lines = [arg.event.title]
if (taskData.description) lines.push(taskData.description)
const pct = taskData.completion_percent || 0
if (pct > 0) lines.push(t('task.calendar.tooltip.progress', { percent: pct }))
arg.el.title = lines.join('\n')
},
// 自定义事件渲染
eventContent: (arg) => {
const taskData = arg.event.extendedProps
......@@ -830,16 +712,31 @@ const calendarOptions = computed(() => ({
const subjectShort = getSubjectShortName(taskData.subject_id) || ''
const compPct = taskData.completion_percent || 0
// 学科背景色由后端给(深浅不定),根据亮度算文字色:暗底配白字、亮底配黑字。
// 在 .fc-event-custom 上设 color,所有文字子元素继承。
const subjectBg = getSubjectColor(taskData.subject_id)
const textColor = getTextColor(subjectBg)
// 进度环:1-99% 画弧,100% 用对勾。0% 不显示。
// 弧长 = 2πr,stroke-dashoffset 控制已绘制比例;rotate(-90) 让起点在 12 点钟方向。
let pctIcon = ''
if (compPct >= 100) {
pctIcon = `<svg class="fc-event-ring" width="11" height="11" viewBox="0 0 24 24" fill="currentColor" aria-label="completed"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`
} else if (compPct > 0) {
const r = 5
const c = 2 * Math.PI * r
const offset = c * (1 - compPct / 100)
pctIcon = `<svg class="fc-event-ring" width="11" height="11" viewBox="0 0 12 12" aria-label="${compPct}%"><circle cx="6" cy="6" r="${r}" fill="none" stroke="currentColor" stroke-opacity="0.3" stroke-width="1.5"/><circle cx="6" cy="6" r="${r}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${offset.toFixed(2)}" transform="rotate(-90 6 6)"/></svg>`
}
return {
html: `
<div class="fc-event-custom" data-task-id="${arg.event.id}">
<div class="fc-event-custom" data-task-id="${arg.event.id}" style="color:${textColor}">
<div class="fc-event-content">
<span class="fc-event-avatar" style="background:${getStudentColor(taskData.student_id)};color:${getTextColor(getStudentColor(taskData.student_id))}">${initial}</span>
<span class="fc-event-subject">[${subjectShort}]</span>
<span class="fc-event-title">${arg.event.title}</span>
</div>
<div class="fc-event-bar">
<div class="fc-event-fill" style="width:${compPct}%;background:#4CAF50"></div>
<span class="fc-event-pct">${pctIcon}</span>
</div>
</div>
`
......@@ -922,14 +819,6 @@ const getTextColor = (backgroundColor) => {
return '#FFFFFF'
}
// 获取完成度对应的颜色
const getCompletionColor = (percent) => {
if (percent >= 100) return 'success'
if (percent >= 70) return 'info'
if (percent >= 30) return 'warning'
return 'error'
}
// 格式化日期
const formatDate = (date) => {
if (!date) return ''
......@@ -972,12 +861,6 @@ const getStudentColor = (studentId) => {
return COLOR_MAPPING[colorKeys[colorIndex]] || '#9E9E9E'
}
// 获取学科名称
const getSubjectName = (subjectId) => {
const subject = subjects.value.find(s => s.subject_id === subjectId)
return subject ? subject.subject_name : `Subject ${subjectId}`
}
// 获取学科简称(取学科名称的第一个字符)
const getSubjectShortName = (subjectId) => {
const subject = subjects.value.find(s => s.subject_id === subjectId)
......@@ -1100,34 +983,26 @@ const fetchStories = async () => {
}
// 事件处理函数
const onEventClick = async (event) => {
const onEventClick = (event) => {
console.log('Calendar event clicked:', event)
// 从事件中获取任务ID
const taskId = event.taskData?.id || event.id
if (taskId) {
try {
// 显示加载状态
isLoading.value = true
// 从后端获取完整的任务详情
const taskDetail = await getTaskById(taskId)
// 打开编辑对话框
openEditDialog(taskDetail)
} catch (error) {
console.error('Failed to fetch task detail:', error)
// 显示错误消息
errorMessage.value = error?.message || t('task.messages.loadError')
// 列表接口已经返回编辑对话框所需的全量字段,直接复用缓存避免一次额外的请求。
// 之前这里会 await getTaskById 并 toggle isLoading,导致日历上的 loading-overlay 闪一下。
const cachedTask = tasks.value.find((t) => t.task_id === taskId)
if (!cachedTask) {
console.error('Task not found in local cache:', taskId)
errorMessage.value = t('task.messages.loadError')
isError.value = true
setTimeout(() => {
isError.value = false
errorMessage.value = ''
}, 3000)
} finally {
// 隐藏加载状态
isLoading.value = false
return
}
openEditDialog(cachedTask)
} else {
console.error('Task ID not found for event:', event)
}
......@@ -1136,26 +1011,13 @@ const onEventClick = async (event) => {
const onDateClick = (date) => {
console.log('onDateClick called with date:', date);
// v-calendar的click:date事件应该传递日期对象,而不是点击事件
// 但如果收到的是点击事件,我们需要处理这种情况
if (date) {
if (date.target) {
// 这是一个DOM事件对象,而不是日期
console.log('Received DOM event instead of date, using current date');
selectedDate.value = new Date();
// 传递null给openCreateDialog,它会使用当前日期
openCreateDialog(null);
} else {
// 这是一个正常的日期对象
// FullCalendar 的 dateClick 始终传入日期字符串
// 若收到 DOM 事件或无效参数,使用当前日期
if (date && !date.target) {
console.log('Received valid date object');
selectedDate.value = date;
// 点击日历空白处,打开新增对话框
openCreateDialog(date);
}
} else {
// 如果没有收到日期参数,使用当前日期
console.log('No date received, using current date');
selectedDate.value = new Date();
console.log('No/invalid date received, using current date');
openCreateDialog(null);
}
}
......@@ -1173,7 +1035,7 @@ const toggleFilterPanel = () => {
// 无对话框时 Esc 才退出全屏
const handleKeydown = (event) => {
if (event.key !== 'Escape') return
if (editDialog.value || deleteDialog.value || imageDialog.value || taskDetailDialog.value) return
if (editDialog.value || deleteDialog.value || imageDialog.value) return
if (isFullscreen.value) {
isFullscreen.value = false
}
......@@ -1197,56 +1059,12 @@ const clearFilters = async () => {
await fetchTasks({})
}
// 学生选择变化处理函数
const onStudentChange = async (studentId) => {
filters.value.studentId = studentId
// cascade: 学生变了 → 重置学期和系列任务
filters.value.termId = null
filters.value.storyId = null
console.log('Student changed to:', studentId, 'Term & Story reset')
// 自动应用筛选
if (studentId || filters.value.subjectId) {
await fetchTasks()
}
}
// 学科选择变化处理函数
const onSubjectChange = async (subjectId) => {
filters.value.subjectId = subjectId
// cascade: 学科变了 → 重置系列任务
filters.value.storyId = null
console.log('Subject changed to:', subjectId, 'Story reset')
// 自动应用筛选
if (subjectId || filters.value.studentId || filters.value.termId) {
await fetchTasks()
}
}
// 学期选择变化处理函数
const onTermChange = async (termId) => {
filters.value.termId = termId
// cascade: 学期变了 → 重置系列任务
filters.value.storyId = null
console.log('Term changed to:', termId, 'Story reset')
// 自动应用筛选
if (termId || filters.value.studentId || filters.value.subjectId) {
await fetchTasks()
}
}
// 系列任务选择变化处理函数
const onStoryChange = async (storyId) => {
filters.value.storyId = storyId
console.log('Story changed to:', storyId)
// 自动应用筛选
if (storyId || filters.value.studentId || filters.value.subjectId || filters.value.termId) {
await fetchTasks()
}
}
// 筛选面板处理器:只负责自动应用筛选;
// 子项重置由 watcher(line 634+)统一处理,避免重复
const onStudentChange = () => fetchTasks()
const onSubjectChange = () => fetchTasks()
const onTermChange = () => fetchTasks()
const onStoryChange = () => fetchTasks()
......@@ -1273,7 +1091,12 @@ const editForm = ref({
image_04: null,
image_04_mime_type: null,
image_05: null,
image_05_mime_type: null
image_05_mime_type: null,
delete_image_01: false,
delete_image_02: false,
delete_image_03: false,
delete_image_04: false,
delete_image_05: false
})
const isSaving = ref(false)
const editFormRef = ref(null)
......@@ -1405,34 +1228,16 @@ const openEditDialog = async (task) => {
if (task && task.task_id) {
// 使用setTimeout让对话框先显示出来,然后再加载图片
setTimeout(() => {
// 尝试预加载5个图片位置的图片
// 仅当该槽位确实有图时才去拉,否则会触发一堆 404 + 5 个 spinner 一起转的视觉噪音。
for (let i = 1; i <= 5; i++) {
// 检查是否有key或etag信息,如果有则尝试加载图片
const imageKey = `image_0${i}`;
const imageKeyField = `${imageKey}_key`;
const imageEtagField = `${imageKey}_etag`;
// 添加调试日志
console.log(`Checking image ${i} for preloading:`, {
hasKey: !!task[imageKeyField],
hasEtag: !!task[imageEtagField],
key: task[imageKeyField],
etag: task[imageEtagField]
});
const slotKey = `image_0${i}`
if (!editForm.value[slotKey]) continue
// 尝试加载图片,无论是否有key或etag信息
loadTaskImage(task.task_id, i)
.then(() => {
console.log(`Image ${i} preloaded successfully`);
loadTaskImage(task.task_id, i).catch((error) => {
// 404 表示该槽位没有图,正常情况
if (error?.response?.status === 404) return
console.error(`Failed to preload image ${i}:`, error)
})
.catch(error => {
// 如果是404错误,说明该索引位置没有图片,这是正常的
if (error.response && error.response.status === 404) {
console.log(`No image found at index ${i} - this is normal`);
} else {
console.error(`Failed to preload image ${i}:`, error);
}
});
}
}, 100);
}
......@@ -1532,7 +1337,12 @@ const openCreateDialog = async (date) => {
image_04: null,
image_04_mime_type: null,
image_05: null,
image_05_mime_type: null
image_05_mime_type: null,
delete_image_01: false,
delete_image_02: false,
delete_image_03: false,
delete_image_04: false,
delete_image_05: false
};
console.log('Creating new task with date:', formattedDate);
......@@ -1570,7 +1380,12 @@ const closeEditDialog = () => {
image_04: null,
image_04_mime_type: null,
image_05: null,
image_05_mime_type: null
image_05_mime_type: null,
delete_image_01: false,
delete_image_02: false,
delete_image_03: false,
delete_image_04: false,
delete_image_05: false
}
// 清理图像缓存
......@@ -1626,12 +1441,7 @@ const confirmDeleteTask = async () => {
tasks.value.splice(index, 1)
}
// 3. 显示成功消息
successMessage.value = t('task.messages.deleteSuccess', { name: taskToDelete.value.task_name })
setTimeout(() => {
successMessage.value = ''
}, 3000)
// 3. 关闭对话框
closeDeleteDialog()
closeEditDialog() // 如果正在编辑,也关闭编辑对话框
......@@ -2277,12 +2087,14 @@ watch(() => editForm.value.subject_id, (newSubjectId, oldSubjectId) => {
/* 任务布局容器 */
.task-layout {
position: relative;
height: calc(100vh - 270px);
overflow: hidden;
overflow: visible;
}
/* 筛选面板容器 */
.filter-panel-container {
position: relative;
width: 240px;
min-width: 240px;
max-width: 240px;
......@@ -2299,6 +2111,20 @@ watch(() => editForm.value.subject_id, (newSubjectId, oldSubjectId) => {
transform: translateX(-100%);
}
/* 浮动折叠按钮:展开时贴在 panel 右侧边缘, 折叠时移至最左侧 */
.filter-panel-toggle {
position: absolute;
top: 50%;
left: 240px; /* panel 宽 240 + margin-right 16, 减半按钮宽约等于贴在右边 */
transform: translate(-50%, -50%);
z-index: 10;
transition: left 0.4s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.filter-panel-toggle.filter-panel-toggle-collapsed {
left: 16px;
}
.filter-panel-card {
width: 240px;
min-width: 240px;
......@@ -2415,33 +2241,31 @@ watch(() => editForm.value.subject_id, (newSubjectId, oldSubjectId) => {
padding: 0;
}
.fc-event-custom {
.task-calendar :deep(.fc-event-custom) {
display: flex;
flex-direction: column;
width: 100%;
border-radius: 3px;
overflow: hidden;
background: #E0E0E0;
border: 1px solid transparent;
transition: all 0.15s;
cursor: grab;
position: relative;
}
.fc-event-custom:hover {
.task-calendar :deep(.fc-event-custom:hover) {
border-color: #000;
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
z-index: 5;
}
.fc-event-content {
.task-calendar :deep(.fc-event-content) {
display: flex;
align-items: center;
gap: 3px;
padding: 1px 3px;
font-size: 11px;
font-size: 14px;
font-weight: 500;
color: #000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -2449,43 +2273,42 @@ watch(() => editForm.value.subject_id, (newSubjectId, oldSubjectId) => {
z-index: 2;
}
.fc-event-avatar {
width: 18px;
height: 18px;
.task-calendar :deep(.fc-event-avatar) {
width: 22px;
height: 22px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.fc-event-subject {
.task-calendar :deep(.fc-event-subject) {
font-weight: 700;
flex-shrink: 0;
color: #000 !important;
}
.fc-event-title {
.task-calendar :deep(.fc-event-title) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
font-weight: 600;
}
.fc-event-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
.task-calendar :deep(.fc-event-pct) {
margin-left: auto;
padding-left: 4px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
line-height: 0;
}
.fc-event-fill {
height: 100%;
transition: width 0.3s;
.task-calendar :deep(.fc-event-ring) {
display: block;
}
.task-calendar :deep(.fc) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment