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'
// 引入Vuetify
import vuetify from './plugins/vuetify'
// 引入i18n
import i18n from './locales'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(vuetify)
app.use(i18n)
// 初始化认证状态
import { useAuthStore } from './stores/auth'
......
......@@ -109,8 +109,16 @@ export const useAuthStore = defineStore('auth', () => {
const originalRequest = error.config || {}
const status = error?.response?.status
const isUnauthorized = status === 401
// 如果不是401错误,直接抛出
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) {
// 已重试过,仍然 401,直接登出
logout()
......
<script setup>
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '../stores/auth'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
const { t } = useI18n()
const authStore = useAuthStore()
const username = ref('')
const password = ref('')
......@@ -10,7 +13,7 @@ const error = ref('')
const handleLogin = async () => {
if (!username.value || !password.value) {
error.value = '请填写用户名和密码'
error.value = t('auth.login.messages.fillRequired')
return
}
......@@ -24,10 +27,14 @@ const handleLogin = async () => {
})
if (!result.success) {
error.value = result.error || '登录失败'
error.value = result.error || t('auth.login.messages.loginFailed')
// 登录失败时清空密码字段,提升安全性和用户体验
password.value = ''
}
} catch {
error.value = '登录过程中发生错误'
error.value = t('auth.login.messages.loginError')
// 登录错误时也清空密码字段
password.value = ''
} finally {
loading.value = false
}
......@@ -56,10 +63,15 @@ const handleLogin = async () => {
class="pa-8 h-100 justify-center align-center flex-grow-1"
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%;">
<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">
欢迎使用学生管理系统!本系统是用来追踪学生课程、作业、考试的一站式服务平台。请登录使用全部功能。
{{ $t('auth.login.description') }}
</div>
<v-row justify="center" class="pa-6">
<v-avatar size="128" class="mb-4" >
......@@ -68,7 +80,7 @@ const handleLogin = async () => {
</v-row>
<v-text-field
v-model="username"
label="用户名"
:label="$t('auth.login.form.username')"
prepend-inner-icon="mdi-account"
class="mb-4"
variant="outlined"
......@@ -76,7 +88,7 @@ const handleLogin = async () => {
/>
<v-text-field
v-model="password"
label="密码"
:label="$t('auth.login.form.password')"
type="password"
prepend-inner-icon="mdi-lock"
class="mb-6"
......@@ -98,7 +110,7 @@ const handleLogin = async () => {
:loading="loading"
type="submit"
>
登录
{{ $t('auth.login.buttons.signIn') }}
</v-btn>
</form>
</v-sheet>
......
<script setup>
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { getStudents, updateStudent, createStudent, deleteStudent } from '@/api/studentService.js'
const { t } = useI18n()
const authStore = useAuthStore()
const isLoading = ref(false)
......@@ -46,10 +48,10 @@ const studentToDelete = ref(null)
const isDeleting = ref(false)
const headers = [
{ title: 'Avatar', value: 'avatar', align: 'start', sortable: false },
{ title: 'Name', value: 'student_name', sortable: true },
{ title: t('student.table.headers.avatar'), value: 'avatar', align: 'start', sortable: false },
{ title: t('student.table.headers.name'), value: 'student_name', sortable: true },
{
title: 'Enabled',
title: t('student.table.headers.enabled'),
value: 'enabled',
sortable: true,
sort: (a, b) => {
......@@ -59,9 +61,9 @@ const headers = [
return 0
}
},
{ title: 'Age', value: 'age', sortable: true },
{ title: 'Grade', value: 'grade', sortable: true },
{ title: 'Actions', value: 'actions', sortable: false, align: 'end' },
{ title: t('student.table.headers.age'), value: 'age', sortable: true },
{ title: t('student.table.headers.grade'), value: 'grade', sortable: true },
{ title: t('student.table.headers.actions'), value: 'actions', sortable: false, align: 'end' },
]
const fetchStudents = async () => {
......@@ -73,7 +75,7 @@ const fetchStudents = async () => {
students.value = Array.isArray(data) ? data : (data?.items || [])
} catch (e) {
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秒后自动隐藏错误消息
setTimeout(() => {
isError.value = false
......@@ -121,7 +123,7 @@ const handleAvatarUpload = (event) => {
// 检查文件类型
if (!file.type.startsWith('image/')) {
saveErrorMessage.value = 'Please select an image file'
saveErrorMessage.value = t('student.avatar.upload.selectImage')
// 3秒后自动隐藏错误消息
setTimeout(() => {
saveErrorMessage.value = ''
......@@ -131,7 +133,7 @@ const handleAvatarUpload = (event) => {
// 检查文件大小 (限制为2MB)
if (file.size > 2 * 1024 * 1024) {
saveErrorMessage.value = 'File size cannot exceed 2MB'
saveErrorMessage.value = t('student.avatar.upload.sizeExceeded')
// 3秒后自动隐藏错误消息
setTimeout(() => {
saveErrorMessage.value = ''
......@@ -154,7 +156,7 @@ const handleAvatarUpload = (event) => {
})
}
reader.onerror = () => {
saveErrorMessage.value = 'File reading failed, please try again'
saveErrorMessage.value = t('student.avatar.upload.readError')
// 3秒后自动隐藏错误消息
setTimeout(() => {
saveErrorMessage.value = ''
......@@ -186,7 +188,7 @@ const handleCreateAvatarUpload = (event) => {
// 检查文件类型
if (!file.type.startsWith('image/')) {
createErrorMessage.value = 'Please select an image file'
createErrorMessage.value = t('student.avatar.upload.selectImage')
setTimeout(() => {
createErrorMessage.value = ''
}, 3000)
......@@ -195,7 +197,7 @@ const handleCreateAvatarUpload = (event) => {
// 检查文件大小 (限制为2MB)
if (file.size > 2 * 1024 * 1024) {
createErrorMessage.value = 'File size cannot exceed 2MB'
createErrorMessage.value = t('student.avatar.upload.sizeExceeded')
setTimeout(() => {
createErrorMessage.value = ''
}, 3000)
......@@ -217,7 +219,7 @@ const handleCreateAvatarUpload = (event) => {
})
}
reader.onerror = () => {
createErrorMessage.value = 'File reading failed, please try again'
createErrorMessage.value = t('student.avatar.upload.readError')
setTimeout(() => {
createErrorMessage.value = ''
}, 3000)
......@@ -344,7 +346,7 @@ const createNewStudent = async () => {
students.value.unshift(studentToAdd) // 在列表顶部添加新学生
// 3. 显示成功消息
successMessage.value = 'Student created successfully'
successMessage.value = t('student.messages.createSuccess')
setTimeout(() => {
successMessage.value = ''
}, 3000)
......@@ -352,7 +354,7 @@ const createNewStudent = async () => {
} catch (error) {
console.error('Creation failed:', error)
createErrorMessage.value = error.message || 'Creation failed, please try again'
createErrorMessage.value = error.message || t('student.messages.createError')
setTimeout(() => {
createErrorMessage.value = ''
}, 3000)
......@@ -393,7 +395,7 @@ const confirmDeleteStudent = async () => {
}
// 3. 显示成功消息
successMessage.value = `Student "${studentToDelete.value.student_name}" deleted successfully`
successMessage.value = t('student.messages.deleteSuccess', { name: studentToDelete.value.student_name })
setTimeout(() => {
successMessage.value = ''
}, 3000)
......@@ -404,7 +406,7 @@ const confirmDeleteStudent = async () => {
} catch (error) {
console.error('Deletion failed:', error)
// 在删除对话框中显示错误,但不关闭对话框
saveErrorMessage.value = error.message || 'Deletion failed, please try again'
saveErrorMessage.value = error.message || t('student.messages.deleteError')
setTimeout(() => {
saveErrorMessage.value = ''
}, 3000)
......@@ -415,7 +417,7 @@ const confirmDeleteStudent = async () => {
const saveStudent = async () => {
if (!editingStudent.value?.student_id) {
saveErrorMessage.value = 'Unable to get student ID'
saveErrorMessage.value = t('common.messages.error.general')
// 3秒后自动隐藏错误消息
setTimeout(() => {
saveErrorMessage.value = ''
......@@ -482,7 +484,7 @@ const saveStudent = async () => {
}
// 3. 显示成功消息
successMessage.value = 'Student information updated successfully'
successMessage.value = t('student.messages.updateSuccess')
// 3秒后自动隐藏成功消息
setTimeout(() => {
successMessage.value = ''
......@@ -492,7 +494,7 @@ const saveStudent = async () => {
} catch (error) {
// 4. API失败时不更新本地数据,显示错误消息
console.error('Save failed:', error)
saveErrorMessage.value = error.message || 'Save failed, please try again'
saveErrorMessage.value = error.message || t('student.messages.updateError')
// 3秒后自动隐藏错误消息
setTimeout(() => {
saveErrorMessage.value = ''
......@@ -507,7 +509,7 @@ const saveStudent = async () => {
<v-container fluid>
<v-row class="mb-4" align="center" justify="space-between">
<v-col cols="12" sm="6">
<h2 class="text-h5">Students</h2>
<h2 class="text-h5">{{ $t('student.title') }}</h2>
</v-col>
<v-col cols="12" sm="6" class="text-sm-right text-right">
<v-btn
......@@ -516,10 +518,10 @@ const saveStudent = async () => {
class="mr-2"
@click="openCreateDialog"
>
Add Student
{{ $t('student.buttons.addStudent') }}
</v-btn>
<v-btn color="primary" prepend-icon="mdi-refresh" :loading="isLoading" @click="fetchStudents">
Refresh
{{ $t('common.buttons.refresh') }}
</v-btn>
</v-col>
</v-row>
......@@ -539,10 +541,20 @@ const saveStudent = async () => {
:headers="headers"
:items="students"
item-key="student_id"
:items-per-page="-1"
:items-per-page="10"
class="elevation-1"
:sort-by="[{ key: 'student_name', order: 'asc' }]"
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 }">
<div class="d-flex align-center py-2">
......@@ -563,7 +575,7 @@ const saveStudent = async () => {
</template>
<template v-slot:[`item.enabled`]="{ item }">
<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>
</template>
<template v-slot:[`item.actions`]="{ item }">
......@@ -582,7 +594,7 @@ const saveStudent = async () => {
<v-dialog v-model="editDialog" max-width="500px">
<v-card>
<v-card-title class="text-h5">
Edit Student Information
{{ $t('student.dialog.edit') }}
</v-card-title>
<v-card-text>
<v-container>
......@@ -595,7 +607,7 @@ const saveStudent = async () => {
<!-- Avatar upload area -->
<v-col cols="12">
<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 -->
<div class="d-flex flex-column align-center">
......@@ -605,7 +617,7 @@ const saveStudent = async () => {
<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'"
:alt="editForm.avatar_file_name || $t('student.avatar.fallback.alt')"
cover
/>
</template>
......@@ -650,16 +662,16 @@ const saveStudent = async () => {
@click="$refs.fileInput.click()"
:disabled="isSaving"
>
{{ editForm.avatar ? 'Change Avatar' : 'Upload Avatar' }}
{{ editForm.avatar ? $t('student.avatar.upload.change') : $t('student.avatar.upload.button') }}
</v-btn>
<!-- File format description -->
<div class="text-center">
<div class="text-caption text-medium-emphasis">
Supports PNG, JPG, GIF formats
{{ $t('student.avatar.upload.formats') }}
</div>
<div class="text-caption text-medium-emphasis">
File size up to 2MB
{{ $t('student.avatar.upload.sizeLimit') }}
</div>
</div>
</div>
......@@ -674,7 +686,7 @@ const saveStudent = async () => {
<v-col cols="12">
<v-text-field
v-model="editForm.student_name"
label="Name"
:label="$t('student.form.name')"
required
variant="outlined"
/>
......@@ -682,7 +694,7 @@ const saveStudent = async () => {
<v-col cols="6">
<v-text-field
v-model="editForm.age"
label="Age"
:label="$t('student.form.age')"
type="number"
variant="outlined"
/>
......@@ -690,14 +702,14 @@ const saveStudent = async () => {
<v-col cols="6">
<v-text-field
v-model="editForm.grade"
label="Grade"
:label="$t('student.form.grade')"
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="editForm.enabled"
label="Enabled Status"
:label="$t('student.form.enabledStatus')"
color="primary"
/>
</v-col>
......@@ -712,7 +724,7 @@ const saveStudent = async () => {
@click="openDeleteDialog(editingStudent)"
:disabled="isSaving"
>
Delete
{{ $t('common.buttons.delete') }}
</v-btn>
<v-spacer />
<v-btn
......@@ -720,7 +732,7 @@ const saveStudent = async () => {
variant="text"
@click="closeEditDialog"
>
Cancel
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="primary"
......@@ -729,7 +741,7 @@ const saveStudent = async () => {
:disabled="isSaving"
@click="saveStudent"
>
Save
{{ $t('common.buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
......@@ -739,7 +751,7 @@ const saveStudent = async () => {
<v-dialog v-model="createDialog" max-width="500px">
<v-card>
<v-card-title class="text-h5">
Add Student
{{ $t('student.dialog.add') }}
</v-card-title>
<v-card-text>
<v-container>
......@@ -752,7 +764,7 @@ const saveStudent = async () => {
<!-- Avatar upload area -->
<v-col cols="12">
<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 -->
<div class="d-flex flex-column align-center">
......@@ -762,7 +774,7 @@ const saveStudent = async () => {
<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'"
:alt="createForm.avatar_file_name || $t('student.avatar.fallback.alt')"
cover
/>
</template>
......@@ -807,16 +819,16 @@ const saveStudent = async () => {
@click="$refs.createFileInput.click()"
:disabled="isCreating"
>
{{ createForm.avatar ? 'Change Avatar' : 'Upload Avatar' }}
{{ createForm.avatar ? $t('student.avatar.upload.change') : $t('student.avatar.upload.button') }}
</v-btn>
<!-- File format description -->
<div class="text-center">
<div class="text-caption text-medium-emphasis">
Supports PNG, JPG, GIF formats
{{ $t('student.avatar.upload.formats') }}
</div>
<div class="text-caption text-medium-emphasis">
File size up to 2MB
{{ $t('student.avatar.upload.sizeLimit') }}
</div>
</div>
</div>
......@@ -831,7 +843,7 @@ const saveStudent = async () => {
<v-col cols="12">
<v-text-field
v-model="createForm.student_name"
label="Name *"
:label="$t('student.form.name') + ' *'"
required
variant="outlined"
/>
......@@ -839,7 +851,7 @@ const saveStudent = async () => {
<v-col cols="6">
<v-text-field
v-model="createForm.age"
label="Age"
:label="$t('student.form.age')"
type="number"
variant="outlined"
/>
......@@ -847,14 +859,14 @@ const saveStudent = async () => {
<v-col cols="6">
<v-text-field
v-model="createForm.grade"
label="Grade"
:label="$t('student.form.grade')"
variant="outlined"
/>
</v-col>
<v-col cols="12">
<v-switch
v-model="createForm.enabled"
label="Enabled Status *"
:label="$t('student.form.enabledStatus') + ' *'"
color="primary"
/>
</v-col>
......@@ -868,7 +880,7 @@ const saveStudent = async () => {
variant="text"
@click="closeCreateDialog"
>
Cancel
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="success"
......@@ -877,7 +889,7 @@ const saveStudent = async () => {
:disabled="isCreating"
@click="createNewStudent"
>
Create
{{ $t('common.buttons.create') }}
</v-btn>
</v-card-actions>
</v-card>
......@@ -888,11 +900,11 @@ const saveStudent = async () => {
<v-card>
<v-card-title class="text-h5 text-error">
<v-icon icon="mdi-alert" class="mr-2" />
Confirm Delete
{{ $t('student.dialog.delete') }}
</v-card-title>
<v-card-text>
<div class="text-body-1 mb-4">
Are you sure you want to delete this student?
{{ $t('student.deleteConfirmation.message') }}
</div>
<div v-if="studentToDelete" class="bg-grey-lighten-4 pa-3 rounded">
<div class="d-flex align-center mb-2">
......@@ -918,7 +930,7 @@ const saveStudent = async () => {
</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.
{{ $t('student.deleteConfirmation.warning') }}
</div>
</v-card-text>
<v-card-actions>
......@@ -929,7 +941,7 @@ const saveStudent = async () => {
@click="closeDeleteDialog"
:disabled="isDeleting"
>
Cancel
{{ $t('common.buttons.cancel') }}
</v-btn>
<v-btn
color="error"
......@@ -939,7 +951,7 @@ const saveStudent = async () => {
:disabled="isDeleting"
@click="confirmDeleteStudent"
>
Confirm Delete
{{ $t('common.buttons.confirm') + ' ' + $t('common.buttons.delete') }}
</v-btn>
</v-card-actions>
</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