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
1f7f2977
Commit
1f7f2977
authored
Aug 28, 2025
by
Administrator
Browse files
added StudentView page to handle CURD operations.
parent
38326bc5
Changes
6
Show whitespace changes
Inline
Side-by-side
src/api/authService.js
View file @
1f7f2977
src/api/index.js
View file @
1f7f2977
...
...
@@ -22,6 +22,9 @@ export const apiEndpoints = {
// 学生相关
STUDENTS
:
{
LIST
:
'
/api/student/
'
,
CREATE
:
'
/api/student/
'
,
UPDATE
:
(
studentId
)
=>
`/api/student/
${
studentId
}
/`
,
DELETE
:
(
studentId
)
=>
`/api/student/
${
studentId
}
/`
,
},
}
...
...
src/api/studentService.js
View file @
1f7f2977
import
apiClient
,
{
apiEndpoints
}
from
'
./index.js
'
//
获取学生列表
//
Get student list
export
const
getStudents
=
async
()
=>
{
try
{
const
response
=
await
apiClient
.
get
(
apiEndpoints
.
STUDENTS
.
LIST
)
...
...
@@ -9,3 +9,118 @@ export const getStudents = async () => {
throw
new
Error
(
error
?.
response
?.
data
?.
detail
||
error
?.
message
||
'
Failed to fetch students
'
)
}
}
// Update student information
export
const
updateStudent
=
async
(
studentId
,
studentData
)
=>
{
try
{
// Convert data format to match API requirements
const
apiData
=
{
student_name
:
studentData
.
student_name
||
''
,
enabled
:
studentData
.
enabled
?
'
Y
'
:
'
N
'
,
grade
:
studentData
.
grade
||
''
,
age
:
parseInt
(
studentData
.
age
)
||
0
,
}
// If avatar data is included, add avatar-related fields
if
(
studentData
.
avatar
&&
studentData
.
avatar_mime_type
)
{
apiData
.
avatar_base64
=
studentData
.
avatar
apiData
.
avatar_mime_type
=
studentData
.
avatar_mime_type
apiData
.
avatar_file_name
=
studentData
.
avatar_file_name
||
''
}
console
.
log
(
'
Sending update request:
'
,
{
studentId
,
url
:
apiEndpoints
.
STUDENTS
.
UPDATE
(
studentId
),
data
:
{
...
apiData
,
avatar_base64
:
apiData
.
avatar_base64
?
'
[base64 data]
'
:
undefined
}
// Don't print full base64
})
const
response
=
await
apiClient
.
patch
(
apiEndpoints
.
STUDENTS
.
UPDATE
(
studentId
),
apiData
)
return
response
.
data
}
catch
(
error
)
{
console
.
error
(
'
API error details:
'
,
{
status
:
error
?.
response
?.
status
,
statusText
:
error
?.
response
?.
statusText
,
data
:
error
?.
response
?.
data
,
headers
:
error
?.
response
?.
headers
})
const
errorMessage
=
error
?.
response
?.
data
?.
detail
||
error
?.
response
?.
data
?.
message
||
error
?.
response
?.
data
?.
error
||
`HTTP
${
error
?.
response
?.
status
}
:
${
error
?.
response
?.
statusText
}
`
||
error
.
message
||
'
Failed to update student
'
throw
new
Error
(
errorMessage
)
}
}
// Create student
export
const
createStudent
=
async
(
studentData
)
=>
{
try
{
// Convert data format to match API requirements
const
apiData
=
{
student_name
:
studentData
.
student_name
||
''
,
enabled
:
studentData
.
enabled
?
'
Y
'
:
'
N
'
,
grade
:
studentData
.
grade
||
''
,
age
:
parseInt
(
studentData
.
age
)
||
0
,
}
// If avatar data is included, add avatar-related fields
if
(
studentData
.
avatar
&&
studentData
.
avatar_mime_type
)
{
apiData
.
avatar_base64
=
studentData
.
avatar
apiData
.
avatar_mime_type
=
studentData
.
avatar_mime_type
apiData
.
avatar_file_name
=
studentData
.
avatar_file_name
||
''
}
console
.
log
(
'
Sending create request:
'
,
{
url
:
apiEndpoints
.
STUDENTS
.
CREATE
,
data
:
{
...
apiData
,
avatar_base64
:
apiData
.
avatar_base64
?
'
[base64 data]
'
:
undefined
}
})
const
response
=
await
apiClient
.
post
(
apiEndpoints
.
STUDENTS
.
CREATE
,
apiData
)
return
response
.
data
}
catch
(
error
)
{
console
.
error
(
'
API error details:
'
,
{
status
:
error
?.
response
?.
status
,
statusText
:
error
?.
response
?.
statusText
,
data
:
error
?.
response
?.
data
,
headers
:
error
?.
response
?.
headers
})
const
errorMessage
=
error
?.
response
?.
data
?.
detail
||
error
?.
response
?.
data
?.
message
||
error
?.
response
?.
data
?.
error
||
`HTTP
${
error
?.
response
?.
status
}
:
${
error
?.
response
?.
statusText
}
`
||
error
.
message
||
'
Failed to create student
'
throw
new
Error
(
errorMessage
)
}
}
// Delete student
export
const
deleteStudent
=
async
(
studentId
)
=>
{
try
{
console
.
log
(
'
Sending delete request:
'
,
{
studentId
,
url
:
apiEndpoints
.
STUDENTS
.
DELETE
(
studentId
)
})
const
response
=
await
apiClient
.
delete
(
apiEndpoints
.
STUDENTS
.
DELETE
(
studentId
))
return
response
.
data
}
catch
(
error
)
{
console
.
error
(
'
API error details:
'
,
{
status
:
error
?.
response
?.
status
,
statusText
:
error
?.
response
?.
statusText
,
data
:
error
?.
response
?.
data
,
headers
:
error
?.
response
?.
headers
})
const
errorMessage
=
error
?.
response
?.
data
?.
detail
||
error
?.
response
?.
data
?.
message
||
error
?.
response
?.
data
?.
error
||
`HTTP
${
error
?.
response
?.
status
}
:
${
error
?.
response
?.
statusText
}
`
||
error
.
message
||
'
Failed to delete student
'
throw
new
Error
(
errorMessage
)
}
}
src/stores/auth.js
View file @
1f7f2977
import
{
ref
,
computed
}
from
'
vue
'
import
{
defineStore
}
from
'
pinia
'
import
{
useRouter
}
from
'
vue-router
'
import
a
xios
from
'
axio
s
'
import
a
piClient
from
'
@/api/index.j
s
'
import
{
login
as
authLogin
,
refreshAccessToken
as
authRefreshToken
}
from
'
@/api/authService.js
'
export
const
useAuthStore
=
defineStore
(
'
auth
'
,
()
=>
{
...
...
@@ -58,9 +58,10 @@ export const useAuthStore = defineStore('auth', () => {
const
setAxiosAuthHeader
=
(
token
)
=>
{
if
(
token
)
{
axios
.
defaults
.
headers
.
common
.
Authorization
=
`Bearer
${
token
}
`
// 设置apiClient默认headers
apiClient
.
defaults
.
headers
.
common
.
Authorization
=
`Bearer
${
token
}
`
}
else
{
delete
a
xios
.
defaults
.
headers
.
common
.
Authorization
delete
a
piClient
.
defaults
.
headers
.
common
.
Authorization
}
}
...
...
@@ -92,8 +93,8 @@ export const useAuthStore = defineStore('auth', () => {
const
setupAxiosInterceptors
=
()
=>
{
if
(
interceptorsInitialized
.
value
)
return
// 请求拦截:总是携带最新的访问令牌
a
xios
.
interceptors
.
request
.
use
((
config
)
=>
{
// 请求拦截
器
:总是携带最新的访问令牌
a
piClient
.
interceptors
.
request
.
use
((
config
)
=>
{
if
(
accessToken
.
value
)
{
config
.
headers
=
config
.
headers
||
{}
config
.
headers
.
Authorization
=
`Bearer
${
accessToken
.
value
}
`
...
...
@@ -101,8 +102,8 @@ export const useAuthStore = defineStore('auth', () => {
return
config
})
// 响应拦截:遇到 401 尝试刷新一次并重试原请求
a
xios
.
interceptors
.
response
.
use
(
// 响应拦截
器
:遇到 401 尝试刷新一次并重试原请求
a
piClient
.
interceptors
.
response
.
use
(
(
response
)
=>
response
,
async
(
error
)
=>
{
const
originalRequest
=
error
.
config
||
{}
...
...
@@ -121,7 +122,7 @@ export const useAuthStore = defineStore('auth', () => {
await
refreshAccessToken
()
originalRequest
.
headers
=
originalRequest
.
headers
||
{}
originalRequest
.
headers
.
Authorization
=
`Bearer
${
accessToken
.
value
}
`
return
a
xios
(
originalRequest
)
return
a
piClient
(
originalRequest
)
}
catch
(
e
)
{
logout
()
return
Promise
.
reject
(
e
)
...
...
src/views/LoginView.vue
View file @
1f7f2977
<
script
setup
>
import
{
ref
}
from
'
vue
'
import
{
useAuthStore
}
from
'
../stores/auth
'
import
loginImage
from
'
@/assets/images/login.jpg
'
const
authStore
=
useAuthStore
()
const
username
=
ref
(
''
)
...
...
@@ -84,6 +83,14 @@ const handleLogin = async () => {
variant=
"outlined"
density=
"comfortable"
/>
<v-alert
v-if=
"error"
type=
"error"
variant=
"tonal"
class=
"mb-4"
>
{{
error
}}
</v-alert>
<v-btn
color=
"primary"
block
...
...
src/views/StudentView.vue
View file @
1f7f2977
<
script
setup
>
import
{
onMounted
,
ref
}
from
'
vue
'
import
{
useAuthStore
}
from
'
@/stores/auth
'
import
{
getStudents
}
from
'
@/api/studentService.js
'
import
{
getStudents
,
updateStudent
,
createStudent
,
deleteStudent
}
from
'
@/api/studentService.js
'
const
authStore
=
useAuthStore
()
...
...
@@ -10,12 +10,58 @@ const isError = ref(false)
const
errorMessage
=
ref
(
''
)
const
students
=
ref
([])
// 编辑功能相关状态
const
editDialog
=
ref
(
false
)
const
editingStudent
=
ref
(
null
)
const
editForm
=
ref
({
student_name
:
''
,
age
:
''
,
grade
:
''
,
enabled
:
true
,
avatar
:
''
,
avatar_mime_type
:
''
,
avatar_file_name
:
''
})
const
isSaving
=
ref
(
false
)
const
successMessage
=
ref
(
''
)
const
saveErrorMessage
=
ref
(
''
)
// 新增功能相关状态
const
createDialog
=
ref
(
false
)
const
createForm
=
ref
({
student_name
:
''
,
age
:
''
,
grade
:
''
,
enabled
:
true
,
avatar
:
''
,
avatar_mime_type
:
''
,
avatar_file_name
:
''
})
const
isCreating
=
ref
(
false
)
const
createErrorMessage
=
ref
(
''
)
// 删除功能相关状态
const
deleteDialog
=
ref
(
false
)
const
studentToDelete
=
ref
(
null
)
const
isDeleting
=
ref
(
false
)
const
headers
=
[
{
title
:
'
Avatar
'
,
value
:
'
avatar
'
,
align
:
'
start
'
},
{
title
:
'
Name
'
,
value
:
'
student_name
'
},
{
title
:
'
Enabled
'
,
value
:
'
enabled_flag
'
},
{
title
:
'
Age
'
,
value
:
'
age
'
},
{
title
:
'
Grade
'
,
value
:
'
grade
'
},
{
title
:
'
Avatar
'
,
value
:
'
avatar
'
,
align
:
'
start
'
,
sortable
:
false
},
{
title
:
'
Name
'
,
value
:
'
student_name
'
,
sortable
:
true
},
{
title
:
'
Enabled
'
,
value
:
'
enabled
'
,
sortable
:
true
,
sort
:
(
a
,
b
)
=>
{
// Y (启用) 排在前面,N (禁用) 排在后面
if
(
a
===
'
Y
'
&&
b
===
'
N
'
)
return
-
1
if
(
a
===
'
N
'
&&
b
===
'
Y
'
)
return
1
return
0
}
},
{
title
:
'
Age
'
,
value
:
'
age
'
,
sortable
:
true
},
{
title
:
'
Grade
'
,
value
:
'
grade
'
,
sortable
:
true
},
{
title
:
'
Actions
'
,
value
:
'
actions
'
,
sortable
:
false
,
align
:
'
end
'
},
]
const
fetchStudents
=
async
()
=>
{
...
...
@@ -28,6 +74,11 @@ const fetchStudents = async () => {
}
catch
(
e
)
{
isError
.
value
=
true
errorMessage
.
value
=
e
?.
response
?.
data
?.
detail
||
e
?.
message
||
'
Failed to load students
'
// 3秒后自动隐藏错误消息
setTimeout
(()
=>
{
isError
.
value
=
false
errorMessage
.
value
=
''
},
3000
)
}
finally
{
isLoading
.
value
=
false
}
...
...
@@ -46,6 +97,410 @@ const getAvatarFallback = (name) => {
const
t
=
name
.
trim
()
return
t
?
t
.
charAt
(
0
).
toUpperCase
()
:
'
?
'
}
// 生成头像的数据URL
const
getAvatarDataUrl
=
(
avatar
,
mimeType
)
=>
{
if
(
!
avatar
||
!
mimeType
)
return
null
return
`data:
${
mimeType
}
;base64,
${
avatar
}
`
}
// 头像上传处理
const
handleAvatarUpload
=
(
event
)
=>
{
console
.
log
(
'
File upload event:
'
,
event
)
// 获取文件 - Vuetify 3 的 v-file-input 事件处理
const
files
=
event
.
target
?.
files
||
event
const
file
=
Array
.
isArray
(
files
)
?
files
[
0
]
:
files
?.[
0
]
||
files
console
.
log
(
'
Selected file:
'
,
file
)
if
(
!
file
)
{
console
.
log
(
'
No file selected
'
)
return
}
// 检查文件类型
if
(
!
file
.
type
.
startsWith
(
'
image/
'
))
{
saveErrorMessage
.
value
=
'
Please select an image file
'
// 3秒后自动隐藏错误消息
setTimeout
(()
=>
{
saveErrorMessage
.
value
=
''
},
3000
)
return
}
// 检查文件大小 (限制为2MB)
if
(
file
.
size
>
2
*
1024
*
1024
)
{
saveErrorMessage
.
value
=
'
File size cannot exceed 2MB
'
// 3秒后自动隐藏错误消息
setTimeout
(()
=>
{
saveErrorMessage
.
value
=
''
},
3000
)
return
}
const
reader
=
new
FileReader
()
reader
.
onload
=
(
e
)
=>
{
const
base64
=
e
.
target
.
result
.
split
(
'
,
'
)[
1
]
// 移除数据URL前缀
editForm
.
value
.
avatar
=
base64
editForm
.
value
.
avatar_mime_type
=
file
.
type
editForm
.
value
.
avatar_file_name
=
file
.
name
saveErrorMessage
.
value
=
''
// 清除错误消息
console
.
log
(
'
Avatar data set:
'
,
{
avatar_length
:
base64
?.
length
,
mime_type
:
file
.
type
,
file_name
:
file
.
name
})
}
reader
.
onerror
=
()
=>
{
saveErrorMessage
.
value
=
'
File reading failed, please try again
'
// 3秒后自动隐藏错误消息
setTimeout
(()
=>
{
saveErrorMessage
.
value
=
''
},
3000
)
}
reader
.
readAsDataURL
(
file
)
}
// 移除头像
const
removeAvatar
=
()
=>
{
editForm
.
value
.
avatar
=
''
editForm
.
value
.
avatar_mime_type
=
''
editForm
.
value
.
avatar_file_name
=
''
}
// 新增功能的头像上传处理
const
handleCreateAvatarUpload
=
(
event
)
=>
{
console
.
log
(
'
Create file upload event:
'
,
event
)
const
files
=
event
.
target
?.
files
||
event
const
file
=
Array
.
isArray
(
files
)
?
files
[
0
]
:
files
?.[
0
]
||
files
console
.
log
(
'
Selected file:
'
,
file
)
if
(
!
file
)
{
console
.
log
(
'
No file selected
'
)
return
}
// 检查文件类型
if
(
!
file
.
type
.
startsWith
(
'
image/
'
))
{
createErrorMessage
.
value
=
'
Please select an image file
'
setTimeout
(()
=>
{
createErrorMessage
.
value
=
''
},
3000
)
return
}
// 检查文件大小 (限制为2MB)
if
(
file
.
size
>
2
*
1024
*
1024
)
{
createErrorMessage
.
value
=
'
File size cannot exceed 2MB
'
setTimeout
(()
=>
{
createErrorMessage
.
value
=
''
},
3000
)
return
}
const
reader
=
new
FileReader
()
reader
.
onload
=
(
e
)
=>
{
const
base64
=
e
.
target
.
result
.
split
(
'
,
'
)[
1
]
createForm
.
value
.
avatar
=
base64
createForm
.
value
.
avatar_mime_type
=
file
.
type
createForm
.
value
.
avatar_file_name
=
file
.
name
createErrorMessage
.
value
=
''
console
.
log
(
'
Create avatar data set:
'
,
{
avatar_length
:
base64
?.
length
,
mime_type
:
file
.
type
,
file_name
:
file
.
name
})
}
reader
.
onerror
=
()
=>
{
createErrorMessage
.
value
=
'
File reading failed, please try again
'
setTimeout
(()
=>
{
createErrorMessage
.
value
=
''
},
3000
)
}
reader
.
readAsDataURL
(
file
)
}
// 移除新增头像
const
removeCreateAvatar
=
()
=>
{
createForm
.
value
.
avatar
=
''
createForm
.
value
.
avatar_mime_type
=
''
createForm
.
value
.
avatar_file_name
=
''
}
// 编辑功能方法
const
openEditDialog
=
(
student
)
=>
{
editingStudent
.
value
=
student
editForm
.
value
=
{
student_name
:
student
.
student_name
||
''
,
age
:
student
.
age
||
''
,
grade
:
student
.
grade
||
''
,
// 处理不同的enabled数据格式
enabled
:
student
.
enabled
===
true
||
student
.
enabled
===
'
Y
'
||
student
.
enabled
===
1
,
avatar
:
student
.
avatar
||
''
,
avatar_mime_type
:
student
.
avatar_mime_type
||
''
,
avatar_file_name
:
student
.
avatar_file_name
||
''
}
console
.
log
(
'
Editing student data:
'
,
student
)
console
.
log
(
'
Converted form data:
'
,
editForm
.
value
)
editDialog
.
value
=
true
}
const
closeEditDialog
=
()
=>
{
editDialog
.
value
=
false
editingStudent
.
value
=
null
editForm
.
value
=
{
student_name
:
''
,
age
:
''
,
grade
:
''
,
enabled
:
true
,
avatar
:
''
,
avatar_mime_type
:
''
,
avatar_file_name
:
''
}
}
// 新增功能方法
const
openCreateDialog
=
()
=>
{
createForm
.
value
=
{
student_name
:
''
,
age
:
''
,
grade
:
''
,
enabled
:
true
,
avatar
:
''
,
avatar_mime_type
:
''
,
avatar_file_name
:
''
}
createErrorMessage
.
value
=
''
createDialog
.
value
=
true
}
const
closeCreateDialog
=
()
=>
{
createDialog
.
value
=
false
createForm
.
value
=
{
student_name
:
''
,
age
:
''
,
grade
:
''
,
enabled
:
true
,
avatar
:
''
,
avatar_mime_type
:
''
,
avatar_file_name
:
''
}
createErrorMessage
.
value
=
''
}
const
createNewStudent
=
async
()
=>
{
isCreating
.
value
=
true
createErrorMessage
.
value
=
''
successMessage
.
value
=
''
try
{
console
.
log
(
'
Form data before creation:
'
,
createForm
.
value
)
// 构建要发送的数据
const
createData
=
{
student_name
:
createForm
.
value
.
student_name
,
grade
:
createForm
.
value
.
grade
,
age
:
createForm
.
value
.
age
,
enabled
:
createForm
.
value
.
enabled
}
// 如果有头像数据,则包含头像字段
if
(
createForm
.
value
.
avatar
&&
createForm
.
value
.
avatar_mime_type
)
{
createData
.
avatar
=
createForm
.
value
.
avatar
createData
.
avatar_mime_type
=
createForm
.
value
.
avatar_mime_type
createData
.
avatar_file_name
=
createForm
.
value
.
avatar_file_name
console
.
log
(
'
Including avatar data:
'
,
{
avatar_length
:
createData
.
avatar
?.
length
,
mime_type
:
createData
.
avatar_mime_type
,
file_name
:
createData
.
avatar_file_name
})
}
else
{
console
.
log
(
'
Not including avatar data
'
)
}
console
.
log
(
'
Sending data:
'
,
{
...
createData
,
avatar_base64
:
createData
.
avatar
?
'
[base64 data]
'
:
undefined
})
// 1. 调用API创建学生
const
newStudent
=
await
createStudent
(
createData
)
// 2. 创建成功后,将新学生添加到本地列表
const
studentToAdd
=
{
...
newStudent
,
enabled
:
createForm
.
value
.
enabled
?
'
Y
'
:
'
N
'
}
// 如果有头像数据,则添加头像信息
if
(
createForm
.
value
.
avatar
&&
createForm
.
value
.
avatar_mime_type
)
{
studentToAdd
.
avatar
=
createForm
.
value
.
avatar
studentToAdd
.
avatar_mime_type
=
createForm
.
value
.
avatar_mime_type
studentToAdd
.
avatar_file_name
=
createForm
.
value
.
avatar_file_name
}
students
.
value
.
unshift
(
studentToAdd
)
// 在列表顶部添加新学生
// 3. 显示成功消息
successMessage
.
value
=
'
Student created successfully
'
setTimeout
(()
=>
{
successMessage
.
value
=
''
},
3000
)
closeCreateDialog
()
}
catch
(
error
)
{
console
.
error
(
'
Creation failed:
'
,
error
)
createErrorMessage
.
value
=
error
.
message
||
'
Creation failed, please try again
'
setTimeout
(()
=>
{
createErrorMessage
.
value
=
''
},
3000
)
}
finally
{
isCreating
.
value
=
false
}
}
// 删除功能方法
const
openDeleteDialog
=
(
student
)
=>
{
studentToDelete
.
value
=
student
deleteDialog
.
value
=
true
}
const
closeDeleteDialog
=
()
=>
{
deleteDialog
.
value
=
false
studentToDelete
.
value
=
null
}
const
confirmDeleteStudent
=
async
()
=>
{
if
(
!
studentToDelete
.
value
?.
student_id
)
{
console
.
error
(
'
Unable to get student ID
'
)
return
}
isDeleting
.
value
=
true
try
{
console
.
log
(
'
Deleting student:
'
,
studentToDelete
.
value
)
// 1. 调用API删除学生
await
deleteStudent
(
studentToDelete
.
value
.
student_id
)
// 2. 从本地列表中移除学生
const
index
=
students
.
value
.
findIndex
(
s
=>
s
.
student_id
===
studentToDelete
.
value
.
student_id
)
if
(
index
!==
-
1
)
{
students
.
value
.
splice
(
index
,
1
)
}
// 3. 显示成功消息
successMessage
.
value
=
`Student "
${
studentToDelete
.
value
.
student_name
}
" deleted successfully`
setTimeout
(()
=>
{
successMessage
.
value
=
''
},
3000
)
closeDeleteDialog
()
closeEditDialog
()
// 自动关闭编辑对话框
}
catch
(
error
)
{
console
.
error
(
'
Deletion failed:
'
,
error
)
// 在删除对话框中显示错误,但不关闭对话框
saveErrorMessage
.
value
=
error
.
message
||
'
Deletion failed, please try again
'
setTimeout
(()
=>
{
saveErrorMessage
.
value
=
''
},
3000
)
}
finally
{
isDeleting
.
value
=
false
}
}
const
saveStudent
=
async
()
=>
{
if
(
!
editingStudent
.
value
?.
student_id
)
{
saveErrorMessage
.
value
=
'
Unable to get student ID
'
// 3秒后自动隐藏错误消息
setTimeout
(()
=>
{
saveErrorMessage
.
value
=
''
},
3000
)
return
}
isSaving
.
value
=
true
saveErrorMessage
.
value
=
''
successMessage
.
value
=
''
try
{
console
.
log
(
'
Form data before saving:
'
,
editForm
.
value
)
// 构建要发送的数据
const
updateData
=
{
student_name
:
editForm
.
value
.
student_name
,
grade
:
editForm
.
value
.
grade
,
age
:
editForm
.
value
.
age
,
enabled
:
editForm
.
value
.
enabled
}
// 如果有头像数据,则包含头像字段
if
(
editForm
.
value
.
avatar
&&
editForm
.
value
.
avatar_mime_type
)
{
updateData
.
avatar
=
editForm
.
value
.
avatar
updateData
.
avatar_mime_type
=
editForm
.
value
.
avatar_mime_type
updateData
.
avatar_file_name
=
editForm
.
value
.
avatar_file_name
console
.
log
(
'
Including avatar data:
'
,
{
avatar_length
:
updateData
.
avatar
?.
length
,
mime_type
:
updateData
.
avatar_mime_type
,
file_name
:
updateData
.
avatar_file_name
})
}
else
{
console
.
log
(
'
Not including avatar data:
'
,
{
avatar
:
editForm
.
value
.
avatar
,
avatar_mime_type
:
editForm
.
value
.
avatar_mime_type
})
}
console
.
log
(
'
Sending data:
'
,
{
...
updateData
,
avatar_base64
:
updateData
.
avatar
?
'
[base64 data]
'
:
undefined
})
// 1. 调用API保存到服务器
await
updateStudent
(
editingStudent
.
value
.
student_id
,
updateData
)
// 2. API调用成功后,更新本地数据
const
index
=
students
.
value
.
findIndex
(
s
=>
s
.
student_id
===
editingStudent
.
value
.
student_id
)
if
(
index
!==
-
1
)
{
const
updatedStudent
=
{
...
students
.
value
[
index
],
student_name
:
editForm
.
value
.
student_name
,
grade
:
editForm
.
value
.
grade
,
age
:
editForm
.
value
.
age
,
enabled
:
editForm
.
value
.
enabled
?
'
Y
'
:
'
N
'
// 转换为API格式
}
// 如果有头像数据,则更新头像信息
if
(
editForm
.
value
.
avatar
&&
editForm
.
value
.
avatar_mime_type
)
{
updatedStudent
.
avatar
=
editForm
.
value
.
avatar
updatedStudent
.
avatar_mime_type
=
editForm
.
value
.
avatar_mime_type
updatedStudent
.
avatar_file_name
=
editForm
.
value
.
avatar_file_name
}
students
.
value
[
index
]
=
updatedStudent
}
// 3. 显示成功消息
successMessage
.
value
=
'
Student information updated successfully
'
// 3秒后自动隐藏成功消息
setTimeout
(()
=>
{
successMessage
.
value
=
''
},
3000
)
closeEditDialog
()
}
catch
(
error
)
{
// 4. API失败时不更新本地数据,显示错误消息
console
.
error
(
'
Save failed:
'
,
error
)
saveErrorMessage
.
value
=
error
.
message
||
'
Save failed, please try again
'
// 3秒后自动隐藏错误消息
setTimeout
(()
=>
{
saveErrorMessage
.
value
=
''
},
3000
)
}
finally
{
isSaving
.
value
=
false
}
}
</
script
>
<
template
>
...
...
@@ -55,6 +510,14 @@ const getAvatarFallback = (name) => {
<h2
class=
"text-h5"
>
Students
</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"
>
Add Student
</v-btn>
<v-btn
color=
"primary"
prepend-icon=
"mdi-refresh"
:loading=
"isLoading"
@
click=
"fetchStudents"
>
Refresh
</v-btn>
...
...
@@ -65,25 +528,422 @@ const getAvatarFallback = (name) => {
{{
errorMessage
}}
</v-alert>
<!-- 成功消息 -->
<v-alert
v-if=
"successMessage"
type=
"success"
class=
"mb-4"
closable
@
click:close=
"successMessage = ''"
>
{{
successMessage
}}
</v-alert>
<v-progress-linear
v-if=
"isLoading"
indeterminate
color=
"primary"
class=
"mb-4"
></v-progress-linear>
<v-data-table
:headers=
"headers"
:items=
"students"
item-key=
"id"
:items-per-page=
"-1"
class=
"elevation-1"
>
<v-data-table
:headers=
"headers"
:items=
"students"
item-key=
"student_id"
:items-per-page=
"-1"
class=
"elevation-1"
:sort-by=
"[
{ key: 'student_name', order: 'asc' }]"
multi-sort
>
<template
v-slot:
[`
item.avatar
`
]=
"
{ item }">
<v-avatar
size=
"36"
>
<template
v-if=
"item.avatar"
>
<v-img
:src=
"item.avatar"
alt=
"avatar"
cover
></v-img>
<div
class=
"d-flex align-center py-2"
>
<v-avatar
size=
"64"
>
<template
v-if=
"item.avatar && item.avatar_mime_type"
>
<v-img
:src=
"getAvatarDataUrl(item.avatar, item.avatar_mime_type)"
:alt=
"item.avatar_file_name || 'avatar'"
cover
@
error=
"console.warn('Avatar loading failed:', item.avatar_file_name)"
/>
</
template
>
<
template
v-else
>
<span>
{{
getAvatarFallback
(
item
.
student_name
)
}}
</span>
<span
class=
"text-body-2"
>
{{
getAvatarFallback
(
item
.
student_name
)
}}
</span>
</
template
>
</v-avatar>
</div>
</template>
<
template
v-slot:
[`
item.enabled
_flag
`
]=
"{ item }"
>
<v-chip
:color=
"item.enabled
_flag
? 'success' : 'error'"
size=
"small"
variant=
"flat"
>
{{
item
.
enabled
_flag
?
'
Enabled
'
:
'
Disabled
'
}}
<
template
v-slot:
[`
item.enabled
`
]=
"{ item }"
>
<v-chip
:color=
"item.enabled
=== 'Y'
? 'success' : 'error'"
size=
"small"
variant=
"flat"
>
{{
item
.
enabled
===
'
Y
'
?
'
Enabled
'
:
'
Disabled
'
}}
</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-dialog
v-model=
"editDialog"
max-width=
"500px"
>
<v-card>
<v-card-title
class=
"text-h5"
>
Edit Student Information
</v-card-title>
<v-card-text>
<v-container>
<!-- 保存错误消息 -->
<v-alert
v-if=
"saveErrorMessage"
type=
"error"
class=
"mb-4"
closable
@
click:close=
"saveErrorMessage = ''"
>
{{ saveErrorMessage }}
</v-alert>
<v-row>
<!-- Avatar upload area -->
<v-col
cols=
"12"
>
<div
class=
"mb-6"
>
<h4
class=
"text-subtitle-1 mb-4 font-weight-medium"
>
Avatar
</h4>
<!-- Avatar preview and upload area -->
<div
class=
"d-flex flex-column align-center"
>
<!-- Avatar preview area -->
<div
class=
"position-relative mb-4"
>
<v-avatar
size=
"120"
class=
"elevation-4"
>
<
template
v-if=
"editForm.avatar && editForm.avatar_mime_type"
>
<v-img
:src=
"getAvatarDataUrl(editForm.avatar, editForm.avatar_mime_type)"
:alt=
"editForm.avatar_file_name || 'avatar'"
cover
/>
</
template
>
<
template
v-else
>
<div
class=
"d-flex flex-column align-center justify-center h-100 bg-grey-lighten-3"
>
<v-icon
size=
"48"
color=
"grey-darken-1"
>
mdi-account-circle
</v-icon>
<span
class=
"text-caption text-grey-darken-1 mt-1"
>
{{
getAvatarFallback
(
editForm
.
student_name
)
}}
</span>
</div>
</
template
>
</v-avatar>
<!-- Remove avatar button -->
<v-btn
v-if=
"editForm.avatar"
icon=
"mdi-close"
size=
"small"
color=
"error"
variant=
"elevated"
class=
"position-absolute"
style=
"top: -8px; right: -8px;"
@
click=
"removeAvatar"
/>
</div>
<!-- Upload button area -->
<div
class=
"d-flex flex-column align-center gap-3"
>
<!-- Hidden file input -->
<input
ref=
"fileInput"
type=
"file"
accept=
"image/*"
style=
"display: none;"
@
change=
"handleAvatarUpload"
/>
<!-- Upload button -->
<v-btn
color=
"primary"
variant=
"outlined"
size=
"large"
prepend-icon=
"mdi-camera-plus"
@
click=
"$refs.fileInput.click()"
:disabled=
"isSaving"
>
{{ editForm.avatar ? 'Change Avatar' : 'Upload Avatar' }}
</v-btn>
<!-- File format description -->
<div
class=
"text-center"
>
<div
class=
"text-caption text-medium-emphasis"
>
Supports PNG, JPG, GIF formats
</div>
<div
class=
"text-caption text-medium-emphasis"
>
File size up to 2MB
</div>
</div>
</div>
</div>
</div>
<!-- Divider -->
<v-divider
class=
"mb-4"
/>
</v-col>
<!-- Form fields -->
<v-col
cols=
"12"
>
<v-text-field
v-model=
"editForm.student_name"
label=
"Name"
required
variant=
"outlined"
/>
</v-col>
<v-col
cols=
"6"
>
<v-text-field
v-model=
"editForm.age"
label=
"Age"
type=
"number"
variant=
"outlined"
/>
</v-col>
<v-col
cols=
"6"
>
<v-text-field
v-model=
"editForm.grade"
label=
"Grade"
variant=
"outlined"
/>
</v-col>
<v-col
cols=
"12"
>
<v-switch
v-model=
"editForm.enabled"
label=
"Enabled Status"
color=
"primary"
/>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-btn
color=
"error"
variant=
"outlined"
prepend-icon=
"mdi-delete"
@
click=
"openDeleteDialog(editingStudent)"
:disabled=
"isSaving"
>
Delete
</v-btn>
<v-spacer
/>
<v-btn
color=
"grey-darken-1"
variant=
"text"
@
click=
"closeEditDialog"
>
Cancel
</v-btn>
<v-btn
color=
"primary"
variant=
"text"
:loading=
"isSaving"
:disabled=
"isSaving"
@
click=
"saveStudent"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Add Student Dialog -->
<v-dialog
v-model=
"createDialog"
max-width=
"500px"
>
<v-card>
<v-card-title
class=
"text-h5"
>
Add Student
</v-card-title>
<v-card-text>
<v-container>
<!-- Error messages -->
<v-alert
v-if=
"createErrorMessage"
type=
"error"
class=
"mb-4"
closable
@
click:close=
"createErrorMessage = ''"
>
{{ createErrorMessage }}
</v-alert>
<v-row>
<!-- Avatar upload area -->
<v-col
cols=
"12"
>
<div
class=
"mb-6"
>
<h4
class=
"text-subtitle-1 mb-4 font-weight-medium"
>
Avatar
</h4>
<!-- Avatar preview and upload area -->
<div
class=
"d-flex flex-column align-center"
>
<!-- Avatar preview area -->
<div
class=
"position-relative mb-4"
>
<v-avatar
size=
"120"
class=
"elevation-4"
>
<
template
v-if=
"createForm.avatar && createForm.avatar_mime_type"
>
<v-img
:src=
"getAvatarDataUrl(createForm.avatar, createForm.avatar_mime_type)"
:alt=
"createForm.avatar_file_name || 'avatar'"
cover
/>
</
template
>
<
template
v-else
>
<div
class=
"d-flex flex-column align-center justify-center h-100 bg-grey-lighten-3"
>
<v-icon
size=
"48"
color=
"grey-darken-1"
>
mdi-account-circle
</v-icon>
<span
class=
"text-caption text-grey-darken-1 mt-1"
>
{{
getAvatarFallback
(
createForm
.
student_name
)
}}
</span>
</div>
</
template
>
</v-avatar>
<!-- Remove avatar button -->
<v-btn
v-if=
"createForm.avatar"
icon=
"mdi-close"
size=
"small"
color=
"error"
variant=
"elevated"
class=
"position-absolute"
style=
"top: -8px; right: -8px;"
@
click=
"removeCreateAvatar"
/>
</div>
<!-- Upload button area -->
<div
class=
"d-flex flex-column align-center gap-3"
>
<!-- Hidden file input -->
<input
ref=
"createFileInput"
type=
"file"
accept=
"image/*"
style=
"display: none;"
@
change=
"handleCreateAvatarUpload"
/>
<!-- Upload button -->
<v-btn
color=
"primary"
variant=
"outlined"
size=
"large"
prepend-icon=
"mdi-camera-plus"
@
click=
"$refs.createFileInput.click()"
:disabled=
"isCreating"
>
{{ createForm.avatar ? 'Change Avatar' : 'Upload Avatar' }}
</v-btn>
<!-- File format description -->
<div
class=
"text-center"
>
<div
class=
"text-caption text-medium-emphasis"
>
Supports PNG, JPG, GIF formats
</div>
<div
class=
"text-caption text-medium-emphasis"
>
File size up to 2MB
</div>
</div>
</div>
</div>
</div>
<!-- Divider -->
<v-divider
class=
"mb-4"
/>
</v-col>
<!-- Form fields -->
<v-col
cols=
"12"
>
<v-text-field
v-model=
"createForm.student_name"
label=
"Name *"
required
variant=
"outlined"
/>
</v-col>
<v-col
cols=
"6"
>
<v-text-field
v-model=
"createForm.age"
label=
"Age"
type=
"number"
variant=
"outlined"
/>
</v-col>
<v-col
cols=
"6"
>
<v-text-field
v-model=
"createForm.grade"
label=
"Grade"
variant=
"outlined"
/>
</v-col>
<v-col
cols=
"12"
>
<v-switch
v-model=
"createForm.enabled"
label=
"Enabled Status *"
color=
"primary"
/>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer
/>
<v-btn
color=
"grey-darken-1"
variant=
"text"
@
click=
"closeCreateDialog"
>
Cancel
</v-btn>
<v-btn
color=
"success"
variant=
"text"
:loading=
"isCreating"
:disabled=
"isCreating"
@
click=
"createNewStudent"
>
Create
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Delete Confirmation 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"
/>
Confirm Delete
</v-card-title>
<v-card-text>
<div
class=
"text-body-1 mb-4"
>
Are you sure you want to delete this student?
</div>
<div
v-if=
"studentToDelete"
class=
"bg-grey-lighten-4 pa-3 rounded"
>
<div
class=
"d-flex align-center mb-2"
>
<v-avatar
size=
"40"
class=
"mr-3"
>
<
template
v-if=
"studentToDelete.avatar && studentToDelete.avatar_mime_type"
>
<v-img
:src=
"getAvatarDataUrl(studentToDelete.avatar, studentToDelete.avatar_mime_type)"
:alt=
"studentToDelete.avatar_file_name || 'avatar'"
cover
/>
</
template
>
<
template
v-else
>
<span
class=
"text-body-2"
>
{{
getAvatarFallback
(
studentToDelete
.
student_name
)
}}
</span>
</
template
>
</v-avatar>
<div>
<div
class=
"font-weight-medium"
>
{{ studentToDelete.student_name }}
</div>
<div
class=
"text-caption text-medium-emphasis"
>
{{ studentToDelete.age }} years old | {{ studentToDelete.grade }}
</div>
</div>
</div>
</div>
<div
class=
"text-body-2 text-error mt-4"
>
<v-icon
icon=
"mdi-alert-circle"
size=
"small"
class=
"mr-1"
/>
This operation cannot be undone. Please proceed with caution.
</div>
</v-card-text>
<v-card-actions>
<v-spacer
/>
<v-btn
color=
"grey-darken-1"
variant=
"text"
@
click=
"closeDeleteDialog"
:disabled=
"isDeleting"
>
Cancel
</v-btn>
<v-btn
color=
"error"
variant=
"elevated"
prepend-icon=
"mdi-delete"
:loading=
"isDeleting"
:disabled=
"isDeleting"
@
click=
"confirmDeleteStudent"
>
Confirm Delete
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
...
...
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