新增教练管理,优化预约人数计数方式
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import request from '@/utils/request'
|
||||
export { type GroupCourse } from '@/api/groupCourse.api'
|
||||
|
||||
export interface Coach {
|
||||
id: string
|
||||
username: string
|
||||
nickname: string
|
||||
email: string
|
||||
phone: string
|
||||
avatar: string
|
||||
status: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CoachCreateRequest {
|
||||
username: string
|
||||
password: string
|
||||
nickname: string
|
||||
email: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
export interface CoachUpdateRequest {
|
||||
nickname?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
}
|
||||
|
||||
export const coachApi = {
|
||||
/** 获取所有教练列表 */
|
||||
getAll: () =>
|
||||
request.get<Coach[]>('/coach/list'),
|
||||
|
||||
/** 创建教练(自动分配教练角色) */
|
||||
create: (data: CoachCreateRequest) =>
|
||||
request.post<Coach>('/coach', data),
|
||||
|
||||
/** 更新教练信息 */
|
||||
update: (id: string, data: CoachUpdateRequest) =>
|
||||
request.put<Coach>(`/coach/${id}`, data),
|
||||
|
||||
/** 禁用教练 */
|
||||
disable: (id: string) =>
|
||||
request.post<void>(`/coach/${id}/disable`),
|
||||
|
||||
/** 获取教练教授的团课列表 */
|
||||
getCourses: (id: string) =>
|
||||
request.get<GroupCourse[]>(`/coach/${id}/courses`),
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import request from '@/utils/request'
|
||||
export interface GroupCourse {
|
||||
id?: number
|
||||
courseName: string
|
||||
coachId?: number
|
||||
coachId?: number | string
|
||||
coachName?: string
|
||||
courseType?: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
@@ -31,7 +32,7 @@ export interface GroupCoursePageRequest {
|
||||
keyword?: string
|
||||
status?: string
|
||||
courseType?: number
|
||||
coachId?: number
|
||||
coachId?: number | string
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
sortBy?: string
|
||||
@@ -75,6 +76,18 @@ export const groupCourseApi = {
|
||||
|
||||
search: (params: any) =>
|
||||
request.post<GroupCourse[]>('/groupCourse/search', params),
|
||||
|
||||
checkCoachConflict: (params: {
|
||||
coachId: number | string
|
||||
startTime: string
|
||||
endTime: string
|
||||
excludeCourseId?: number | string
|
||||
}) =>
|
||||
request.post<{
|
||||
success: boolean
|
||||
hasConflict: boolean
|
||||
conflicts: GroupCourse[]
|
||||
}>('/groupCourse/check-coach-conflict', params),
|
||||
}
|
||||
|
||||
// ==================== 团课类型 ====================
|
||||
|
||||
@@ -40,6 +40,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/system/UserManagement.vue'),
|
||||
meta: { title: '用户管理' }
|
||||
},
|
||||
{
|
||||
path: 'coach',
|
||||
name: 'CoachManagement',
|
||||
component: () => import('@/views/system/CoachManagement.vue'),
|
||||
meta: { title: '教练管理' }
|
||||
},
|
||||
{
|
||||
path: 'roles',
|
||||
name: 'RoleManagement',
|
||||
|
||||
@@ -31,6 +31,7 @@ function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] {
|
||||
'system/user/index': '/users',
|
||||
'system/role/index': '/roles',
|
||||
'system/menu/index': '/menus',
|
||||
'system/coach/index': '/coach',
|
||||
'system/dict/index': '/dict',
|
||||
'system/config/index': '/sys/config',
|
||||
'system/notice/index': '/notice',
|
||||
@@ -92,6 +93,7 @@ function getMenuIcon(menuName: string): string {
|
||||
'审计日志': 'Document',
|
||||
'系统监控': 'Monitor',
|
||||
'用户管理': 'User',
|
||||
'教练管理': 'Medal',
|
||||
'角色管理': 'UserFilled',
|
||||
'菜单管理': 'Menu',
|
||||
'字典管理': 'Collection',
|
||||
|
||||
@@ -64,6 +64,11 @@
|
||||
{{ getCourseTypeName(row.courseType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="教练" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ getCoachDisplayName(row.coachId) || row.coachName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
@@ -150,6 +155,28 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="教练" prop="coachId">
|
||||
<el-select
|
||||
v-model="formState.coachId"
|
||||
placeholder="请选择教练"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="coach in coaches"
|
||||
:key="coach.id"
|
||||
:value="coach.id"
|
||||
:label="`${coach.nickname || coach.username} (${coach.username})`"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="coachConflictWarning" style="margin-top: 6px; color: #e6a23c; font-size: 12px; line-height: 1.5;">
|
||||
<el-icon style="vertical-align: middle;"><WarningFilled /></el-icon>
|
||||
{{ coachConflictWarning }}
|
||||
</div>
|
||||
<div v-if="coachConflictChecking" style="margin-top: 4px; color: #909399; font-size: 12px;">
|
||||
正在检查教练排期...
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-date-picker
|
||||
v-model="formState.startTime"
|
||||
@@ -246,8 +273,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus } from '@element-plus/icons-vue'
|
||||
import { Search, Plus, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { groupCourseApi, groupCourseTypeApi, type GroupCourse, type GroupCourseType } from '@/api/groupCourse.api'
|
||||
import { coachApi, type Coach } from '@/api/coach.api'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
@@ -256,6 +284,7 @@ const dataSource = ref<GroupCourse[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const searchStatus = ref('')
|
||||
const courseTypes = ref<GroupCourseType[]>([])
|
||||
const coaches = ref<Coach[]>([])
|
||||
|
||||
const statusMap: Record<string, { type: string; label: string }> = {
|
||||
'0': { type: 'success', label: '正常' },
|
||||
@@ -279,9 +308,12 @@ const sortInfo = reactive({
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const coachConflictWarning = ref('')
|
||||
const coachConflictChecking = ref(false)
|
||||
const formState = reactive<GroupCourse>({
|
||||
courseName: '',
|
||||
courseType: undefined,
|
||||
coachId: undefined,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
maxMembers: 20,
|
||||
@@ -342,12 +374,70 @@ const fetchCourseTypes = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCoaches = async () => {
|
||||
try {
|
||||
coaches.value = await coachApi.getAll()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const getCourseTypeName = (typeId: number | undefined): string => {
|
||||
if (typeId === undefined || typeId === null) return '-'
|
||||
const type = courseTypes.value.find(t => t.id === typeId)
|
||||
return type?.typeName || String(typeId)
|
||||
}
|
||||
|
||||
const getCoachDisplayName = (coachId: number | string | undefined): string => {
|
||||
if (coachId === undefined || coachId === null) return ''
|
||||
const coach = coaches.value.find(c => String(c.id) === String(coachId))
|
||||
if (!coach) return ''
|
||||
return coach.nickname || coach.username
|
||||
}
|
||||
|
||||
const getComputedEndTime = (): string => {
|
||||
if (!formState.startTime || !(formState as any).duration) return ''
|
||||
const start = new Date(formState.startTime)
|
||||
if (isNaN(start.getTime())) return ''
|
||||
start.setMinutes(start.getMinutes() + (formState as any).duration)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}T${pad(start.getHours())}:${pad(start.getMinutes())}:${pad(start.getSeconds())}`
|
||||
}
|
||||
|
||||
const checkCoachConflict = async () => {
|
||||
const coachId = formState.coachId
|
||||
const startTime = formState.startTime
|
||||
const durationVal = (formState as any).duration
|
||||
if (!coachId || !startTime || !durationVal) {
|
||||
coachConflictWarning.value = ''
|
||||
return
|
||||
}
|
||||
const endTime = getComputedEndTime()
|
||||
if (!endTime) {
|
||||
coachConflictWarning.value = ''
|
||||
return
|
||||
}
|
||||
coachConflictChecking.value = true
|
||||
try {
|
||||
const res = await groupCourseApi.checkCoachConflict({
|
||||
coachId,
|
||||
startTime,
|
||||
endTime,
|
||||
excludeCourseId: formState.id,
|
||||
})
|
||||
if ((res as any).hasConflict && (res as any).conflicts?.length > 0) {
|
||||
const names = (res as any).conflicts.map((c: any) => c.courseName || '未知课程').join('、')
|
||||
coachConflictWarning.value = `该教练在此时间段已有团课:${names}`
|
||||
} else {
|
||||
coachConflictWarning.value = ''
|
||||
}
|
||||
} catch {
|
||||
coachConflictWarning.value = ''
|
||||
} finally {
|
||||
coachConflictChecking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -522,12 +612,25 @@ const loadCoverPreview = async (val: string) => {
|
||||
// Watch coverImage changes to auto-load preview (e.g. when editing an existing course)
|
||||
watch(() => formState.coverImage, (val) => loadCoverPreview(val || ''), { immediate: true })
|
||||
|
||||
// 监听教练选择和时间变化,检查冲突
|
||||
watch(() => [formState.coachId, formState.startTime, (formState as any).duration] as const, () => {
|
||||
checkCoachConflict()
|
||||
}, { deep: false })
|
||||
|
||||
// 打开弹窗时清除冲突提示
|
||||
watch(modalVisible, (val) => {
|
||||
if (val) {
|
||||
coachConflictWarning.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增团课'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
courseName: '',
|
||||
courseType: undefined,
|
||||
coachId: undefined,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
maxMembers: 20,
|
||||
@@ -591,12 +694,35 @@ const handleModalOk = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
// Auto-calculate endTime from startTime + duration
|
||||
if (formState.startTime && formState.duration) {
|
||||
if (formState.startTime && (formState as any).duration) {
|
||||
const start = new Date(formState.startTime)
|
||||
start.setMinutes(start.getMinutes() + formState.duration)
|
||||
start.setMinutes(start.getMinutes() + (formState as any).duration)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
formState.endTime = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}T${pad(start.getHours())}:${pad(start.getMinutes())}:${pad(start.getSeconds())}`
|
||||
}
|
||||
// 最终教练冲突检查
|
||||
if (formState.coachId && formState.startTime && formState.endTime) {
|
||||
try {
|
||||
const res = await groupCourseApi.checkCoachConflict({
|
||||
coachId: formState.coachId,
|
||||
startTime: formState.startTime,
|
||||
endTime: formState.endTime,
|
||||
excludeCourseId: formState.id,
|
||||
})
|
||||
if ((res as any).hasConflict && (res as any).conflicts?.length > 0) {
|
||||
const names = (res as any).conflicts.map((c: any) => c.courseName || '未知课程').join('、')
|
||||
await ElMessageBox.confirm(
|
||||
`该教练在此时间段已有团课:${names},确定继续吗?`,
|
||||
'时间冲突警告',
|
||||
{ confirmButtonText: '继续保存', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
// 用户点击了"继续保存",继续执行
|
||||
}
|
||||
} catch (e) {
|
||||
// 用户取消了冲突确认对话框,中止保存
|
||||
return
|
||||
}
|
||||
}
|
||||
if (formState.id) {
|
||||
await groupCourseApi.update(formState.id, formState)
|
||||
} else {
|
||||
@@ -612,8 +738,8 @@ const handleModalOk = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchCourseTypes()
|
||||
onMounted(async () => {
|
||||
await Promise.all([fetchCourseTypes(), fetchCoaches()])
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div class="coach-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索教练用户名或昵称"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
新增教练
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="filteredData" style="width: 100%">
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="nickname" label="昵称" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="phone" label="手机号" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 1 ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
>
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="info" link @click="handleViewCourses(row)">
|
||||
查看团课
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDisable(row)"
|
||||
>
|
||||
禁用
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑教练弹窗 -->
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username" required>
|
||||
<el-input v-model="formState.username" :disabled="!!formState.id" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!formState.id" label="密码" prop="password" required>
|
||||
<el-input v-model="formState.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="formState.nickname" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="formState.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="formState.phone" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleModalCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleModalOk">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看团课弹窗 -->
|
||||
<el-dialog
|
||||
v-model="coursesDialogVisible"
|
||||
:title="'教练团课 - ' + selectedCoachName"
|
||||
width="900px"
|
||||
>
|
||||
<el-table v-loading="coursesLoading" :data="coachCourses" style="width: 100%" max-height="400">
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="courseName" label="课程名称" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getCourseStatusTag(row.status)" effect="dark">
|
||||
{{ getCourseStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开课时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.startTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结课时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.endTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约人数" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.currentMembers }} / {{ row.maxMembers }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="location" label="上课地点" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="coursesDialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { coachApi, type Coach, type CoachCreateRequest, type CoachUpdateRequest, type GroupCourse } from '@/api/coach.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Coach[]>([])
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const filteredData = computed(() => {
|
||||
if (!searchKeyword.value) return dataSource.value
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
return dataSource.value.filter(
|
||||
(c) =>
|
||||
c.username.toLowerCase().includes(keyword) ||
|
||||
c.nickname.toLowerCase().includes(keyword) ||
|
||||
c.email.toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
// 新增/编辑表单
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const formState = reactive<CoachCreateRequest & { id?: string }>({
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '用户名只能包含字母、数字、下划线和横线', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 20, message: '密码长度在 8 到 20 个字符', trigger: 'blur' },
|
||||
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, message: '密码必须包含大小写字母和数字', trigger: 'blur' },
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 团课查看
|
||||
const coursesDialogVisible = ref(false)
|
||||
const coursesLoading = ref(false)
|
||||
const coachCourses = ref<GroupCourse[]>([])
|
||||
const selectedCoachName = ref('')
|
||||
|
||||
// 团课状态映射
|
||||
const courseStatusMap: Record<number, { text: string; tag: string }> = {
|
||||
0: { text: '正常', tag: 'success' },
|
||||
1: { text: '已取消', tag: 'info' },
|
||||
2: { text: '已结束', tag: 'warning' },
|
||||
3: { text: '进行中', tag: 'danger' },
|
||||
4: { text: '超时', tag: 'info' },
|
||||
}
|
||||
|
||||
function getCourseStatusText(status: number) {
|
||||
return courseStatusMap[status]?.text || '未知'
|
||||
}
|
||||
|
||||
function getCourseStatusTag(status: number) {
|
||||
return courseStatusMap[status]?.tag || 'info'
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
dataSource.value = await coachApi.getAll()
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 前端过滤,无需重新请求
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增教练'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: Coach) => {
|
||||
modalTitle.value = '编辑教练'
|
||||
Object.assign(formState, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
nickname: row.nickname,
|
||||
email: row.email,
|
||||
phone: row.phone,
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDisable = async (row: Coach) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要禁用教练「${row.nickname || row.username}」吗?\n\n禁用后:\n1. 该教练正在进行中的团课不会被影响\n2. 该教练非进行中的团课将被自动取消\n3. 该教练将无法再被关联到团课`,
|
||||
'禁用教练确认',
|
||||
{
|
||||
confirmButtonText: '确定禁用',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: false,
|
||||
}
|
||||
)
|
||||
await coachApi.disable(row.id)
|
||||
ElMessage.success('教练已禁用')
|
||||
fetchData()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formState.id) {
|
||||
const updateData: CoachUpdateRequest = {
|
||||
nickname: formState.nickname,
|
||||
email: formState.email,
|
||||
phone: formState.phone,
|
||||
}
|
||||
await coachApi.update(formState.id, updateData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
const createData: CoachCreateRequest = {
|
||||
username: formState.username,
|
||||
password: formState.password,
|
||||
nickname: formState.nickname,
|
||||
email: formState.email,
|
||||
phone: formState.phone,
|
||||
}
|
||||
await coachApi.create(createData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const handleViewCourses = async (row: Coach) => {
|
||||
selectedCoachName.value = row.nickname || row.username
|
||||
coursesDialogVisible.value = true
|
||||
coursesLoading.value = true
|
||||
try {
|
||||
coachCourses.value = await coachApi.getCourses(row.id)
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
coursesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.coach-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user