Commit b8cdc054 authored by Administrator's avatar Administrator
Browse files

added Story page

parent 1a5a30c9
import apiClient from './index.js'
/**
* 通用的获取所有分页数据的函数
* @param {Function} apiCall - API调用函数
* @param {Object} params - 查询参数
* @returns {Promise<Array>} 所有页面的数据数组
*/
const fetchAllPages = async (apiCall, params = {}) => {
let allData = []
let page = 1
let hasNext = true
console.log('Starting to fetch all pages for API call...')
while (hasNext) {
try {
const currentParams = { ...params, page, page_size: 100 } // 每页100条,减少请求次数
console.log(`Fetching page ${page} with params:`, currentParams)
const response = await apiCall(currentParams)
if (response && response.results) {
// 分页格式响应
allData = [...allData, ...response.results]
hasNext = !!response.next
console.log(`Page ${page}: Got ${response.results.length} items, total so far: ${allData.length}, hasNext: ${hasNext}`)
} else if (Array.isArray(response)) {
// 非分页格式响应(向后兼容)
allData = response
hasNext = false
console.log('Non-paginated response detected, got all data at once:', allData.length, 'items')
} else {
console.log('Unexpected response format, stopping pagination')
hasNext = false
}
page++
// 安全检查:防止无限循环
if (page > 100) {
console.warn('Reached maximum page limit (100), stopping pagination')
break
}
} catch (error) {
console.error(`Error fetching page ${page}:`, error)
throw error
}
}
console.log(`Finished fetching all pages. Total items: ${allData.length}`)
return allData
}
// API端点定义
const apiEndpoints = {
STORIES: {
LIST: '/api/stories/',
DETAIL: (storyId) => `/api/stories/${storyId}/`,
UPDATE: (storyId) => `/api/stories/${storyId}/`,
DELETE: (storyId) => `/api/stories/${storyId}/`
}
}
/**
* 单页API调用函数(内部使用)
* @param {Object} params - 查询参数(包含分页参数)
* @returns {Promise} API响应
*/
const getStoriesPage = async (params = {}) => {
const response = await apiClient.get(apiEndpoints.STORIES.LIST, { params })
return response.data
}
/**
* 获取所有系列任务
* @param {Object} params - 查询参数
* @returns {Promise} 系列任务列表(完整数据,已处理分页)
*/
export const getStories = async (params = {}) => {
try {
// 构建查询参数,过滤掉null和undefined值
const queryParams = {}
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
queryParams[key] = value
}
})
console.log('Stories API request params:', queryParams)
// 使用通用分页函数获取所有数据
const allStories = await fetchAllPages(getStoriesPage, queryParams)
console.log('Stories API final result:', {
totalCount: allStories.length,
sampleData: allStories.slice(0, 3) // 显示前3条数据作为样例
})
return allStories
} catch (error) {
console.error('Failed to fetch stories:', error)
throw error
}
}
/**
* 根据ID获取系列任务
* @param {number} storyId - 系列任务ID
* @returns {Promise} 系列任务详情
*/
export const getStoryById = async (storyId) => {
try {
const response = await apiClient.get(apiEndpoints.STORIES.DETAIL(storyId))
console.log('Story detail API response:', response.data)
return response.data
} catch (error) {
console.error('Failed to fetch story detail:', error)
// 提供更详细的错误信息
if (error.response) {
// 服务器响应了错误状态码
throw new Error(`获取系列任务详情失败: ${error.response.status} - ${error.response.statusText}`)
} else if (error.request) {
// 请求已发出但没有收到响应
throw new Error('网络错误: 无法连接到服务器')
} else {
// 其他错误
throw new Error(`请求配置错误: ${error.message}`)
}
}
}
/**
* 创建新的系列任务
* @param {Object} storyData - 系列任务数据
* @returns {Promise} 创建的系列任务
*/
export const createStory = async (storyData) => {
try {
const response = await apiClient.post(apiEndpoints.STORIES.LIST, storyData)
console.log('Create story API response:', response.data)
return response.data
} catch (error) {
console.error('Failed to create story:', error)
// 提供更详细的错误信息
if (error.response) {
// 服务器响应了错误状态码
throw new Error(`创建系列任务失败: ${error.response.status} - ${error.response.statusText}`)
} else if (error.request) {
// 请求已发出但没有收到响应
throw new Error('网络错误: 无法连接到服务器')
} else {
// 其他错误
throw new Error(`请求配置错误: ${error.message}`)
}
}
}
/**
* 更新系列任务
* @param {number} storyId - 系列任务ID
* @param {Object} storyData - 更新的系列任务数据
* @returns {Promise} 更新后的系列任务
*/
export const updateStory = async (storyId, storyData) => {
try {
const response = await apiClient.patch(apiEndpoints.STORIES.UPDATE(storyId), storyData)
console.log('Update story API response:', response.data)
return response.data
} catch (error) {
console.error('Failed to update story:', error)
// 提供更详细的错误信息
if (error.response) {
// 服务器响应了错误状态码
throw new Error(`更新系列任务失败: ${error.response.status} - ${error.response.statusText}`)
} else if (error.request) {
// 请求已发出但没有收到响应
throw new Error('网络错误: 无法连接到服务器')
} else {
// 其他错误
throw new Error(`请求配置错误: ${error.message}`)
}
}
}
/**
* 删除系列任务
* @param {number} storyId - 系列任务ID
* @returns {Promise} 删除结果
*/
export const deleteStory = async (storyId) => {
try {
const response = await apiClient.delete(apiEndpoints.STORIES.DELETE(storyId))
console.log('Delete story API response:', response.data)
return response.data
} catch (error) {
console.error('Failed to delete story:', error)
// 提供更详细的错误信息
if (error.response) {
// 服务器响应了错误状态码
throw new Error(`删除系列任务失败: ${error.response.status} - ${error.response.statusText}`)
} else if (error.request) {
// 请求已发出但没有收到响应
throw new Error('网络错误: 无法连接到服务器')
} else {
// 其他错误
throw new Error(`请求配置错误: ${error.message}`)
}
}
}
......@@ -8,16 +8,23 @@
<v-list-item :title="$t('navigation.welcome.title')" :subtitle="$t('navigation.welcome.subtitle')"></v-list-item>
<v-divider></v-divider>
<v-list v-model:opened="opened">
<v-list v-model:opened="opened" nav>
<template v-for="item in filteredMenu" :key="item.path || item.nameKey">
<!-- 分组(有 children):点击父项仅用于展开,不路由 -->
<v-list-group
v-if="item.children && item.children.length"
:value="item.path"
:prepend-icon="item.mdiIcon"
slim
>
<template #activator="{ props }">
<v-list-item v-bind="props">
<v-list-item
v-bind="props"
:ripple="true"
:disabled="false"
nav
density="comfortable"
>
<v-list-item-title class="text-body-1">{{ $t(item.nameKey) }}</v-list-item-title>
</v-list-item>
</template>
......@@ -30,6 +37,7 @@
:ripple="child.hasRouter"
:disabled="!child.hasRouter"
nav
density="compact"
>
<v-list-item-title class="text-body-1">{{ $t(child.nameKey) }}</v-list-item-title>
</v-list-item>
......@@ -43,13 +51,14 @@
:ripple="item.hasRouter"
:disabled="false"
@click="!item.hasRouter ? noop() : undefined"
nav>
nav
density="comfortable"
>
<v-list-item-title class="text-body-1">{{ $t(item.nameKey) }}</v-list-item-title>
</v-list-item>
</template>
</v-list>
</v-navigation-drawer>
</template>
<script setup>
......@@ -104,4 +113,9 @@ const noop = () => {}
</script>
<style scoped>
/* 使用Vuetify的类来确保一致的外观 */
.v-list-group--activator .v-list-item {
/* 确保分组标题与普通列表项有相同的密度设置 */
--v-list-item-density: -1;
}
</style>
......@@ -34,6 +34,12 @@ export const GLOBAL_MENU_ITEMS = [
path: '/master-data/terms',
mdiIcon: 'mdi-calendar-month-outline',
hasRouter: true
},
{
nameKey: 'navigation.menu.stories',
path: '/master-data/stories',
mdiIcon: 'mdi-book-multiple-outline',
hasRouter: true
}
]
},
......
export default {
pagination: {
itemsPerPage: 'Items per page',
itemsPerPageAll: 'All',
noDataText: 'No data available',
loadingText: 'Loading items...',
pageText: '{0}-{1} of {2}',
rowsPerPageText: 'Rows per page:'
},
actions: {
edit: 'Edit',
delete: 'Delete',
view: 'View'
},
filter: {
search: 'Search',
clear: 'Clear',
apply: 'Apply'
}
}
\ No newline at end of file
......@@ -3,10 +3,12 @@ import buttons from './common/buttons'
import messages from './common/messages'
import status from './common/status'
import validation from './common/validation'
import table from './common/table'
import student from './modules/student'
import subject from './modules/subject'
import term from './modules/term'
import task from './modules/task'
import story from './modules/story'
import auth from './modules/auth'
import navigation from './modules/navigation'
import appHeader from './components/app-header'
......@@ -17,12 +19,14 @@ export default {
buttons,
messages,
status,
validation
validation,
table
},
student,
subject,
term,
task,
story,
auth,
navigation,
components: {
......
......@@ -12,6 +12,7 @@ export default {
students: 'Students',
subjects: 'Subjects',
terms: 'Terms',
stories: 'Story',
tasks: 'Task Management',
dashboard: 'Dashboard',
settings: 'Settings',
......@@ -24,6 +25,7 @@ export default {
students: 'Students',
subjects: 'Subjects',
terms: 'Terms',
stories: 'Story',
tasks: 'Task Management',
profile: 'Profile'
},
......
export default {
title: 'Story',
tableHeader: {
storyId: 'Story ID',
storyName: 'Story Name',
description: 'Description',
studentId: 'Student',
subjectId: 'Subject',
termId: 'Term',
trackedFlag: 'Tracked',
createdBy: 'Created By',
creationDate: 'Creation Date',
lastUpdatedBy: 'Last Updated By',
lastUpdateDate: 'Last Update Date',
actions: 'Actions'
},
form: {
storyName: 'Story Name',
description: 'Description',
studentId: 'Student',
subjectId: 'Subject',
termId: 'Term',
trackedFlag: 'Tracked'
},
dialog: {
create: 'Create Story',
edit: 'Edit Story',
delete: 'Delete Story'
},
button: {
create: 'Create Story',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
cancel: 'Cancel',
confirm: 'Confirm'
},
message: {
createSuccess: 'Story created successfully',
updateSuccess: 'Story updated successfully',
deleteSuccess: 'Story deleted successfully',
createError: 'Failed to create story',
updateError: 'Failed to update story',
deleteError: 'Failed to delete story',
loadError: 'Failed to load stories'
},
deleteConfirm: {
title: 'Delete Story',
message: 'Are you sure you want to delete this story?',
description: 'This action cannot be undone.',
warning: 'This action cannot be undone. Please proceed with caution.',
storyInfo: '{name}'
},
validation: {
storyNameRequired: 'Story name is required',
studentRequired: 'Student is required',
subjectRequired: 'Subject is required',
termRequired: 'Term is required'
},
status: {
tracked: 'Tracked',
notTracked: 'Not Tracked',
enabled: 'Enabled',
disabled: 'Disabled'
}
}
export default {
pagination: {
itemsPerPage: '每页显示',
itemsPerPageAll: '全部',
noDataText: '暂无数据',
loadingText: '加载中...',
pageText: '{0}-{1} 共 {2}',
rowsPerPageText: '每页行数:'
},
actions: {
edit: '编辑',
delete: '删除',
view: '查看'
},
filter: {
search: '搜索',
clear: '清除',
apply: '应用'
}
}
\ No newline at end of file
......@@ -3,10 +3,12 @@ import buttons from './common/buttons'
import messages from './common/messages'
import status from './common/status'
import validation from './common/validation'
import table from './common/table'
import student from './modules/student'
import subject from './modules/subject'
import term from './modules/term'
import task from './modules/task'
import story from './modules/story'
import auth from './modules/auth'
import navigation from './modules/navigation'
import appHeader from './components/app-header'
......@@ -17,12 +19,14 @@ export default {
buttons,
messages,
status,
validation
validation,
table
},
student,
subject,
term,
task,
story,
auth,
navigation,
components: {
......
......@@ -12,6 +12,7 @@ export default {
students: '学生',
subjects: '学科',
terms: '学期',
stories: '系列任务',
tasks: '作业管理',
dashboard: '仪表盘',
settings: '设置',
......@@ -24,6 +25,7 @@ export default {
students: '学生',
subjects: '学科',
terms: '学期',
stories: '系列任务',
tasks: '作业管理',
profile: '个人资料'
},
......
export default {
title: '系列任务',
tableHeader: {
storyId: '系列任务ID',
storyName: '系列任务名称',
description: '描述',
studentId: '学生',
subjectId: '学科',
termId: '学期',
trackedFlag: '跟踪状态',
createdBy: '创建人',
creationDate: '创建日期',
lastUpdatedBy: '最后更新人',
lastUpdateDate: '最后更新日期',
actions: '操作'
},
form: {
storyName: '系列任务名称',
description: '描述',
studentId: '学生',
subjectId: '学科',
termId: '学期',
trackedFlag: '跟踪状态'
},
dialog: {
create: '创建系列任务',
edit: '编辑系列任务',
delete: '删除系列任务'
},
button: {
create: '创建系列任务',
edit: '编辑',
delete: '删除',
save: '保存',
cancel: '取消',
confirm: '确认'
},
message: {
createSuccess: '系列任务创建成功',
updateSuccess: '系列任务更新成功',
deleteSuccess: '系列任务删除成功',
createError: '创建系列任务失败',
updateError: '更新系列任务失败',
deleteError: '删除系列任务失败',
loadError: '加载系列任务失败'
},
deleteConfirm: {
title: '删除系列任务',
message: '确定要删除此系列任务吗?',
description: '此操作无法撤销。',
warning: '此操作无法撤销,请谨慎操作。',
storyInfo: '{name}'
},
validation: {
storyNameRequired: '系列任务名称不能为空',
studentRequired: '学生不能为空',
subjectRequired: '学科不能为空',
termRequired: '学期不能为空'
},
status: {
tracked: '已跟踪',
notTracked: '未跟踪',
enabled: '启用',
disabled: '禁用'
}
}
......@@ -6,6 +6,7 @@ import ProfileView from '../views/ProfileView.vue'
import StudentView from '../views/StudentView.vue'
import SubjectView from '../views/SubjectView.vue'
import TermView from '../views/TermView.vue'
import StoryView from '../views/StoryView.vue'
import TaskView from '../views/TaskView.vue'
const router = createRouter({
......@@ -84,8 +85,7 @@ const router = createRouter({
]
},
},
{
path: '/master-data/terms',
{ path: '/master-data/terms',
name: 'terms',
component: TermView,
meta: {
......@@ -102,6 +102,24 @@ const router = createRouter({
]
},
},
{
path: '/master-data/stories',
name: 'stories',
component: StoryView,
meta: {
requiresAuth: true,
layout: 'default',
title: 'Stories',
breadcrumb: {
key: 'navigation.breadcrumb.stories'
},
breadcrumbPath: [
{ key: 'navigation.breadcrumb.home', to: '/', disabled: false },
{ key: 'navigation.breadcrumb.masterData', to: null, disabled: true },
{ key: 'navigation.breadcrumb.stories', to: '/master-data/stories', disabled: true }
]
},
},
{
path: '/tasks',
name: 'tasks',
......
<script setup>
import { onMounted, ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { getStories, updateStory, createStory, deleteStory } from '@/api/storyService.js'
import { getStudents } from '@/api/studentService.js'
import { getSubjects } from '@/api/subjectService.js'
import { getTerms } from '@/api/termService.js'
const { t } = useI18n()
const authStore = useAuthStore()
const isLoading = ref(false)
const isError = ref(false)
const errorMessage = ref('')
const stories = ref([])
// 关联数据
const students = ref([])
const subjects = ref([])
const terms = ref([])
const isLoadingRelatedData = ref(false)
// 编辑功能相关状态
const editDialog = ref(false)
const editingStory = ref(null)
const editForm = ref({
story_name: '',
description: '',
student_id: null,
subject_id: null,
term_id: null,
tracked_flag: 'N'
})
const isSaving = ref(false)
const successMessage = ref('')
const saveErrorMessage = ref('')
// 新增功能相关状态
const createDialog = ref(false)
const createForm = ref({
story_name: '',
description: '',
student_id: null,
subject_id: null,
term_id: null,
tracked_flag: 'N'
})
const isCreating = ref(false)
const createErrorMessage = ref('')
// 删除功能相关状态
const deleteDialog = ref(false)
const storyToDelete = ref(null)
const isDeleting = ref(false)
const headers = computed(() => [
{ title: t('story.tableHeader.storyName'), value: 'story_name', sortable: true, width: '20%' },
{ title: t('story.tableHeader.description'), value: 'description', sortable: false, width: '20%' },
{ title: t('story.tableHeader.studentId'), value: 'student_id', sortable: true, width: '15%' },
{ title: t('story.tableHeader.subjectId'), value: 'subject_id', sortable: true, width: '15%' },
{ title: t('story.tableHeader.termId'), value: 'term_id', sortable: true, width: '15%' },
{
title: t('story.tableHeader.trackedFlag'),
value: 'tracked_flag',
sortable: true,
width: '15%',
sort: (a, b) => {
if (a === 'Y' && b === 'N') return -1
if (a === 'N' && b === 'Y') return 1
return 0
}
},
{ title: t('story.tableHeader.actions'), value: 'actions', sortable: false, align: 'end', width: '12%' },
])
// 获取系列任务数据
const fetchStories = async () => {
isLoading.value = true
isError.value = false
errorMessage.value = ''
try {
const storiesData = await getStories()
console.log('Stories data:', storiesData)
stories.value = Array.isArray(storiesData) ? storiesData : []
// 为每个story添加关联数据的名称
stories.value.forEach(story => {
story.student_name = getStudentName(story.student_id)
story.subject_name = getSubjectName(story.subject_id)
story.term_name = getTermName(story.term_id)
})
} catch (e) {
isError.value = true
errorMessage.value = e?.response?.data?.detail || e?.message || t('story.message.loadError')
// 3秒后自动隐藏错误消息
setTimeout(() => {
isError.value = false
errorMessage.value = ''
}, 3000)
} finally {
isLoading.value = false
}
}
// 获取关联数据(学生、学科、学期)
const fetchRelatedData = async () => {
isLoadingRelatedData.value = true
try {
const [studentsData, subjectsData, termsData] = await Promise.all([
getStudents(),
getSubjects(),
getTerms()
])
students.value = studentsData
subjects.value = subjectsData
terms.value = termsData
console.log('Related data loaded:', {
students: students.value.length,
subjects: subjects.value.length,
terms: terms.value.length
})
} catch (error) {
console.error('Failed to load related data:', error)
} finally {
isLoadingRelatedData.value = false
}
}
onMounted(() => {
// 确保 axios 鉴权拦截器与头已设置
if (authStore && authStore.initializeAuth) {
authStore.initializeAuth()
}
fetchStories()
fetchRelatedData()
})
// 编辑功能方法
const openEditDialog = (story) => {
editingStory.value = story
editForm.value = {
story_name: story.story_name || '',
description: story.description || '',
student_id: story.student_id || null,
subject_id: story.subject_id || null,
term_id: story.term_id || null,
tracked_flag: story.tracked_flag || 'N'
}
console.log('Editing story data:', story)
console.log('Converted form data:', editForm.value)
editDialog.value = true
}
const closeEditDialog = () => {
editDialog.value = false
editingStory.value = null
editForm.value = {
story_name: '',
description: '',
student_id: null,
subject_id: null,
term_id: null,
tracked_flag: 'N'
}
}
const saveStory = async () => {
// 检查是否有正在编辑的故事以及故事ID
if (!editingStory.value?.story_id) {
saveErrorMessage.value = t('common.messages.error.general')
// 3秒后自动隐藏错误消息
setTimeout(() => {
saveErrorMessage.value = ''
}, 3000)
return
}
isSaving.value = true
saveErrorMessage.value = ''
successMessage.value = ''
try {
console.log('Form data before update:', editForm.value)
// 构建要发送的数据
const updateData = {
story_name: editForm.value.story_name,
description: editForm.value.description,
student_id: editForm.value.student_id,
subject_id: editForm.value.subject_id,
term_id: editForm.value.term_id,
tracked_flag: editForm.value.tracked_flag
}
console.log('Sending data:', updateData)
// 1. 调用API更新系列任务
await updateStory(editingStory.value.story_id, updateData)
// 2. 更新本地数据
const index = stories.value.findIndex(s => s.story_id === editingStory.value.story_id)
if (index !== -1) {
stories.value[index] = {
...stories.value[index],
...updateData
}
}
// 3. 显示成功消息
successMessage.value = t('story.message.updateSuccess')
setTimeout(() => {
successMessage.value = ''
}, 3000)
closeEditDialog()
} catch (error) {
console.error('Update failed:', error)
saveErrorMessage.value = error.message || t('story.message.updateError')
setTimeout(() => {
saveErrorMessage.value = ''
}, 3000)
} finally {
isSaving.value = false
}
}
// 新增功能方法
const openCreateDialog = () => {
createForm.value = {
story_name: '',
description: '',
student_id: null,
subject_id: null,
term_id: null,
tracked_flag: 'N'
}
createErrorMessage.value = ''
createDialog.value = true
}
const closeCreateDialog = () => {
createDialog.value = false
createForm.value = {
story_name: '',
description: '',
student_id: null,
subject_id: null,
term_id: null,
tracked_flag: 'N'
}
createErrorMessage.value = ''
}
const createNewStory = async () => {
isCreating.value = true
createErrorMessage.value = ''
successMessage.value = ''
try {
console.log('Form data before creation:', createForm.value)
// 构建要发送的数据
const createData = {
story_name: createForm.value.story_name,
description: createForm.value.description,
student_id: createForm.value.student_id,
subject_id: createForm.value.subject_id,
term_id: createForm.value.term_id,
tracked_flag: createForm.value.tracked_flag
}
console.log('Sending data:', createData)
// 1. 调用API创建系列任务
const newStory = await createStory(createData)
// 2. 创建成功后,将新系列任务添加到本地列表
const storyToAdd = {
...newStory,
...createData
}
stories.value.unshift(storyToAdd) // 在列表顶部添加新系列任务
// 3. 显示成功消息
successMessage.value = t('story.message.createSuccess')
setTimeout(() => {
successMessage.value = ''
}, 3000)
closeCreateDialog()
} catch (error) {
console.error('Creation failed:', error)
createErrorMessage.value = error.message || t('story.message.createError')
setTimeout(() => {
createErrorMessage.value = ''
}, 3000)
} finally {
isCreating.value = false
}
}
// 删除功能方法
const openDeleteDialog = (story) => {
storyToDelete.value = story
deleteDialog.value = true
}
const closeDeleteDialog = () => {
deleteDialog.value = false
storyToDelete.value = null
}
const confirmDelete = async () => {
if (!storyToDelete.value?.story_id) {
console.error('Unable to get story ID')
return
}
isDeleting.value = true
try {
console.log('Deleting story:', storyToDelete.value)
// 1. 调用API删除系列任务
await deleteStory(storyToDelete.value.story_id)
// 2. 从本地列表中移除
const index = stories.value.findIndex(s => s.story_id === storyToDelete.value.story_id)
if (index !== -1) {
stories.value.splice(index, 1)
}
// 3. 显示成功消息
successMessage.value = t('story.message.deleteSuccess')
setTimeout(() => {
successMessage.value = ''
}, 3000)
closeDeleteDialog()
closeEditDialog() // 自动关闭编辑对话框
} catch (error) {
console.error('Deletion failed:', error)
// 在删除对话框中显示错误,但不关闭对话框
saveErrorMessage.value = error.message || t('story.message.deleteError')
setTimeout(() => {
saveErrorMessage.value = ''
}, 3000)
} finally {
isDeleting.value = false
}
}
// 格式化显示
const getStudentName = (studentId) => {
const student = students.value.find(s => s.student_id === studentId)
return student ? student.student_name : studentId
}
const getSubjectName = (subjectId) => {
const subject = subjects.value.find(s => s.subject_id === subjectId)
return subject ? subject.subject_name : subjectId
}
const getTermName = (termId) => {
const term = terms.value.find(t => t.term_id === termId)
return term ? term.term_name : termId
}
const getTrackedStatus = (flag) => {
return flag === 'Y' ? t('story.status.tracked') : t('story.status.notTracked')
}
</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('story.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('story.button.create') }}
</v-btn>
<v-btn color="primary" prepend-icon="mdi-refresh" :loading="isLoading" @click="fetchStories">
{{ t('common.buttons.refresh') }}
</v-btn>
</v-col>
</v-row>
<!-- 成功消息提示 -->
<v-alert
v-if="successMessage"
type="success"
class="mb-4"
closable
@click:close="successMessage = ''"
>
{{ successMessage }}
</v-alert>
<!-- 错误消息提示 -->
<v-alert
v-if="isError"
type="error"
class="mb-4"
closable
>
{{ errorMessage }}
</v-alert>
<v-progress-linear v-if="isLoading" indeterminate color="primary" class="mb-4"></v-progress-linear>
<!-- 数据表格 -->
<v-data-table
:headers="headers"
:items="stories"
item-key="story_id"
:items-per-page="10"
class="elevation-1"
:sort-by="[{ key: 'story_name', order: 'asc' }]"
multi-sort
:items-per-page-text="t('common.table.pagination.itemsPerPage')"
:no-data-text="t('common.table.pagination.noDataText')"
:loading-text="t('common.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('common.table.pagination.itemsPerPageAll') }
]"
:hide-default-footer="false"
height="calc(100vh - 360px)"
fixed-header
>
<template v-slot:[`item.student_id`]="{ item }">
{{ getStudentName(item.student_id) }}
</template>
<template v-slot:[`item.subject_id`]="{ item }">
{{ getSubjectName(item.subject_id) }}
</template>
<template v-slot:[`item.term_id`]="{ item }">
{{ getTermName(item.term_id) }}
</template>
<template v-slot:[`item.tracked_flag`]="{ item }">
<v-chip
:color="item.tracked_flag === 'Y' ? 'success' : 'grey'"
text-color="white"
size="small"
>
{{ getTrackedStatus(item.tracked_flag) }}
</v-chip>
</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-container>
<!-- 编辑对话框 -->
<v-dialog v-model="editDialog" max-width="600px">
<v-card>
<v-card-title>{{ t('story.dialog.edit') }}</v-card-title>
<v-card-text>
<v-alert
v-if="saveErrorMessage"
type="error"
variant="tonal"
closable
class="mb-4"
>
{{ saveErrorMessage }}
</v-alert>
<v-form @submit.prevent="saveStory">
<v-container>
<v-row>
<v-col cols="12">
<v-text-field
v-model="editForm.story_name"
:label="t('story.form.storyName') + ' *'"
required
:rules="[v => !!v || t('story.validation.storyNameRequired')]"
variant="outlined"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
v-model="editForm.description"
:label="t('story.form.description')"
rows="3"
variant="outlined"
></v-textarea>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="editForm.student_id"
:items="students"
item-title="student_name"
item-value="student_id"
:label="t('story.form.studentId') + ' *'"
required
:rules="[v => !!v || t('story.validation.studentRequired')]"
:loading="isLoadingRelatedData"
variant="outlined"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="editForm.subject_id"
:items="subjects"
item-title="subject_name"
item-value="subject_id"
:label="t('story.form.subjectId') + ' *'"
required
:rules="[v => !!v || t('story.validation.subjectRequired')]"
:loading="isLoadingRelatedData"
variant="outlined"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="editForm.term_id"
:items="terms"
item-title="term_name"
item-value="term_id"
:label="t('story.form.termId') + ' *'"
required
:rules="[v => !!v || t('story.validation.termRequired')]"
:loading="isLoadingRelatedData"
variant="outlined"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-switch
v-model="editForm.tracked_flag"
:label="t('story.form.trackedFlag')"
color="primary"
true-value="Y"
false-value="N"
inset
></v-switch>
</v-col>
</v-row>
</v-container>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn
color="error"
variant="outlined"
prepend-icon="mdi-delete"
@click="openDeleteDialog(editingStory)"
:disabled="isSaving"
>
{{ t('common.buttons.delete') }}
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="grey-darken-1"
variant="text"
@click="closeEditDialog"
>
{{ t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="primary"
variant="text"
@click="saveStory"
:loading="isSaving"
:disabled="isSaving"
>
{{ t('common.buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 新增对话框 -->
<v-dialog v-model="createDialog" max-width="600px">
<v-card>
<v-card-title>{{ t('story.dialog.create') }}</v-card-title>
<v-card-text>
<v-alert
v-if="createErrorMessage"
type="error"
variant="tonal"
closable
class="mb-4"
>
{{ createErrorMessage }}
</v-alert>
<v-form @submit.prevent="createNewStory">
<v-container>
<v-row>
<v-col cols="12">
<v-text-field
v-model="createForm.story_name"
:label="t('story.form.storyName') + ' *'"
required
:rules="[v => !!v || t('story.validation.storyNameRequired')]"
variant="outlined"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
v-model="createForm.description"
:label="t('story.form.description')"
rows="3"
variant="outlined"
></v-textarea>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="createForm.student_id"
:items="students"
item-title="student_name"
item-value="student_id"
:label="t('story.form.studentId') + ' *'"
required
:rules="[v => !!v || t('story.validation.studentRequired')]"
:loading="isLoadingRelatedData"
variant="outlined"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="createForm.subject_id"
:items="subjects"
item-title="subject_name"
item-value="subject_id"
:label="t('story.form.subjectId') + ' *'"
required
:rules="[v => !!v || t('story.validation.subjectRequired')]"
:loading="isLoadingRelatedData"
variant="outlined"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="createForm.term_id"
:items="terms"
item-title="term_name"
item-value="term_id"
:label="t('story.form.termId') + ' *'"
required
:rules="[v => !!v || t('story.validation.termRequired')]"
:loading="isLoadingRelatedData"
variant="outlined"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-switch
v-model="createForm.tracked_flag"
:label="t('story.form.trackedFlag')"
color="primary"
true-value="Y"
false-value="N"
inset
></v-switch>
</v-col>
</v-row>
</v-container>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="grey-darken-1"
variant="text"
@click="closeCreateDialog"
>
{{ t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="success"
variant="text"
@click="createNewStory"
:loading="isCreating"
:disabled="isCreating"
>
{{ 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('story.deleteConfirm.title') }}
</v-card-title>
<v-card-text>
<div class="text-body-1 mb-4">
{{ t('story.deleteConfirm.message') }}
</div>
<div v-if="storyToDelete" class="bg-grey-lighten-4 pa-3 rounded">
<div class="font-weight-medium">{{ storyToDelete.story_name }}</div>
<div class="text-caption text-medium-emphasis">
{{ t('story.deleteConfirm.storyInfo', {
name: storyToDelete.story_name
}) }}
</div>
</div>
<div class="text-body-2 text-error mt-4">
<v-icon icon="mdi-alert-circle" size="small" class="mr-1" />
{{ t('story.deleteConfirm.warning') }}
</div>
</v-card-text>
<v-card-actions>
<v-spacer></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"
@click="confirmDelete"
:loading="isDeleting"
:disabled="isDeleting"
>
{{ t('common.buttons.confirm') + ' ' + t('common.buttons.delete') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.v-data-table {
width: 100%;
}
</style>
......@@ -577,7 +577,7 @@ const saveStudent = async () => {
</div>
</template>
<template v-slot:[`item.enabled`]="{ item }">
<v-chip :color="item.enabled === 'Y' ? 'success' : 'error'" size="small" variant="flat">
<v-chip :color="item.enabled === 'Y' ? 'success' : 'error'" text-color="white" size="small">
{{ item.enabled === 'Y' ? $t('common.status.enabled') : $t('common.status.disabled') }}
</v-chip>
</template>
......@@ -714,6 +714,9 @@ const saveStudent = async () => {
v-model="editForm.enabled"
:label="$t('student.form.enabledStatus')"
color="primary"
:true-value="true"
:false-value="false"
inset
/>
</v-col>
</v-row>
......@@ -871,6 +874,9 @@ const saveStudent = async () => {
v-model="createForm.enabled"
:label="$t('student.form.enabledStatus') + ' *'"
color="primary"
:true-value="true"
:false-value="false"
inset
/>
</v-col>
</v-row>
......
......@@ -390,12 +390,12 @@ const getCalendarColorValue = (colorCode) => {
fixed-header
>
<template v-slot:[`item.enabled_flag`]="{ item }">
<v-chip :color="item.enabled_flag === 'Y' ? 'success' : 'error'" size="small" variant="flat">
<v-chip :color="item.enabled_flag === 'Y' ? 'success' : 'error'" text-color="white" size="small">
{{ item.enabled_flag === 'Y' ? $t('subject.status.enabled') : $t('subject.status.disabled') }}
</v-chip>
</template>
<template v-slot:[`item.primary_flag`]="{ item }">
<v-chip :color="item.primary_flag === 'Y' ? 'primary' : 'default'" size="small" variant="flat">
<v-chip :color="item.primary_flag === 'Y' ? 'primary' : 'default'" text-color="white" size="small">
{{ item.primary_flag === 'Y' ? $t('subject.status.primary') : $t('subject.status.notPrimary') }}
</v-chip>
</template>
......@@ -484,6 +484,7 @@ const getCalendarColorValue = (colorCode) => {
color="primary"
true-value="Y"
false-value="N"
inset
/>
</v-col>
<v-col cols="6">
......@@ -493,6 +494,7 @@ const getCalendarColorValue = (colorCode) => {
color="primary"
true-value="Y"
false-value="N"
inset
/>
</v-col>
</v-row>
......@@ -596,6 +598,7 @@ const getCalendarColorValue = (colorCode) => {
color="primary"
true-value="Y"
false-value="N"
inset
/>
</v-col>
<v-col cols="6">
......@@ -605,6 +608,7 @@ const getCalendarColorValue = (colorCode) => {
color="primary"
true-value="Y"
false-value="N"
inset
/>
</v-col>
</v-row>
......
......@@ -345,7 +345,7 @@
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
<!-- <v-text-field
v-model="editForm.completion_percent"
:label="$t('task.task.completionPercent') + ' *'"
:placeholder="$t('task.task.completionPercent')"
......@@ -355,7 +355,29 @@
min="0"
max="100"
required
/> -->
<v-slider
v-model="editForm.completion_percent"
:label="$t('task.task.completionPercent') + ' *'"
:min="0"
:max="100"
:step="1"
thumb-label="always"
persistent-hint
:hint="`${editForm.completion_percent}%`"
>
<template #append>
<v-text-field
v-model="editForm.completion_percent"
type="number"
min="0"
max="100"
hide-details
density="compact"
style="width: 80px;"
/>
</template>
</v-slider>
</v-col>
<v-col cols="12" md="6">
<v-text-field
......
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