Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Clark Lin
student-app-frontend
Commits
c9b259a1
Commit
c9b259a1
authored
Aug 29, 2025
by
Administrator
Browse files
added i18n support for Chinese for student page.
parent
0caedf2f
Changes
30
Hide whitespace changes
Inline
Side-by-side
src/locales/zh-CN/common/validation.js
0 → 100644
View file @
c9b259a1
export
default
{
required
:
'
此字段为必填项
'
,
email
:
'
请输入有效的邮箱地址
'
,
minLength
:
'
最少需要 {min} 个字符
'
,
maxLength
:
'
最多允许 {max} 个字符
'
,
numeric
:
'
请输入有效数字
'
,
positive
:
'
请输入正数
'
,
fileSize
:
'
文件大小不能超过 {size}
'
,
fileType
:
'
请选择有效的文件类型
'
,
imageFile
:
'
请选择图片文件
'
,
passwordMatch
:
'
密码不匹配
'
,
uniqueValue
:
'
该值已存在
'
}
src/locales/zh-CN/components/app-header.js
0 → 100644
View file @
c9b259a1
export
default
{
title
:
'
学生管理系统
'
,
search
:
{
placeholder
:
'
搜索...
'
,
button
:
'
搜索
'
},
notifications
:
{
button
:
'
通知
'
,
empty
:
'
暂无通知
'
,
title
:
'
通知
'
},
user
:
{
menu
:
'
用户菜单
'
,
profile
:
'
个人资料
'
,
settings
:
'
设置
'
,
logout
:
'
退出登录
'
},
// 登出对话框
logoutDialog
:
{
title
:
'
确认登出
'
,
message
:
'
你确定要退出登录吗?
'
,
confirm
:
'
确认
'
,
cancel
:
'
取消
'
}
}
src/locales/zh-CN/index.js
0 → 100644
View file @
c9b259a1
// 导入所有翻译模块
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
}
}
src/locales/zh-CN/modules/auth.js
0 → 100644
View file @
c9b259a1
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
:
'
退出登录成功
'
}
}
src/locales/zh-CN/modules/navigation.js
0 → 100644
View file @
c9b259a1
export
default
{
// 抽屉欢迎信息
welcome
:
{
title
:
'
欢迎使用学生管理应用
'
,
subtitle
:
'
由 vuetify 支持
'
},
menu
:
{
home
:
'
首页
'
,
profile
:
'
个人资料
'
,
masterData
:
'
基础数据
'
,
students
:
'
学生
'
,
subjects
:
'
科目
'
,
dashboard
:
'
仪表盘
'
,
settings
:
'
设置
'
,
about
:
'
关于
'
},
breadcrumb
:
{
home
:
'
首页
'
,
masterData
:
'
基础数据
'
,
students
:
'
学生
'
,
profile
:
'
个人资料
'
},
drawer
:
{
title
:
'
导航
'
,
collapse
:
'
收起菜单
'
,
expand
:
'
展开菜单
'
}
}
src/locales/zh-CN/modules/student.js
0 → 100644
View file @
c9b259a1
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
:
'
状态为必填项
'
}
}
src/main.js
View file @
c9b259a1
...
@@ -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
'
...
...
src/stores/auth.js
View file @
c9b259a1
...
@@ -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
()
...
...
src/views/LoginView.vue
View file @
c9b259a1
<
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>
...
...
src/views/StudentView.vue
View file @
c9b259a1
<
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
:
'
A
vatar
'
,
value
:
'
avatar
'
,
align
:
'
start
'
,
sortable
:
false
},
{
title
:
t
(
'
student.table.headers.a
vatar
'
)
,
value
:
'
avatar
'
,
align
:
'
start
'
,
sortable
:
false
},
{
title
:
'
N
ame
'
,
value
:
'
student_name
'
,
sortable
:
true
},
{
title
:
t
(
'
student.table.headers.n
ame
'
)
,
value
:
'
student_name
'
,
sortable
:
true
},
{
{
title
:
'
E
nabled
'
,
title
:
t
(
'
student.table.headers.e
nabled
'
)
,
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
:
'
A
ge
'
,
value
:
'
age
'
,
sortable
:
true
},
{
title
:
t
(
'
student.table.headers.a
ge
'
)
,
value
:
'
age
'
,
sortable
:
true
},
{
title
:
'
G
rade
'
,
value
:
'
grade
'
,
sortable
:
true
},
{
title
:
t
(
'
student.table.headers.g
rade
'
)
,
value
:
'
grade
'
,
sortable
:
true
},
{
title
:
'
A
ctions
'
,
value
:
'
actions
'
,
sortable
:
false
,
align
:
'
end
'
},
{
title
:
t
(
'
student.table.headers.a
ctions
'
)
,
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
=
'
S
tudent
create
d s
uccess
fully
'
successMessage
.
value
=
t
(
'
s
tudent
.messages.
create
S
uccess
'
)
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
=
`S
tudent
"
${
studentToDelete
.
value
.
student_name
}
" deleted successfully`
successMessage
.
value
=
t
(
'
s
tudent
.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
=
'
S
tudent
information
update
d s
uccess
fully
'
successMessage
.
value
=
t
(
'
s
tudent
.messages.
update
S
uccess
'
)
// 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"
>
>
A
dd
Student
{{
$t
(
'
student.buttons.a
ddStudent
'
)
}}
</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"
>
R
efresh
{{
$t
(
'
common.buttons.r
efresh
'
)
}}
</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=
"1
0
"
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
'
?
'
E
nabled
'
:
'
D
isabled
'
}}
{{
item
.
enabled
===
'
Y
'
?
$t
(
'
common.status.e
nabled
'
)
:
$t
(
'
common.status.d
isabled
'
)
}}
</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=
"
N
ame"
:
label=
"
$t('student.form.n
ame
')
"
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=
"
G
rade"
:
label=
"
$t('student.form.g
rade
')
"
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=
"
E
nabled
Status"
:
label=
"
$t('student.form.e
nabledStatus
')
"
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=
"
G
rade"
:
label=
"
$t('student.form.g
rade
')
"
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=
"
E
nabled
Status *"
:
label=
"
$t('student.form.e
nabledStatus
') + '
*
'
"
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>
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment