Commit c9b259a1 authored by Administrator's avatar Administrator
Browse files

added i18n support for Chinese for student page.

parent 0caedf2f
export default {
required: '此字段为必填项',
email: '请输入有效的邮箱地址',
minLength: '最少需要 {min} 个字符',
maxLength: '最多允许 {max} 个字符',
numeric: '请输入有效数字',
positive: '请输入正数',
fileSize: '文件大小不能超过 {size}',
fileType: '请选择有效的文件类型',
imageFile: '请选择图片文件',
passwordMatch: '密码不匹配',
uniqueValue: '该值已存在'
}
export default {
title: '学生管理系统',
search: {
placeholder: '搜索...',
button: '搜索'
},
notifications: {
button: '通知',
empty: '暂无通知',
title: '通知'
},
user: {
menu: '用户菜单',
profile: '个人资料',
settings: '设置',
logout: '退出登录'
},
// 登出对话框
logoutDialog: {
title: '确认登出',
message: '你确定要退出登录吗?',
confirm: '确认',
cancel: '取消'
}
}
// 导入所有翻译模块
import buttons from './common/buttons'
import messages from './common/messages'
import status from './common/status'
import validation from './common/validation'
import student from './modules/student'
import auth from './modules/auth'
import navigation from './modules/navigation'
import appHeader from './components/app-header'
// 组合所有翻译
export default {
common: {
buttons,
messages,
status,
validation
},
student,
auth,
navigation,
components: {
appHeader
}
}
export default {
login: {
title: 'Welcome!',
description: '欢迎使用学生管理系统!本系统是用来追踪学生课程、作业、考试的一站式服务平台。请登录使用全部功能。',
form: {
username: '用户名',
password: '密码',
rememberMe: '记住我',
forgotPassword: '忘记密码?'
},
buttons: {
signIn: '登录',
signUp: '注册'
},
messages: {
success: '登录成功',
error: '登录失败,请检查您的凭据',
unauthorized: '用户名或密码无效',
fillRequired: '请填写用户名和密码',
loginFailed: '登录失败',
loginError: '登录过程中发生错误'
}
},
profile: {
title: '个人资料',
subtitle: '管理您的账户信息',
form: {
displayName: '显示名称',
email: '邮箱',
phone: '电话',
bio: '简介'
},
messages: {
updateSuccess: '个人资料更新成功',
updateError: '更新个人资料失败'
}
},
logout: {
title: '退出登录',
message: '确定要退出登录吗?',
success: '退出登录成功'
}
}
export default {
// 抽屉欢迎信息
welcome: {
title: '欢迎使用学生管理应用',
subtitle: '由 vuetify 支持'
},
menu: {
home: '首页',
profile: '个人资料',
masterData: '基础数据',
students: '学生',
subjects: '科目',
dashboard: '仪表盘',
settings: '设置',
about: '关于'
},
breadcrumb: {
home: '首页',
masterData: '基础数据',
students: '学生',
profile: '个人资料'
},
drawer: {
title: '导航',
collapse: '收起菜单',
expand: '展开菜单'
}
}
export default {
title: '学生',
subtitle: '学生管理',
// 表格标题
table: {
headers: {
avatar: '头像',
name: '姓名',
age: '年龄',
grade: '年级',
enabled: '状态',
actions: '操作'
},
// 数据表分页
pagination: {
itemsPerPage: '每页项目数:',
itemsPerPageAll: '全部',
itemsPerPageText: '第 {start}-{end} 项,共 {total} 项',
pageText: '第 {page} 页,共 {pages} 页',
noDataText: '暂无数据',
loadingText: '加载中...'
}
},
// 表单字段
form: {
name: '姓名',
age: '年龄',
grade: '年级',
enabled: '状态',
enabledStatus: '启用状态',
avatar: '头像',
namePlaceholder: '请输入学生姓名',
agePlaceholder: '请输入年龄',
gradePlaceholder: '请输入年级'
},
// 对话框标题
dialog: {
add: '添加学生',
edit: '编辑学生信息',
delete: '确认删除'
},
// 按钮文本
buttons: {
addStudent: '添加学生',
uploadAvatar: '上传头像',
changeAvatar: '更换头像'
},
// 消息
messages: {
createSuccess: '学生创建成功',
updateSuccess: '学生信息更新成功',
deleteSuccess: '学生 "{name}" 删除成功',
createError: '创建失败,请重试',
updateError: '保存失败,请重试',
deleteError: '删除失败,请重试',
loadError: '加载学生列表失败'
},
// 删除确认
deleteConfirmation: {
title: '确认删除',
message: '确定要删除此学生吗?',
warning: '此操作无法撤销,请谨慎操作。',
studentInfo: '{name} | {age} 岁 | {grade}'
},
// 头像上传
avatar: {
upload: {
title: '头像',
button: '上传头像',
change: '更换头像',
remove: '移除头像',
formats: '支持 PNG、JPG、GIF 格式',
sizeLimit: '文件大小不超过 2MB',
selectImage: '请选择图片文件',
sizeExceeded: '文件大小不能超过 2MB',
readError: '文件读取失败,请重试'
},
fallback: {
alt: '头像',
placeholder: '无头像'
}
},
// 验证消息
validation: {
nameRequired: '姓名为必填项',
enabledRequired: '状态为必填项'
}
}
...@@ -7,12 +7,16 @@ import router from './router' ...@@ -7,12 +7,16 @@ import router from './router'
// 引入Vuetify // 引入Vuetify
import vuetify from './plugins/vuetify' import vuetify from './plugins/vuetify'
// 引入i18n
import i18n from './locales'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia() const pinia = createPinia()
app.use(pinia) app.use(pinia)
app.use(router) app.use(router)
app.use(vuetify) app.use(vuetify)
app.use(i18n)
// 初始化认证状态 // 初始化认证状态
import { useAuthStore } from './stores/auth' import { useAuthStore } from './stores/auth'
......
...@@ -109,8 +109,16 @@ export const useAuthStore = defineStore('auth', () => { ...@@ -109,8 +109,16 @@ export const useAuthStore = defineStore('auth', () => {
const originalRequest = error.config || {} const originalRequest = error.config || {}
const status = error?.response?.status const status = error?.response?.status
const isUnauthorized = status === 401 const isUnauthorized = status === 401
// 如果不是401错误,直接抛出
if (!isUnauthorized) return Promise.reject(error) if (!isUnauthorized) return Promise.reject(error)
// 如果是登录接口的401错误,直接抛出原始错误,不尝试刷新token
const isLoginRequest = originalRequest.url && originalRequest.url.includes('/api/token/')
if (isLoginRequest) {
return Promise.reject(error)
}
if (originalRequest._retry) { if (originalRequest._retry) {
// 已重试过,仍然 401,直接登出 // 已重试过,仍然 401,直接登出
logout() logout()
......
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '../stores/auth' import { useAuthStore } from '../stores/auth'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const username = ref('') const username = ref('')
const password = ref('') const password = ref('')
...@@ -10,7 +13,7 @@ const error = ref('') ...@@ -10,7 +13,7 @@ const error = ref('')
const handleLogin = async () => { const handleLogin = async () => {
if (!username.value || !password.value) { if (!username.value || !password.value) {
error.value = '请填写用户名和密码' error.value = t('auth.login.messages.fillRequired')
return return
} }
...@@ -24,10 +27,14 @@ const handleLogin = async () => { ...@@ -24,10 +27,14 @@ const handleLogin = async () => {
}) })
if (!result.success) { if (!result.success) {
error.value = result.error || '登录失败' error.value = result.error || t('auth.login.messages.loginFailed')
// 登录失败时清空密码字段,提升安全性和用户体验
password.value = ''
} }
} catch { } catch {
error.value = '登录过程中发生错误' error.value = t('auth.login.messages.loginError')
// 登录错误时也清空密码字段
password.value = ''
} finally { } finally {
loading.value = false loading.value = false
} }
...@@ -56,10 +63,15 @@ const handleLogin = async () => { ...@@ -56,10 +63,15 @@ const handleLogin = async () => {
class="pa-8 h-100 justify-center align-center flex-grow-1" class="pa-8 h-100 justify-center align-center flex-grow-1"
style="background: rgba(255, 255, 255, 0.95);" style="background: rgba(255, 255, 255, 0.95);"
> >
<!-- 语言切换器 -->
<div class="d-flex justify-end mb-4">
<LanguageSwitcher />
</div>
<form @submit.prevent="handleLogin" style="width: 100%;"> <form @submit.prevent="handleLogin" style="width: 100%;">
<div class="text-h4 font-weight-bold mb-4">Welcome!</div> <div class="text-h4 font-weight-bold mb-4">{{ $t('auth.login.title') }}</div>
<div class="text-body-1"> <div class="text-body-1">
欢迎使用学生管理系统!本系统是用来追踪学生课程、作业、考试的一站式服务平台。请登录使用全部功能。 {{ $t('auth.login.description') }}
</div> </div>
<v-row justify="center" class="pa-6"> <v-row justify="center" class="pa-6">
<v-avatar size="128" class="mb-4" > <v-avatar size="128" class="mb-4" >
...@@ -68,7 +80,7 @@ const handleLogin = async () => { ...@@ -68,7 +80,7 @@ const handleLogin = async () => {
</v-row> </v-row>
<v-text-field <v-text-field
v-model="username" v-model="username"
label="用户名" :label="$t('auth.login.form.username')"
prepend-inner-icon="mdi-account" prepend-inner-icon="mdi-account"
class="mb-4" class="mb-4"
variant="outlined" variant="outlined"
...@@ -76,7 +88,7 @@ const handleLogin = async () => { ...@@ -76,7 +88,7 @@ const handleLogin = async () => {
/> />
<v-text-field <v-text-field
v-model="password" v-model="password"
label="密码" :label="$t('auth.login.form.password')"
type="password" type="password"
prepend-inner-icon="mdi-lock" prepend-inner-icon="mdi-lock"
class="mb-6" class="mb-6"
...@@ -98,7 +110,7 @@ const handleLogin = async () => { ...@@ -98,7 +110,7 @@ const handleLogin = async () => {
:loading="loading" :loading="loading"
type="submit" type="submit"
> >
登录 {{ $t('auth.login.buttons.signIn') }}
</v-btn> </v-btn>
</form> </form>
</v-sheet> </v-sheet>
......
<script setup> <script setup>
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { getStudents, updateStudent, createStudent, deleteStudent } from '@/api/studentService.js' import { getStudents, updateStudent, createStudent, deleteStudent } from '@/api/studentService.js'
const { t } = useI18n()
const authStore = useAuthStore() const authStore = useAuthStore()
const isLoading = ref(false) const isLoading = ref(false)
...@@ -46,10 +48,10 @@ const studentToDelete = ref(null) ...@@ -46,10 +48,10 @@ const studentToDelete = ref(null)
const isDeleting = ref(false) const isDeleting = ref(false)
const headers = [ const headers = [
{ title: 'Avatar', value: 'avatar', align: 'start', sortable: false }, { title: t('student.table.headers.avatar'), value: 'avatar', align: 'start', sortable: false },
{ title: 'Name', value: 'student_name', sortable: true }, { title: t('student.table.headers.name'), value: 'student_name', sortable: true },
{ {
title: 'Enabled', title: t('student.table.headers.enabled'),
value: 'enabled', value: 'enabled',
sortable: true, sortable: true,
sort: (a, b) => { sort: (a, b) => {
...@@ -59,9 +61,9 @@ const headers = [ ...@@ -59,9 +61,9 @@ const headers = [
return 0 return 0
} }
}, },
{ title: 'Age', value: 'age', sortable: true }, { title: t('student.table.headers.age'), value: 'age', sortable: true },
{ title: 'Grade', value: 'grade', sortable: true }, { title: t('student.table.headers.grade'), value: 'grade', sortable: true },
{ title: 'Actions', value: 'actions', sortable: false, align: 'end' }, { title: t('student.table.headers.actions'), value: 'actions', sortable: false, align: 'end' },
] ]
const fetchStudents = async () => { const fetchStudents = async () => {
...@@ -73,7 +75,7 @@ const fetchStudents = async () => { ...@@ -73,7 +75,7 @@ const fetchStudents = async () => {
students.value = Array.isArray(data) ? data : (data?.items || []) students.value = Array.isArray(data) ? data : (data?.items || [])
} catch (e) { } catch (e) {
isError.value = true isError.value = true
errorMessage.value = e?.response?.data?.detail || e?.message || 'Failed to load students' errorMessage.value = e?.response?.data?.detail || e?.message || t('student.messages.loadError')
// 3秒后自动隐藏错误消息 // 3秒后自动隐藏错误消息
setTimeout(() => { setTimeout(() => {
isError.value = false isError.value = false
...@@ -121,7 +123,7 @@ const handleAvatarUpload = (event) => { ...@@ -121,7 +123,7 @@ const handleAvatarUpload = (event) => {
// 检查文件类型 // 检查文件类型
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
saveErrorMessage.value = 'Please select an image file' saveErrorMessage.value = t('student.avatar.upload.selectImage')
// 3秒后自动隐藏错误消息 // 3秒后自动隐藏错误消息
setTimeout(() => { setTimeout(() => {
saveErrorMessage.value = '' saveErrorMessage.value = ''
...@@ -131,7 +133,7 @@ const handleAvatarUpload = (event) => { ...@@ -131,7 +133,7 @@ const handleAvatarUpload = (event) => {
// 检查文件大小 (限制为2MB) // 检查文件大小 (限制为2MB)
if (file.size > 2 * 1024 * 1024) { if (file.size > 2 * 1024 * 1024) {
saveErrorMessage.value = 'File size cannot exceed 2MB' saveErrorMessage.value = t('student.avatar.upload.sizeExceeded')
// 3秒后自动隐藏错误消息 // 3秒后自动隐藏错误消息
setTimeout(() => { setTimeout(() => {
saveErrorMessage.value = '' saveErrorMessage.value = ''
...@@ -154,7 +156,7 @@ const handleAvatarUpload = (event) => { ...@@ -154,7 +156,7 @@ const handleAvatarUpload = (event) => {
}) })
} }
reader.onerror = () => { reader.onerror = () => {
saveErrorMessage.value = 'File reading failed, please try again' saveErrorMessage.value = t('student.avatar.upload.readError')
// 3秒后自动隐藏错误消息 // 3秒后自动隐藏错误消息
setTimeout(() => { setTimeout(() => {
saveErrorMessage.value = '' saveErrorMessage.value = ''
...@@ -186,7 +188,7 @@ const handleCreateAvatarUpload = (event) => { ...@@ -186,7 +188,7 @@ const handleCreateAvatarUpload = (event) => {
// 检查文件类型 // 检查文件类型
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
createErrorMessage.value = 'Please select an image file' createErrorMessage.value = t('student.avatar.upload.selectImage')
setTimeout(() => { setTimeout(() => {
createErrorMessage.value = '' createErrorMessage.value = ''
}, 3000) }, 3000)
...@@ -195,7 +197,7 @@ const handleCreateAvatarUpload = (event) => { ...@@ -195,7 +197,7 @@ const handleCreateAvatarUpload = (event) => {
// 检查文件大小 (限制为2MB) // 检查文件大小 (限制为2MB)
if (file.size > 2 * 1024 * 1024) { if (file.size > 2 * 1024 * 1024) {
createErrorMessage.value = 'File size cannot exceed 2MB' createErrorMessage.value = t('student.avatar.upload.sizeExceeded')
setTimeout(() => { setTimeout(() => {
createErrorMessage.value = '' createErrorMessage.value = ''
}, 3000) }, 3000)
...@@ -217,7 +219,7 @@ const handleCreateAvatarUpload = (event) => { ...@@ -217,7 +219,7 @@ const handleCreateAvatarUpload = (event) => {
}) })
} }
reader.onerror = () => { reader.onerror = () => {
createErrorMessage.value = 'File reading failed, please try again' createErrorMessage.value = t('student.avatar.upload.readError')
setTimeout(() => { setTimeout(() => {
createErrorMessage.value = '' createErrorMessage.value = ''
}, 3000) }, 3000)
...@@ -344,7 +346,7 @@ const createNewStudent = async () => { ...@@ -344,7 +346,7 @@ const createNewStudent = async () => {
students.value.unshift(studentToAdd) // 在列表顶部添加新学生 students.value.unshift(studentToAdd) // 在列表顶部添加新学生
// 3. 显示成功消息 // 3. 显示成功消息
successMessage.value = 'Student created successfully' successMessage.value = t('student.messages.createSuccess')
setTimeout(() => { setTimeout(() => {
successMessage.value = '' successMessage.value = ''
}, 3000) }, 3000)
...@@ -352,7 +354,7 @@ const createNewStudent = async () => { ...@@ -352,7 +354,7 @@ const createNewStudent = async () => {
} catch (error) { } catch (error) {
console.error('Creation failed:', error) console.error('Creation failed:', error)
createErrorMessage.value = error.message || 'Creation failed, please try again' createErrorMessage.value = error.message || t('student.messages.createError')
setTimeout(() => { setTimeout(() => {
createErrorMessage.value = '' createErrorMessage.value = ''
}, 3000) }, 3000)
...@@ -393,7 +395,7 @@ const confirmDeleteStudent = async () => { ...@@ -393,7 +395,7 @@ const confirmDeleteStudent = async () => {
} }
// 3. 显示成功消息 // 3. 显示成功消息
successMessage.value = `Student "${studentToDelete.value.student_name}" deleted successfully` successMessage.value = t('student.messages.deleteSuccess', { name: studentToDelete.value.student_name })
setTimeout(() => { setTimeout(() => {
successMessage.value = '' successMessage.value = ''
}, 3000) }, 3000)
...@@ -404,7 +406,7 @@ const confirmDeleteStudent = async () => { ...@@ -404,7 +406,7 @@ const confirmDeleteStudent = async () => {
} catch (error) { } catch (error) {
console.error('Deletion failed:', error) console.error('Deletion failed:', error)
// 在删除对话框中显示错误,但不关闭对话框 // 在删除对话框中显示错误,但不关闭对话框
saveErrorMessage.value = error.message || 'Deletion failed, please try again' saveErrorMessage.value = error.message || t('student.messages.deleteError')
setTimeout(() => { setTimeout(() => {
saveErrorMessage.value = '' saveErrorMessage.value = ''
}, 3000) }, 3000)
...@@ -415,7 +417,7 @@ const confirmDeleteStudent = async () => { ...@@ -415,7 +417,7 @@ const confirmDeleteStudent = async () => {
const saveStudent = async () => { const saveStudent = async () => {
if (!editingStudent.value?.student_id) { if (!editingStudent.value?.student_id) {
saveErrorMessage.value = 'Unable to get student ID' saveErrorMessage.value = t('common.messages.error.general')
// 3秒后自动隐藏错误消息 // 3秒后自动隐藏错误消息
setTimeout(() => { setTimeout(() => {
saveErrorMessage.value = '' saveErrorMessage.value = ''
...@@ -482,7 +484,7 @@ const saveStudent = async () => { ...@@ -482,7 +484,7 @@ const saveStudent = async () => {
} }
// 3. 显示成功消息 // 3. 显示成功消息
successMessage.value = 'Student information updated successfully' successMessage.value = t('student.messages.updateSuccess')
// 3秒后自动隐藏成功消息 // 3秒后自动隐藏成功消息
setTimeout(() => { setTimeout(() => {
successMessage.value = '' successMessage.value = ''
...@@ -492,7 +494,7 @@ const saveStudent = async () => { ...@@ -492,7 +494,7 @@ const saveStudent = async () => {
} catch (error) { } catch (error) {
// 4. API失败时不更新本地数据,显示错误消息 // 4. API失败时不更新本地数据,显示错误消息
console.error('Save failed:', error) console.error('Save failed:', error)
saveErrorMessage.value = error.message || 'Save failed, please try again' saveErrorMessage.value = error.message || t('student.messages.updateError')
// 3秒后自动隐藏错误消息 // 3秒后自动隐藏错误消息
setTimeout(() => { setTimeout(() => {
saveErrorMessage.value = '' saveErrorMessage.value = ''
...@@ -507,7 +509,7 @@ const saveStudent = async () => { ...@@ -507,7 +509,7 @@ const saveStudent = async () => {
<v-container fluid> <v-container fluid>
<v-row class="mb-4" align="center" justify="space-between"> <v-row class="mb-4" align="center" justify="space-between">
<v-col cols="12" sm="6"> <v-col cols="12" sm="6">
<h2 class="text-h5">Students</h2> <h2 class="text-h5">{{ $t('student.title') }}</h2>
</v-col> </v-col>
<v-col cols="12" sm="6" class="text-sm-right text-right"> <v-col cols="12" sm="6" class="text-sm-right text-right">
<v-btn <v-btn
...@@ -516,10 +518,10 @@ const saveStudent = async () => { ...@@ -516,10 +518,10 @@ const saveStudent = async () => {
class="mr-2" class="mr-2"
@click="openCreateDialog" @click="openCreateDialog"
> >
Add Student {{ $t('student.buttons.addStudent') }}
</v-btn> </v-btn>
<v-btn color="primary" prepend-icon="mdi-refresh" :loading="isLoading" @click="fetchStudents"> <v-btn color="primary" prepend-icon="mdi-refresh" :loading="isLoading" @click="fetchStudents">
Refresh {{ $t('common.buttons.refresh') }}
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
...@@ -539,10 +541,20 @@ const saveStudent = async () => { ...@@ -539,10 +541,20 @@ const saveStudent = async () => {
:headers="headers" :headers="headers"
:items="students" :items="students"
item-key="student_id" item-key="student_id"
:items-per-page="-1" :items-per-page="10"
class="elevation-1" class="elevation-1"
:sort-by="[{ key: 'student_name', order: 'asc' }]" :sort-by="[{ key: 'student_name', order: 'asc' }]"
multi-sort multi-sort
:items-per-page-text="$t('student.table.pagination.itemsPerPage')"
:no-data-text="$t('student.table.pagination.noDataText')"
:loading-text="$t('student.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('student.table.pagination.itemsPerPageAll') }
]"
> >
<template v-slot:[`item.avatar`]="{ item }"> <template v-slot:[`item.avatar`]="{ item }">
<div class="d-flex align-center py-2"> <div class="d-flex align-center py-2">
...@@ -563,7 +575,7 @@ const saveStudent = async () => { ...@@ -563,7 +575,7 @@ const saveStudent = async () => {
</template> </template>
<template v-slot:[`item.enabled`]="{ item }"> <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'" size="small" variant="flat">
{{ item.enabled === 'Y' ? 'Enabled' : 'Disabled' }} {{ item.enabled === 'Y' ? $t('common.status.enabled') : $t('common.status.disabled') }}
</v-chip> </v-chip>
</template> </template>
<template v-slot:[`item.actions`]="{ item }"> <template v-slot:[`item.actions`]="{ item }">
...@@ -582,7 +594,7 @@ const saveStudent = async () => { ...@@ -582,7 +594,7 @@ const saveStudent = async () => {
<v-dialog v-model="editDialog" max-width="500px"> <v-dialog v-model="editDialog" max-width="500px">
<v-card> <v-card>
<v-card-title class="text-h5"> <v-card-title class="text-h5">
Edit Student Information {{ $t('student.dialog.edit') }}
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-container> <v-container>
...@@ -595,7 +607,7 @@ const saveStudent = async () => { ...@@ -595,7 +607,7 @@ const saveStudent = async () => {
<!-- Avatar upload area --> <!-- Avatar upload area -->
<v-col cols="12"> <v-col cols="12">
<div class="mb-6"> <div class="mb-6">
<h4 class="text-subtitle-1 mb-4 font-weight-medium">Avatar</h4> <h4 class="text-subtitle-1 mb-4 font-weight-medium">{{ $t('student.avatar.upload.title') }}</h4>
<!-- Avatar preview and upload area --> <!-- Avatar preview and upload area -->
<div class="d-flex flex-column align-center"> <div class="d-flex flex-column align-center">
...@@ -605,7 +617,7 @@ const saveStudent = async () => { ...@@ -605,7 +617,7 @@ const saveStudent = async () => {
<template v-if="editForm.avatar && editForm.avatar_mime_type"> <template v-if="editForm.avatar && editForm.avatar_mime_type">
<v-img <v-img
:src="getAvatarDataUrl(editForm.avatar, editForm.avatar_mime_type)" :src="getAvatarDataUrl(editForm.avatar, editForm.avatar_mime_type)"
:alt="editForm.avatar_file_name || 'avatar'" :alt="editForm.avatar_file_name || $t('student.avatar.fallback.alt')"
cover cover
/> />
</template> </template>
...@@ -650,16 +662,16 @@ const saveStudent = async () => { ...@@ -650,16 +662,16 @@ const saveStudent = async () => {
@click="$refs.fileInput.click()" @click="$refs.fileInput.click()"
:disabled="isSaving" :disabled="isSaving"
> >
{{ editForm.avatar ? 'Change Avatar' : 'Upload Avatar' }} {{ editForm.avatar ? $t('student.avatar.upload.change') : $t('student.avatar.upload.button') }}
</v-btn> </v-btn>
<!-- File format description --> <!-- File format description -->
<div class="text-center"> <div class="text-center">
<div class="text-caption text-medium-emphasis"> <div class="text-caption text-medium-emphasis">
Supports PNG, JPG, GIF formats {{ $t('student.avatar.upload.formats') }}
</div> </div>
<div class="text-caption text-medium-emphasis"> <div class="text-caption text-medium-emphasis">
File size up to 2MB {{ $t('student.avatar.upload.sizeLimit') }}
</div> </div>
</div> </div>
</div> </div>
...@@ -674,7 +686,7 @@ const saveStudent = async () => { ...@@ -674,7 +686,7 @@ const saveStudent = async () => {
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-text-field
v-model="editForm.student_name" v-model="editForm.student_name"
label="Name" :label="$t('student.form.name')"
required required
variant="outlined" variant="outlined"
/> />
...@@ -682,7 +694,7 @@ const saveStudent = async () => { ...@@ -682,7 +694,7 @@ const saveStudent = async () => {
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
v-model="editForm.age" v-model="editForm.age"
label="Age" :label="$t('student.form.age')"
type="number" type="number"
variant="outlined" variant="outlined"
/> />
...@@ -690,14 +702,14 @@ const saveStudent = async () => { ...@@ -690,14 +702,14 @@ const saveStudent = async () => {
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
v-model="editForm.grade" v-model="editForm.grade"
label="Grade" :label="$t('student.form.grade')"
variant="outlined" variant="outlined"
/> />
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<v-switch <v-switch
v-model="editForm.enabled" v-model="editForm.enabled"
label="Enabled Status" :label="$t('student.form.enabledStatus')"
color="primary" color="primary"
/> />
</v-col> </v-col>
...@@ -712,7 +724,7 @@ const saveStudent = async () => { ...@@ -712,7 +724,7 @@ const saveStudent = async () => {
@click="openDeleteDialog(editingStudent)" @click="openDeleteDialog(editingStudent)"
:disabled="isSaving" :disabled="isSaving"
> >
Delete {{ $t('common.buttons.delete') }}
</v-btn> </v-btn>
<v-spacer /> <v-spacer />
<v-btn <v-btn
...@@ -720,7 +732,7 @@ const saveStudent = async () => { ...@@ -720,7 +732,7 @@ const saveStudent = async () => {
variant="text" variant="text"
@click="closeEditDialog" @click="closeEditDialog"
> >
Cancel {{ $t('common.buttons.cancel') }}
</v-btn> </v-btn>
<v-btn <v-btn
color="primary" color="primary"
...@@ -729,7 +741,7 @@ const saveStudent = async () => { ...@@ -729,7 +741,7 @@ const saveStudent = async () => {
:disabled="isSaving" :disabled="isSaving"
@click="saveStudent" @click="saveStudent"
> >
Save {{ $t('common.buttons.save') }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
...@@ -739,7 +751,7 @@ const saveStudent = async () => { ...@@ -739,7 +751,7 @@ const saveStudent = async () => {
<v-dialog v-model="createDialog" max-width="500px"> <v-dialog v-model="createDialog" max-width="500px">
<v-card> <v-card>
<v-card-title class="text-h5"> <v-card-title class="text-h5">
Add Student {{ $t('student.dialog.add') }}
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-container> <v-container>
...@@ -752,7 +764,7 @@ const saveStudent = async () => { ...@@ -752,7 +764,7 @@ const saveStudent = async () => {
<!-- Avatar upload area --> <!-- Avatar upload area -->
<v-col cols="12"> <v-col cols="12">
<div class="mb-6"> <div class="mb-6">
<h4 class="text-subtitle-1 mb-4 font-weight-medium">Avatar</h4> <h4 class="text-subtitle-1 mb-4 font-weight-medium">{{ $t('student.avatar.upload.title') }}</h4>
<!-- Avatar preview and upload area --> <!-- Avatar preview and upload area -->
<div class="d-flex flex-column align-center"> <div class="d-flex flex-column align-center">
...@@ -762,7 +774,7 @@ const saveStudent = async () => { ...@@ -762,7 +774,7 @@ const saveStudent = async () => {
<template v-if="createForm.avatar && createForm.avatar_mime_type"> <template v-if="createForm.avatar && createForm.avatar_mime_type">
<v-img <v-img
:src="getAvatarDataUrl(createForm.avatar, createForm.avatar_mime_type)" :src="getAvatarDataUrl(createForm.avatar, createForm.avatar_mime_type)"
:alt="createForm.avatar_file_name || 'avatar'" :alt="createForm.avatar_file_name || $t('student.avatar.fallback.alt')"
cover cover
/> />
</template> </template>
...@@ -807,16 +819,16 @@ const saveStudent = async () => { ...@@ -807,16 +819,16 @@ const saveStudent = async () => {
@click="$refs.createFileInput.click()" @click="$refs.createFileInput.click()"
:disabled="isCreating" :disabled="isCreating"
> >
{{ createForm.avatar ? 'Change Avatar' : 'Upload Avatar' }} {{ createForm.avatar ? $t('student.avatar.upload.change') : $t('student.avatar.upload.button') }}
</v-btn> </v-btn>
<!-- File format description --> <!-- File format description -->
<div class="text-center"> <div class="text-center">
<div class="text-caption text-medium-emphasis"> <div class="text-caption text-medium-emphasis">
Supports PNG, JPG, GIF formats {{ $t('student.avatar.upload.formats') }}
</div> </div>
<div class="text-caption text-medium-emphasis"> <div class="text-caption text-medium-emphasis">
File size up to 2MB {{ $t('student.avatar.upload.sizeLimit') }}
</div> </div>
</div> </div>
</div> </div>
...@@ -831,7 +843,7 @@ const saveStudent = async () => { ...@@ -831,7 +843,7 @@ const saveStudent = async () => {
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-text-field
v-model="createForm.student_name" v-model="createForm.student_name"
label="Name *" :label="$t('student.form.name') + ' *'"
required required
variant="outlined" variant="outlined"
/> />
...@@ -839,7 +851,7 @@ const saveStudent = async () => { ...@@ -839,7 +851,7 @@ const saveStudent = async () => {
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
v-model="createForm.age" v-model="createForm.age"
label="Age" :label="$t('student.form.age')"
type="number" type="number"
variant="outlined" variant="outlined"
/> />
...@@ -847,14 +859,14 @@ const saveStudent = async () => { ...@@ -847,14 +859,14 @@ const saveStudent = async () => {
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
v-model="createForm.grade" v-model="createForm.grade"
label="Grade" :label="$t('student.form.grade')"
variant="outlined" variant="outlined"
/> />
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<v-switch <v-switch
v-model="createForm.enabled" v-model="createForm.enabled"
label="Enabled Status *" :label="$t('student.form.enabledStatus') + ' *'"
color="primary" color="primary"
/> />
</v-col> </v-col>
...@@ -868,7 +880,7 @@ const saveStudent = async () => { ...@@ -868,7 +880,7 @@ const saveStudent = async () => {
variant="text" variant="text"
@click="closeCreateDialog" @click="closeCreateDialog"
> >
Cancel {{ $t('common.buttons.cancel') }}
</v-btn> </v-btn>
<v-btn <v-btn
color="success" color="success"
...@@ -877,7 +889,7 @@ const saveStudent = async () => { ...@@ -877,7 +889,7 @@ const saveStudent = async () => {
:disabled="isCreating" :disabled="isCreating"
@click="createNewStudent" @click="createNewStudent"
> >
Create {{ $t('common.buttons.create') }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
...@@ -888,11 +900,11 @@ const saveStudent = async () => { ...@@ -888,11 +900,11 @@ const saveStudent = async () => {
<v-card> <v-card>
<v-card-title class="text-h5 text-error"> <v-card-title class="text-h5 text-error">
<v-icon icon="mdi-alert" class="mr-2" /> <v-icon icon="mdi-alert" class="mr-2" />
Confirm Delete {{ $t('student.dialog.delete') }}
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<div class="text-body-1 mb-4"> <div class="text-body-1 mb-4">
Are you sure you want to delete this student? {{ $t('student.deleteConfirmation.message') }}
</div> </div>
<div v-if="studentToDelete" class="bg-grey-lighten-4 pa-3 rounded"> <div v-if="studentToDelete" class="bg-grey-lighten-4 pa-3 rounded">
<div class="d-flex align-center mb-2"> <div class="d-flex align-center mb-2">
...@@ -918,7 +930,7 @@ const saveStudent = async () => { ...@@ -918,7 +930,7 @@ const saveStudent = async () => {
</div> </div>
<div class="text-body-2 text-error mt-4"> <div class="text-body-2 text-error mt-4">
<v-icon icon="mdi-alert-circle" size="small" class="mr-1" /> <v-icon icon="mdi-alert-circle" size="small" class="mr-1" />
This operation cannot be undone. Please proceed with caution. {{ $t('student.deleteConfirmation.warning') }}
</div> </div>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
...@@ -929,7 +941,7 @@ const saveStudent = async () => { ...@@ -929,7 +941,7 @@ const saveStudent = async () => {
@click="closeDeleteDialog" @click="closeDeleteDialog"
:disabled="isDeleting" :disabled="isDeleting"
> >
Cancel {{ $t('common.buttons.cancel') }}
</v-btn> </v-btn>
<v-btn <v-btn
color="error" color="error"
...@@ -939,7 +951,7 @@ const saveStudent = async () => { ...@@ -939,7 +951,7 @@ const saveStudent = async () => {
:disabled="isDeleting" :disabled="isDeleting"
@click="confirmDeleteStudent" @click="confirmDeleteStudent"
> >
Confirm Delete {{ $t('common.buttons.confirm') + ' ' + $t('common.buttons.delete') }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
......
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