Commit 8752832b authored by Administrator's avatar Administrator
Browse files

added Subject and Term page

parent c9b259a1
......@@ -21,10 +21,24 @@ export const apiEndpoints = {
},
// 学生相关
STUDENTS: {
LIST: '/api/student/',
CREATE: '/api/student/',
UPDATE: (studentId) => `/api/student/${studentId}/`,
DELETE: (studentId) => `/api/student/${studentId}/`,
LIST: '/api/students/',
CREATE: '/api/students/',
UPDATE: (studentId) => `/api/students/${studentId}/`,
DELETE: (studentId) => `/api/students/${studentId}/`,
},
// 学科相关
SUBJECTS: {
LIST: '/api/subjects/',
CREATE: '/api/subjects/',
UPDATE: (subjectId) => `/api/subjects/${subjectId}/`,
DELETE: (subjectId) => `/api/subjects/${subjectId}/`,
},
// 学期相关
TERMS: {
LIST: '/api/terms/',
CREATE: '/api/terms/',
UPDATE: '/api/terms/{termId}/',
DELETE: '/api/terms/{termId}/',
},
}
......
import apiClient, { apiEndpoints } from './index.js'
// Get subject list
export const getSubjects = async () => {
try {
const response = await apiClient.get(apiEndpoints.SUBJECTS.LIST)
return response.data
} catch (error) {
throw new Error(error?.response?.data?.detail || error?.message || 'Failed to fetch subjects')
}
}
// Update subject information
export const updateSubject = async (subjectId, subjectData) => {
try {
// Convert data format to match API requirements
const apiData = {
subject_name: subjectData.subject_name || '',
enabled_flag: subjectData.enabled_flag || 'Y',
primary_flag: subjectData.primary_flag || 'N',
calendar_color: subjectData.calendar_color || '',
sort_sequence: parseInt(subjectData.sort_sequence) || 0,
tenant_id: subjectData.tenant_id || 1,
}
console.log('Sending update request:', {
subjectId,
url: apiEndpoints.SUBJECTS.UPDATE(subjectId),
data: apiData
})
const response = await apiClient.patch(apiEndpoints.SUBJECTS.UPDATE(subjectId), apiData)
return response.data
} catch (error) {
console.error('API error details:', {
status: error?.response?.status,
statusText: error?.response?.statusText,
data: error?.response?.data,
headers: error?.response?.headers
})
const errorMessage = error?.response?.data?.detail ||
error?.response?.data?.message ||
error?.response?.data?.error ||
`HTTP ${error?.response?.status}: ${error?.response?.statusText}` ||
error.message ||
'Failed to update subject'
throw new Error(errorMessage)
}
}
// Create subject
export const createSubject = async (subjectData) => {
try {
// Convert data format to match API requirements
const apiData = {
subject_name: subjectData.subject_name || '',
enabled_flag: subjectData.enabled_flag || 'Y',
primary_flag: subjectData.primary_flag || 'N',
calendar_color: subjectData.calendar_color || '',
sort_sequence: parseInt(subjectData.sort_sequence) || 0,
tenant_id: subjectData.tenant_id || 1,
}
console.log('Sending create request:', {
url: apiEndpoints.SUBJECTS.CREATE,
data: apiData
})
const response = await apiClient.post(apiEndpoints.SUBJECTS.CREATE, apiData)
return response.data
} catch (error) {
console.error('API error details:', {
status: error?.response?.status,
statusText: error?.response?.statusText,
data: error?.response?.data,
headers: error?.response?.headers
})
const errorMessage = error?.response?.data?.detail ||
error?.response?.data?.message ||
error?.response?.data?.error ||
`HTTP ${error?.response?.status}: ${error?.response?.statusText}` ||
error.message ||
'Failed to create subject'
throw new Error(errorMessage)
}
}
// Delete subject
export const deleteSubject = async (subjectId) => {
try {
console.log('Sending delete request:', {
subjectId,
url: apiEndpoints.SUBJECTS.DELETE(subjectId)
})
const response = await apiClient.delete(apiEndpoints.SUBJECTS.DELETE(subjectId))
return response.data
} catch (error) {
console.error('API error details:', {
status: error?.response?.status,
statusText: error?.response?.statusText,
data: error?.response?.data,
headers: error?.response?.headers
})
const errorMessage = error?.response?.data?.detail ||
error?.response?.data?.message ||
error?.response?.data?.error ||
`HTTP ${error?.response?.status}: ${error?.response?.statusText}` ||
error.message ||
'Failed to delete subject'
throw new Error(errorMessage)
}
}
import apiClient, { apiEndpoints } from './index.js'
export const getTerms = async () => {
try {
const response = await apiClient.get(apiEndpoints.TERMS.LIST)
return response.data
} catch (error) {
throw new Error(error?.response?.data?.detail || error?.message || 'Failed to fetch terms')
}
}
export const createTerm = async (termData) => {
try {
const response = await apiClient.post(apiEndpoints.TERMS.CREATE, termData)
return response.data
} catch (error) {
throw new Error(error?.response?.data?.message || 'Failed to create term')
}
}
export const updateTerm = async (termId, termData) => {
try {
const response = await apiClient.patch(apiEndpoints.TERMS.UPDATE.replace('{termId}', termId), termData)
return response.data
} catch (error) {
throw new Error(error?.response?.data?.message || 'Failed to update term')
}
}
export const deleteTerm = async (termId) => {
try {
const response = await apiClient.delete(apiEndpoints.TERMS.DELETE.replace('{termId}', termId))
return response.data
} catch (error) {
throw new Error(error?.response?.data?.message || 'Failed to delete term')
}
}
......@@ -13,15 +13,13 @@
<v-main>
<v-container fluid class="pt-4 pb-0">
<v-breadcrumbs :items="breadcrumbs">
<template #prepend>
<v-icon :icon="currentBreadcrumbIcon" size="small" class="me-2" />
</template>
<template #item="{ item }">
<v-breadcrumbs-item
:disabled="item.disabled"
:to="item.to"
:exact="true"
>
<v-icon v-if="item.icon" :icon="item.icon" size="small" class="me-1" />
{{ item.title }}
</v-breadcrumbs-item>
</template>
......@@ -87,36 +85,8 @@ onMounted(() => {
drawerActive.value = true
})
const breadcrumbs = computed(() => {
const path = route.path
// 根据路径生成面包屑
if (path === '/') {
return [{ title: t('navigation.breadcrumb.home'), to: '/', disabled: true }]
}
if (path === '/profile') {
return [
{ title: t('navigation.breadcrumb.home'), to: '/', disabled: false },
{ title: t('navigation.breadcrumb.profile'), to: '/profile', disabled: true }
]
}
if (path === '/master-data/students') {
return [
{ title: t('navigation.breadcrumb.home'), to: '/', disabled: false },
{ title: t('navigation.breadcrumb.masterData'), to: null, disabled: true },
{ title: t('navigation.breadcrumb.students'), to: '/master-data/students', disabled: true }
]
}
// 默认返回首页
return [{ title: t('navigation.breadcrumb.home'), to: '/', disabled: true }]
})
const currentBreadcrumbIcon = computed(() => {
const targetPath = route.path
const findIconByPath = (items, path) => {
// 从菜单配置中查找图标的辅助函数
const findIconByPath = (items, path) => {
for (const item of items) {
if (item.path === path && item.mdiIcon) return item.mdiIcon
if (item.children) {
......@@ -125,9 +95,66 @@ const currentBreadcrumbIcon = computed(() => {
}
}
return null
}
const breadcrumbs = computed(() => {
// 如果路由meta中定义了breadcrumbPath,直接使用
if (route.meta.breadcrumbPath) {
return route.meta.breadcrumbPath.map((item, index) => {
const breadcrumbItem = {
title: t(item.key),
to: item.to,
disabled: item.disabled
}
return findIconByPath(GLOBAL_MENU_ITEMS, targetPath) || 'mdi-home'
// 为首页添加home图标
if (index === 0 && item.key === 'navigation.breadcrumb.home') {
breadcrumbItem.icon = 'mdi-home'
}
// 为基础数据层级添加数据库图标
else if (item.key === 'navigation.breadcrumb.masterData') {
breadcrumbItem.icon = 'mdi-database-outline'
}
// 为当前页面(最后一项)添加页面特定图标
else if (index === route.meta.breadcrumbPath.length - 1) {
// 优先使用路由meta中定义的图标
if (route.meta.breadcrumb?.icon) {
breadcrumbItem.icon = route.meta.breadcrumb.icon
} else {
// 从菜单配置中查找图标
const pageIcon = findIconByPath(GLOBAL_MENU_ITEMS, route.path)
if (pageIcon) {
breadcrumbItem.icon = pageIcon
}
}
}
return breadcrumbItem
})
}
// 默认情况下,根据路径生成简单面包屑
const path = route.path
if (path === '/') {
return [{
title: t('navigation.breadcrumb.home'),
to: '/',
disabled: true,
icon: 'mdi-home'
}]
}
// 如果没有定义breadcrumbPath,返回默认面包屑
return [{
title: t('navigation.breadcrumb.home'),
to: '/',
disabled: true,
icon: 'mdi-home'
}]
})
</script>
<style scoped>
......
/**
* 颜色常量定义
* 统一管理应用中使用的颜色选项
*/
// 颜色名称到CSS颜色值的映射
export const COLOR_MAPPING = {
'color-red': '#f44336',
'color-orange': '#ff9800',
'color-yellow': '#ffeb3b',
'color-green': '#4caf50',
'color-blue': '#2196f3',
'color-darkblue': '#1565c0',
'color-purple': '#9c27b0',
'color-brown': '#795548',
'color-pink': '#ff69b4',
'color-gray': '#9e9e9e',
'color-lightred': '#ffcdd2',
'color-lightorange': '#ffe0b2',
'color-lightyellow': '#fff9c4',
'color-lightgreen': '#c8e6c9',
'color-lightblue': '#bbdefb',
'color-lightpurple': '#e1bee7',
'color-lightpink': '#f8bbd9',
'color-lightgray': '#e0e0e0',
'color-darkred': '#b71c1c',
'color-darkorange': '#e65100',
'color-darkgreen': '#1b5e20',
'color-darkpurple': '#4a148c',
'color-darkgray': '#424242',
'color-cyan': '#00bcd4',
'color-lime': '#cddc39',
'color-indigo': '#3f51b5',
'color-teal': '#009688',
'color-amber': '#ffc107',
'color-deeporange': '#ff5722',
'color-silver': '#c0c0c0',
'color-bluesky': '#87ceeb'
}
// 颜色键名数组(用于生成颜色选项)
export const COLOR_KEYS = Object.keys(COLOR_MAPPING)
// 获取颜色值的辅助函数
export const getColorValue = (colorKey) => {
return COLOR_MAPPING[colorKey] || '#e0e0e0'
}
// 检查颜色键是否有效的辅助函数
export const isValidColorKey = (colorKey) => {
return COLOR_KEYS.includes(colorKey)
}
......@@ -25,9 +25,15 @@ export const GLOBAL_MENU_ITEMS = [
},
{
nameKey: 'navigation.menu.subjects',
path: '/subjects',
path: '/master-data/subjects',
mdiIcon: 'mdi-book-open-blank-variant-outline',
hasRouter: true
},
{
nameKey: 'navigation.menu.terms',
path: '/master-data/terms',
mdiIcon: 'mdi-calendar-month-outline',
hasRouter: true
}
]
},
......
......@@ -4,6 +4,8 @@ import messages from './common/messages'
import status from './common/status'
import validation from './common/validation'
import student from './modules/student'
import subject from './modules/subject'
import term from './modules/term'
import auth from './modules/auth'
import navigation from './modules/navigation'
import appHeader from './components/app-header'
......@@ -17,6 +19,8 @@ export default {
validation
},
student,
subject,
term,
auth,
navigation,
components: {
......
......@@ -11,6 +11,7 @@ export default {
masterData: 'Master Data',
students: 'Students',
subjects: 'Subjects',
terms: 'Terms',
dashboard: 'Dashboard',
settings: 'Settings',
about: 'About'
......@@ -20,6 +21,8 @@ export default {
home: 'Home',
masterData: 'Master Data',
students: 'Students',
subjects: 'Subjects',
terms: 'Terms',
profile: 'Profile'
},
......
export default {
title: 'Subjects',
subtitle: 'Subject Management',
// Table headers
table: {
headers: {
subject_name: 'Subject Name',
enabled_flag: 'Status',
primary_flag: 'Primary Subject',
calendar_color: 'Calendar Color',
sort_sequence: 'Sort Order',
actions: 'Actions'
},
// Data table pagination
pagination: {
itemsPerPage: 'Items per page:',
itemsPerPageAll: 'All',
itemsPerPageText: '{start}-{end} of {total}',
pageText: 'Page {page} of {pages}',
noDataText: 'No data available',
loadingText: 'Loading items...'
}
},
// Form fields
form: {
subject_name: 'Subject Name',
enabled_flag: 'Enabled Status',
primary_flag: 'Primary Subject',
calendar_color: 'Calendar Color',
sort_sequence: 'Sort Order',
subject_namePlaceholder: 'Enter subject name',
sort_sequencePlaceholder: 'Enter sort order number',
calendar_colorPlaceholder: 'Select calendar color'
},
// Dialog titles
dialog: {
add: 'Add Subject',
edit: 'Edit Subject Information',
delete: 'Confirm Delete'
},
// Button text
buttons: {
addSubject: 'Add Subject'
},
// Messages
messages: {
createSuccess: 'Subject created successfully',
updateSuccess: 'Subject information updated successfully',
deleteSuccess: 'Subject "{name}" deleted successfully',
createError: 'Creation failed, please try again',
updateError: 'Save failed, please try again',
deleteError: 'Delete failed, please try again',
loadError: 'Failed to load subject list'
},
// Delete confirmation
deleteConfirmation: {
title: 'Confirm Delete',
message: 'Are you sure you want to delete this subject?',
warning: 'This action cannot be undone, please proceed with caution.',
subjectInfo: '{name} | Sort: {sort}'
},
// Validation messages
validation: {
subjectNameRequired: 'Subject name is required',
enabledFlagRequired: 'Status is required'
},
// Status display
status: {
enabled: 'Enabled',
disabled: 'Disabled',
primary: 'Primary',
notPrimary: 'Regular'
},
// Calendar color options
calendarColors: {
'color-red': 'Red',
'color-orange': 'Orange',
'color-yellow': 'Yellow',
'color-green': 'Green',
'color-blue': 'Blue',
'color-darkblue': 'Dark Blue',
'color-purple': 'Purple',
'color-brown': 'Brown',
'color-pink': 'Pink',
'color-gray': 'Gray',
'color-lightred': 'Light Red',
'color-lightorange': 'Light Orange',
'color-lightyellow': 'Light Yellow',
'color-lightgreen': 'Light Green',
'color-lightblue': 'Light Blue',
'color-lightpurple': 'Light Purple',
'color-lightpink': 'Light Pink',
'color-lightgray': 'Light Gray',
'color-darkred': 'Dark Red',
'color-darkorange': 'Dark Orange',
'color-darkgreen': 'Dark Green',
'color-darkpurple': 'Dark Purple',
'color-darkgray': 'Dark Gray',
'color-cyan': 'Cyan',
'color-lime': 'Lime',
'color-indigo': 'Indigo',
'color-teal': 'Teal',
'color-amber': 'Amber',
'color-deeporange': 'Deep Orange',
'color-silver': 'Silver',
'color-bluesky': 'Blue Sky'
}
}
export default {
title: 'Terms',
subtitle: 'Term Management',
// Table headers
table: {
headers: {
termName: 'Term Name',
studentName: 'Student Name',
startDate: 'Start Date',
endDate: 'End Date',
actions: 'Actions'
},
// Data table pagination
pagination: {
itemsPerPage: 'Items per page:',
itemsPerPageAll: 'All',
itemsPerPageText: '{start}-{end} of {total}',
pageText: 'Page {page} of {pages}',
noDataText: 'No data available',
loadingText: 'Loading...'
}
},
// Form fields
form: {
student: 'Student',
termName: 'Term Name',
startDate: 'Start Date',
endDate: 'End Date',
termNamePlaceholder: 'Enter term name',
studentPlaceholder: 'Select a student'
},
// Dialog titles
dialog: {
add: 'Add Term',
edit: 'Edit Term Information',
delete: 'Confirm Deletion'
},
// Button text
buttons: {
addTerm: 'Add Term'
},
// Messages
messages: {
createSuccess: 'Term created successfully',
updateSuccess: 'Term information updated successfully',
deleteSuccess: 'Term "{name}" deleted successfully',
createError: 'Creation failed, please try again',
updateError: 'Save failed, please try again',
deleteError: 'Deletion failed, please try again',
loadError: 'Failed to load term list',
unknownStudent: 'Unknown Student'
},
// Delete confirmation
deleteConfirmation: {
title: 'Confirm Deletion',
message: 'Are you sure you want to delete this term?',
warning: 'This action cannot be undone. Please proceed with caution.'
},
// Validation messages
validation: {
studentRequired: 'Student is required',
termNameRequired: 'Term name is required',
startDateRequired: 'Start date is required',
endDateRequired: 'End date is required'
}
}
......@@ -4,6 +4,8 @@ import messages from './common/messages'
import status from './common/status'
import validation from './common/validation'
import student from './modules/student'
import subject from './modules/subject'
import term from './modules/term'
import auth from './modules/auth'
import navigation from './modules/navigation'
import appHeader from './components/app-header'
......@@ -17,6 +19,8 @@ export default {
validation
},
student,
subject,
term,
auth,
navigation,
components: {
......
......@@ -10,7 +10,8 @@ export default {
profile: '个人资料',
masterData: '基础数据',
students: '学生',
subjects: '科目',
subjects: '学科',
terms: '学期',
dashboard: '仪表盘',
settings: '设置',
about: '关于'
......@@ -20,6 +21,8 @@ export default {
home: '首页',
masterData: '基础数据',
students: '学生',
subjects: '学科',
terms: '学期',
profile: '个人资料'
},
......
export default {
title: '学科',
subtitle: '学科管理',
// 表格标题
table: {
headers: {
subject_name: '学科名称',
enabled_flag: '状态',
primary_flag: '主要学科',
calendar_color: '日历颜色',
sort_sequence: '排序',
actions: '操作'
},
// 数据表分页
pagination: {
itemsPerPage: '每页项目数:',
itemsPerPageAll: '全部',
itemsPerPageText: '第 {start}-{end} 项,共 {total} 项',
pageText: '第 {page} 页,共 {pages} 页',
noDataText: '暂无数据',
loadingText: '加载中...'
}
},
// 表单字段
form: {
subject_name: '学科名称',
enabled_flag: '启用状态',
primary_flag: '主要学科',
calendar_color: '日历颜色',
sort_sequence: '排序',
subject_namePlaceholder: '请输入学科名称',
sort_sequencePlaceholder: '请输入排序数字',
calendar_colorPlaceholder: '请选择日历颜色'
},
// 对话框标题
dialog: {
add: '添加学科',
edit: '编辑学科信息',
delete: '确认删除'
},
// 按钮文本
buttons: {
addSubject: '添加学科'
},
// 消息
messages: {
createSuccess: '学科创建成功',
updateSuccess: '学科信息更新成功',
deleteSuccess: '学科 "{name}" 删除成功',
createError: '创建失败,请重试',
updateError: '保存失败,请重试',
deleteError: '删除失败,请重试',
loadError: '加载学科列表失败'
},
// 删除确认
deleteConfirmation: {
title: '确认删除',
message: '确定要删除此学科吗?',
warning: '此操作无法撤销,请谨慎操作。',
subjectInfo: '{name} | 排序: {sort}'
},
// 验证消息
validation: {
subjectNameRequired: '学科名称为必填项',
enabledFlagRequired: '状态为必填项'
},
// 状态显示
status: {
enabled: '启用',
disabled: '禁用',
primary: '主要',
notPrimary: '普通'
},
// 日历颜色选项
calendarColors: {
'color-red': '红色',
'color-orange': '橙色',
'color-yellow': '黄色',
'color-green': '绿色',
'color-blue': '蓝色',
'color-darkblue': '深蓝色',
'color-purple': '紫色',
'color-brown': '棕色',
'color-pink': '粉色',
'color-gray': '灰色',
'color-lightred': '浅红色',
'color-lightorange': '浅橙色',
'color-lightyellow': '浅黄色',
'color-lightgreen': '浅绿色',
'color-lightblue': '浅蓝色',
'color-lightpurple': '浅紫色',
'color-lightpink': '浅粉色',
'color-lightgray': '浅灰色',
'color-darkred': '深红色',
'color-darkorange': '深橙色',
'color-darkgreen': '深绿色',
'color-darkpurple': '深紫色',
'color-darkgray': '深灰色',
'color-cyan': '青色',
'color-lime': '青柠色',
'color-indigo': '靛蓝色',
'color-teal': '蓝绿色',
'color-amber': '琥珀色',
'color-deeporange': '深橙红色',
'color-silver': '银色',
'color-bluesky': '天蓝色'
}
}
export default {
title: '学期',
subtitle: '学期管理',
// 表格标题
table: {
headers: {
termName: '学期名称',
studentName: '学生姓名',
startDate: '开始日期',
endDate: '结束日期',
actions: '操作'
},
// 数据表分页
pagination: {
itemsPerPage: '每页项目数:',
itemsPerPageAll: '全部',
itemsPerPageText: '第 {start}-{end} 项,共 {total} 项',
pageText: '第 {page} 页,共 {pages} 页',
noDataText: '暂无数据',
loadingText: '加载中...'
}
},
// 表单字段
form: {
student: '学生',
termName: '学期名称',
startDate: '开始日期',
endDate: '结束日期',
termNamePlaceholder: '请输入学期名称',
studentPlaceholder: '请选择学生'
},
// 对话框标题
dialog: {
add: '添加学期',
edit: '编辑学期信息',
delete: '确认删除'
},
// 按钮文本
buttons: {
addTerm: '添加学期'
},
// 消息
messages: {
createSuccess: '学期创建成功',
updateSuccess: '学期信息更新成功',
deleteSuccess: '学期 "{name}" 删除成功',
createError: '创建失败,请重试',
updateError: '保存失败,请重试',
deleteError: '删除失败,请重试',
loadError: '加载学期列表失败',
unknownStudent: '未知学生'
},
// 删除确认
deleteConfirmation: {
title: '确认删除',
message: '确定要删除此学期吗?',
warning: '此操作无法撤销,请谨慎操作。'
},
// 验证消息
validation: {
studentRequired: '学生为必选项',
termNameRequired: '学期名称为必填项',
startDateRequired: '开始日期为必填项',
endDateRequired: '结束日期为必填项'
}
}
......@@ -4,6 +4,8 @@ import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import ProfileView from '../views/ProfileView.vue'
import StudentView from '../views/StudentView.vue'
import SubjectView from '../views/SubjectView.vue'
import TermView from '../views/TermView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
......@@ -12,7 +14,15 @@ const router = createRouter({
path: '/',
name: 'home',
component: HomeView,
meta: { requiresAuth: true, layout: 'default', title: 'Home' },
meta: {
requiresAuth: true,
layout: 'default',
title: 'Home',
breadcrumb: {
key: 'navigation.breadcrumb.home',
icon: 'mdi-home'
}
},
},
{
path: '/login',
......@@ -24,13 +34,72 @@ const router = createRouter({
path: '/profile',
name: 'profile',
component: ProfileView,
meta: { requiresAuth: true, layout: 'default', title: 'Profile' },
meta: {
requiresAuth: true,
layout: 'default',
title: 'Profile',
breadcrumb: {
key: 'navigation.breadcrumb.profile'
},
breadcrumbPath: [
{ key: 'navigation.breadcrumb.home', to: '/', disabled: false },
{ key: 'navigation.breadcrumb.profile', to: '/profile', disabled: true }
]
},
},
{
path: '/master-data/students',
name: 'students',
component: StudentView,
meta: { requiresAuth: true, layout: 'default', title: 'Students' },
meta: {
requiresAuth: true,
layout: 'default',
title: 'Students',
breadcrumb: {
key: 'navigation.breadcrumb.students'
},
breadcrumbPath: [
{ key: 'navigation.breadcrumb.home', to: '/', disabled: false },
{ key: 'navigation.breadcrumb.masterData', to: null, disabled: true },
{ key: 'navigation.breadcrumb.students', to: '/master-data/students', disabled: true }
]
},
},
{
path: '/master-data/subjects',
name: 'subjects',
component: SubjectView,
meta: {
requiresAuth: true,
layout: 'default',
title: 'Subjects',
breadcrumb: {
key: 'navigation.breadcrumb.subjects'
},
breadcrumbPath: [
{ key: 'navigation.breadcrumb.home', to: '/', disabled: false },
{ key: 'navigation.breadcrumb.masterData', to: null, disabled: true },
{ key: 'navigation.breadcrumb.subjects', to: '/master-data/subjects', disabled: true }
]
},
},
{
path: '/master-data/terms',
name: 'terms',
component: TermView,
meta: {
requiresAuth: true,
layout: 'default',
title: 'Terms',
breadcrumb: {
key: 'navigation.breadcrumb.terms'
},
breadcrumbPath: [
{ key: 'navigation.breadcrumb.home', to: '/', disabled: false },
{ key: 'navigation.breadcrumb.masterData', to: null, disabled: true },
{ key: 'navigation.breadcrumb.terms', to: '/master-data/terms', disabled: true }
]
},
},
{
path: '/:pathMatch(.*)*',
......
<script setup>
import { onMounted, ref } from 'vue'
import { onMounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { getStudents, updateStudent, createStudent, deleteStudent } from '@/api/studentService.js'
......@@ -47,7 +47,7 @@ const deleteDialog = ref(false)
const studentToDelete = ref(null)
const isDeleting = ref(false)
const headers = [
const headers = computed(() => [
{ title: t('student.table.headers.avatar'), value: 'avatar', align: 'start', sortable: false },
{ title: t('student.table.headers.name'), value: 'student_name', sortable: true },
{
......@@ -64,7 +64,7 @@ const headers = [
{ title: t('student.table.headers.age'), value: 'age', sortable: true },
{ title: t('student.table.headers.grade'), value: 'grade', sortable: true },
{ title: t('student.table.headers.actions'), value: 'actions', sortable: false, align: 'end' },
]
])
const fetchStudents = async () => {
isLoading.value = true
......
This diff is collapsed.
<script setup>
import { onMounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { getTerms, updateTerm, createTerm, deleteTerm } from '@/api/termService.js'
import { getStudents } from '@/api/studentService.js'
const { t } = useI18n()
const authStore = useAuthStore()
const isLoading = ref(false)
const isError = ref(false)
const errorMessage = ref('')
const terms = ref([])
const students = ref([])
// 编辑功能相关状态
const editDialog = ref(false)
const editingTerm = ref(null)
const editForm = ref({
student_id: '',
term_name: '',
term_start_date: '',
term_end_date: '',
tenant_id: 1
})
const isSaving = ref(false)
const successMessage = ref('')
const saveErrorMessage = ref('')
// 新增功能相关状态
const createDialog = ref(false)
const createForm = ref({
student_id: '',
term_name: '',
term_start_date: '',
term_end_date: '',
tenant_id: 1
})
const isCreating = ref(false)
const createErrorMessage = ref('')
// 删除功能相关状态
const deleteDialog = ref(false)
const termToDelete = ref(null)
const isDeleting = ref(false)
const headers = computed(() => [
{ title: t('term.table.headers.termName'), value: 'term_name', sortable: true },
{ title: t('term.table.headers.studentName'), value: 'student_name', sortable: true },
{ title: t('term.table.headers.startDate'), value: 'term_start_date', sortable: true },
{ title: t('term.table.headers.endDate'), value: 'term_end_date', sortable: true },
{ title: t('term.table.headers.actions'), value: 'actions', sortable: false, align: 'end' },
])
// 学生下拉选项(仅显示启用的学生)
const studentOptions = computed(() =>
students.value
.filter(student => student.enabled === 'Y' || student.enabled === true) // 过滤出启用的学生,兼容字符串和布尔值格式
.map(student => ({
title: student.student_name,
value: student.student_id
}))
)
const fetchTerms = async () => {
isLoading.value = true
isError.value = false
errorMessage.value = ''
try {
const data = await getTerms()
terms.value = Array.isArray(data) ? data : (data?.items || [])
// 为每个term添加学生姓名(包括已禁用的学生)
terms.value.forEach(term => {
const student = students.value.find(s => s.student_id === term.student_id)
term.student_name = student ? student.student_name : t('term.messages.unknownStudent')
})
} catch (e) {
isError.value = true
errorMessage.value = e?.response?.data?.detail || e?.message || t('term.messages.loadError')
setTimeout(() => {
isError.value = false
errorMessage.value = ''
}, 3000)
} finally {
isLoading.value = false
}
}
const fetchStudents = async () => {
try {
const data = await getStudents()
students.value = Array.isArray(data) ? data : (data?.items || [])
} catch (e) {
console.error('Failed to fetch students:', e)
}
}
onMounted(async () => {
if (authStore && authStore.initializeAuth) {
authStore.initializeAuth()
}
await fetchStudents()
await fetchTerms()
})
// 格式化日期显示
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toLocaleDateString('zh-CN')
}
// 格式化日期为输入框格式
const formatDateForInput = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
return date.toISOString().split('T')[0]
}
// 编辑功能方法
const openEditDialog = (term) => {
editingTerm.value = term
editForm.value = {
student_id: term.student_id || '',
term_name: term.term_name || '',
term_start_date: formatDateForInput(term.term_start_date),
term_end_date: formatDateForInput(term.term_end_date),
tenant_id: term.tenant_id || 1
}
console.log('Editing term data:', term)
console.log('Converted form data:', editForm.value)
editDialog.value = true
}
const closeEditDialog = () => {
editDialog.value = false
editingTerm.value = null
editForm.value = {
student_id: '',
term_name: '',
term_start_date: '',
term_end_date: '',
tenant_id: 1
}
}
// 新增功能方法
const openCreateDialog = () => {
createForm.value = {
student_id: '',
term_name: '',
term_start_date: '',
term_end_date: '',
tenant_id: 1
}
createErrorMessage.value = ''
createDialog.value = true
}
const closeCreateDialog = () => {
createDialog.value = false
createForm.value = {
student_id: '',
term_name: '',
term_start_date: '',
term_end_date: '',
tenant_id: 1
}
createErrorMessage.value = ''
}
const createNewTerm = async () => {
isCreating.value = true
createErrorMessage.value = ''
successMessage.value = ''
try {
console.log('Form data before creation:', createForm.value)
const createData = {
student_id: parseInt(createForm.value.student_id),
term_name: createForm.value.term_name,
term_start_date: new Date(createForm.value.term_start_date).toISOString(),
term_end_date: new Date(createForm.value.term_end_date).toISOString(),
tenant_id: createForm.value.tenant_id
}
console.log('Sending data:', createData)
const newTerm = await createTerm(createData)
// 添加学生姓名到新创建的term
const student = students.value.find(s => s.student_id === newTerm.student_id)
newTerm.student_name = student ? student.student_name : t('term.messages.unknownStudent')
terms.value.unshift(newTerm)
successMessage.value = t('term.messages.createSuccess')
setTimeout(() => {
successMessage.value = ''
}, 3000)
closeCreateDialog()
} catch (error) {
console.error('Creation failed:', error)
createErrorMessage.value = error.message || t('term.messages.createError')
setTimeout(() => {
createErrorMessage.value = ''
}, 3000)
} finally {
isCreating.value = false
}
}
// 删除功能方法
const openDeleteDialog = (term) => {
termToDelete.value = term
deleteDialog.value = true
}
const closeDeleteDialog = () => {
deleteDialog.value = false
termToDelete.value = null
}
const confirmDeleteTerm = async () => {
if (!termToDelete.value?.term_id) {
console.error('Unable to get term ID')
return
}
isDeleting.value = true
try {
console.log('Deleting term:', termToDelete.value)
await deleteTerm(termToDelete.value.term_id)
const index = terms.value.findIndex(t => t.term_id === termToDelete.value.term_id)
if (index !== -1) {
terms.value.splice(index, 1)
}
successMessage.value = t('term.messages.deleteSuccess', { name: termToDelete.value.term_name })
setTimeout(() => {
successMessage.value = ''
}, 3000)
closeDeleteDialog()
closeEditDialog()
} catch (error) {
console.error('Deletion failed:', error)
saveErrorMessage.value = error.message || t('term.messages.deleteError')
setTimeout(() => {
saveErrorMessage.value = ''
}, 3000)
} finally {
isDeleting.value = false
}
}
const saveTerm = async () => {
if (!editingTerm.value?.term_id) {
saveErrorMessage.value = t('common.messages.error.general')
setTimeout(() => {
saveErrorMessage.value = ''
}, 3000)
return
}
isSaving.value = true
saveErrorMessage.value = ''
successMessage.value = ''
try {
console.log('Form data before saving:', editForm.value)
const updateData = {
student_id: parseInt(editForm.value.student_id),
term_name: editForm.value.term_name,
term_start_date: new Date(editForm.value.term_start_date).toISOString(),
term_end_date: new Date(editForm.value.term_end_date).toISOString(),
tenant_id: editForm.value.tenant_id
}
console.log('Sending data:', updateData)
await updateTerm(editingTerm.value.term_id, updateData)
const index = terms.value.findIndex(t => t.term_id === editingTerm.value.term_id)
if (index !== -1) {
const updatedTerm = {
...terms.value[index],
student_id: editForm.value.student_id,
term_name: editForm.value.term_name,
term_start_date: new Date(editForm.value.term_start_date).toISOString(),
term_end_date: new Date(editForm.value.term_end_date).toISOString(),
tenant_id: editForm.value.tenant_id
}
// 更新学生姓名
const student = students.value.find(s => s.student_id === parseInt(editForm.value.student_id))
updatedTerm.student_name = student ? student.student_name : t('term.messages.unknownStudent')
terms.value[index] = updatedTerm
}
successMessage.value = t('term.messages.updateSuccess')
setTimeout(() => {
successMessage.value = ''
}, 3000)
closeEditDialog()
} catch (error) {
console.error('Save failed:', error)
saveErrorMessage.value = error.message || t('term.messages.updateError')
setTimeout(() => {
saveErrorMessage.value = ''
}, 3000)
} finally {
isSaving.value = false
}
}
</script>
<template>
<v-container fluid>
<v-row class="mb-4" align="center" justify="space-between">
<v-col cols="12" sm="6">
<h2 class="text-h5">{{ $t('term.title') }}</h2>
</v-col>
<v-col cols="12" sm="6" class="text-sm-right text-right">
<v-btn
color="success"
prepend-icon="mdi-plus"
class="mr-2"
@click="openCreateDialog"
>
{{ $t('term.buttons.addTerm') }}
</v-btn>
<v-btn color="primary" prepend-icon="mdi-refresh" :loading="isLoading" @click="fetchTerms">
{{ $t('common.buttons.refresh') }}
</v-btn>
</v-col>
</v-row>
<v-alert v-if="isError" type="error" class="mb-4" closable>
{{ errorMessage }}
</v-alert>
<v-alert v-if="successMessage" type="success" class="mb-4" closable @click:close="successMessage = ''">
{{ successMessage }}
</v-alert>
<v-progress-linear v-if="isLoading" indeterminate color="primary" class="mb-4"></v-progress-linear>
<v-data-table
:headers="headers"
:items="terms"
item-key="term_id"
:items-per-page="10"
class="elevation-1"
:sort-by="[{ key: 'term_start_date', order: 'desc' }]"
multi-sort
:items-per-page-text="$t('term.table.pagination.itemsPerPage')"
:no-data-text="$t('term.table.pagination.noDataText')"
:loading-text="$t('term.table.pagination.loadingText')"
:items-per-page-options="[
{ value: 5, title: '5' },
{ value: 10, title: '10' },
{ value: 25, title: '25' },
{ value: 50, title: '50' },
{ value: -1, title: $t('term.table.pagination.itemsPerPageAll') }
]"
:hide-default-footer="false"
height="calc(100vh - 360px)"
fixed-header
>
<template v-slot:[`item.term_start_date`]="{ item }">
{{ formatDate(item.term_start_date) }}
</template>
<template v-slot:[`item.term_end_date`]="{ item }">
{{ formatDate(item.term_end_date) }}
</template>
<template v-slot:[`item.actions`]="{ item }">
<v-btn
icon="mdi-pencil"
size="small"
color="primary"
variant="text"
@click="openEditDialog(item)"
>
</v-btn>
</template>
</v-data-table>
<!-- 编辑对话框 -->
<v-dialog v-model="editDialog" max-width="500px">
<v-card>
<v-card-title class="text-h5">
{{ $t('term.dialog.edit') }}
</v-card-title>
<v-card-text>
<v-container>
<v-alert v-if="saveErrorMessage" type="error" class="mb-4" closable @click:close="saveErrorMessage = ''">
{{ saveErrorMessage }}
</v-alert>
<v-row>
<v-col cols="12">
<v-select
v-model="editForm.student_id"
:items="studentOptions"
:label="$t('term.form.student')"
required
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="editForm.term_name"
:label="$t('term.form.termName')"
required
variant="outlined"
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model="editForm.term_start_date"
:label="$t('term.form.startDate')"
type="date"
variant="outlined"
required
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model="editForm.term_end_date"
:label="$t('term.form.endDate')"
type="date"
variant="outlined"
required
/>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-btn
color="error"
variant="outlined"
prepend-icon="mdi-delete"
@click="openDeleteDialog(editingTerm)"
:disabled="isSaving"
>
{{ $t('common.buttons.delete') }}
</v-btn>
<v-spacer />
<v-btn
color="grey-darken-1"
variant="text"
@click="closeEditDialog"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="primary"
variant="text"
:loading="isSaving"
:disabled="isSaving"
@click="saveTerm"
>
{{ $t('common.buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 新增对话框 -->
<v-dialog v-model="createDialog" max-width="500px">
<v-card>
<v-card-title class="text-h5">
{{ $t('term.dialog.add') }}
</v-card-title>
<v-card-text>
<v-container>
<v-alert v-if="createErrorMessage" type="error" class="mb-4" closable @click:close="createErrorMessage = ''">
{{ createErrorMessage }}
</v-alert>
<v-row>
<v-col cols="12">
<v-select
v-model="createForm.student_id"
:items="studentOptions"
:label="$t('term.form.student') + ' *'"
required
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model="createForm.term_name"
:label="$t('term.form.termName') + ' *'"
required
variant="outlined"
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model="createForm.term_start_date"
:label="$t('term.form.startDate') + ' *'"
type="date"
variant="outlined"
required
/>
</v-col>
<v-col cols="6">
<v-text-field
v-model="createForm.term_end_date"
:label="$t('term.form.endDate') + ' *'"
type="date"
variant="outlined"
required
/>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="grey-darken-1"
variant="text"
@click="closeCreateDialog"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="success"
variant="text"
:loading="isCreating"
:disabled="isCreating"
@click="createNewTerm"
>
{{ $t('common.buttons.create') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog v-model="deleteDialog" max-width="400px">
<v-card>
<v-card-title class="text-h5 text-error">
<v-icon icon="mdi-alert" class="mr-2" />
{{ $t('term.dialog.delete') }}
</v-card-title>
<v-card-text>
<div class="text-body-1 mb-4">
{{ $t('term.deleteConfirmation.message') }}
</div>
<div v-if="termToDelete" class="bg-grey-lighten-4 pa-3 rounded">
<div class="font-weight-medium">{{ termToDelete.term_name }}</div>
<div class="text-caption text-medium-emphasis">
{{ termToDelete.student_name }} | {{ formatDate(termToDelete.term_start_date) }} - {{ formatDate(termToDelete.term_end_date) }}
</div>
</div>
<div class="text-body-2 text-error mt-4">
<v-icon icon="mdi-alert-circle" size="small" class="mr-1" />
{{ $t('term.deleteConfirmation.warning') }}
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="grey-darken-1"
variant="text"
@click="closeDeleteDialog"
:disabled="isDeleting"
>
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="error"
variant="elevated"
prepend-icon="mdi-delete"
:loading="isDeleting"
:disabled="isDeleting"
@click="confirmDeleteTerm"
>
{{ $t('common.buttons.confirm') + ' ' + $t('common.buttons.delete') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<style scoped>
</style>
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