1077 lines
30 KiB
Vue
1077 lines
30 KiB
Vue
<script setup>
|
||
import { ref, reactive, onMounted, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus, Edit, Delete, Search, Refresh } from '@element-plus/icons-vue'
|
||
import { formatDateTime } from '@/utils/format'
|
||
import { reverseArray } from '@/utils/sort'
|
||
import {
|
||
getActivityList,
|
||
createActivity,
|
||
updateActivity,
|
||
cancelActivity,
|
||
batchCancelActivities,
|
||
updateActivityStatus,
|
||
checkActivityCapacity,
|
||
getActivityEnrollments,
|
||
updateEnrollmentStatus
|
||
} from '@/api/activity/study'
|
||
import { useUserStore } from '@/stores/user'
|
||
|
||
// 查询参数
|
||
const queryParams = reactive({
|
||
page: 1,
|
||
page_size: 10,
|
||
title: '',
|
||
category: undefined,
|
||
status: undefined
|
||
})
|
||
|
||
// 数据列表
|
||
const loading = ref(false)
|
||
const tableData = ref([])
|
||
const selectedRows = ref([])
|
||
|
||
// 分页配置
|
||
const currentPage = ref(1)
|
||
const pageSize = ref(10)
|
||
const total = ref(0)
|
||
|
||
// 研学活动类型选项
|
||
const categoryOptions = [
|
||
{ label: '实地考察', value: 'field_study' },
|
||
{ label: '科普教育', value: 'science' },
|
||
{ label: '文化体验', value: 'culture' },
|
||
{ label: '环保实践', value: 'environment' }
|
||
]
|
||
|
||
// 状态选项
|
||
const statusOptions = [
|
||
{ label: '报名中', value: 1 },
|
||
{ label: '进行中', value: 2 },
|
||
{ label: '已结束', value: 0 }
|
||
]
|
||
|
||
// 搜索表单
|
||
const searchForm = ref({
|
||
title: '',
|
||
category: '',
|
||
status: ''
|
||
})
|
||
|
||
// 获取用户store
|
||
const userStore = useUserStore()
|
||
|
||
// 获取列表数据
|
||
const getList = async () => {
|
||
loading.value = true
|
||
try {
|
||
const res = await getActivityList()
|
||
if (res.success) {
|
||
// 保存所有数据到临时变量
|
||
let allData = res.data.map(item => ({
|
||
id: item.id,
|
||
title: item.title,
|
||
category: item.category,
|
||
start_time: item.start_time,
|
||
end_time: item.end_time,
|
||
location: item.location,
|
||
capacity: item.capacity,
|
||
enrolled: item.enrolled_count,
|
||
description: item.description,
|
||
requirements: item.requirements,
|
||
cost: item.cost,
|
||
status: item.status,
|
||
created_at: item.created_at,
|
||
updated_at: item.updated_at,
|
||
activity_code: item.activity_code,
|
||
image: item.image
|
||
}))
|
||
|
||
// 将数据倒序排列
|
||
allData = reverseArray(allData)
|
||
|
||
// 应用搜索过滤
|
||
if (searchForm.value.title) {
|
||
const keyword = searchForm.value.title.toLowerCase()
|
||
allData = allData.filter(item =>
|
||
item.title.toLowerCase().includes(keyword)
|
||
)
|
||
}
|
||
|
||
if (searchForm.value.category) {
|
||
allData = allData.filter(item =>
|
||
item.category === searchForm.value.category
|
||
)
|
||
}
|
||
|
||
if (searchForm.value.status !== '') {
|
||
allData = allData.filter(item =>
|
||
item.status === Number(searchForm.value.status)
|
||
)
|
||
}
|
||
|
||
// 更新总数据
|
||
total.value = allData.length
|
||
|
||
// 前端分页处理
|
||
const start = (currentPage.value - 1) * pageSize.value
|
||
const end = start + pageSize.value
|
||
tableData.value = allData.slice(start, end)
|
||
} else {
|
||
ElMessage.error(res.message || '获取研学活动列表失败')
|
||
}
|
||
} catch (error) {
|
||
ElMessage.error('获取研学活动列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 处理页码改变
|
||
const handleCurrentChange = (val) => {
|
||
currentPage.value = val
|
||
getList()
|
||
}
|
||
|
||
// 处理每页条数改变
|
||
const handleSizeChange = (val) => {
|
||
pageSize.value = val
|
||
currentPage.value = 1
|
||
getList()
|
||
}
|
||
|
||
// 处理搜索
|
||
const handleSearch = () => {
|
||
currentPage.value = 1 // 重置到第一页
|
||
getList()
|
||
}
|
||
|
||
// 重置搜索
|
||
const resetSearch = () => {
|
||
searchForm.value = {
|
||
title: '',
|
||
category: '',
|
||
status: ''
|
||
}
|
||
currentPage.value = 1
|
||
getList()
|
||
}
|
||
|
||
// 处理清空操作
|
||
const handleClear = (field) => {
|
||
// 当任何字段被清空时,都执行重置操作
|
||
resetSearch()
|
||
}
|
||
|
||
// 监听分页变化
|
||
watch([currentPage, pageSize], () => {
|
||
getList()
|
||
})
|
||
|
||
// 监听搜索条件变化
|
||
watch(searchForm, (newVal, oldVal) => {
|
||
// 如果是状态字段发生变化且变为空字符串,说明是清空操作
|
||
if (oldVal.status !== '' && newVal.status === '') {
|
||
currentPage.value = 1
|
||
}
|
||
getList()
|
||
}, { deep: true })
|
||
|
||
// 刷新列表
|
||
const handleRefresh = () => {
|
||
getList()
|
||
}
|
||
|
||
// 新增/编辑对话框
|
||
const dialogVisible = ref(false)
|
||
const dialogTitle = ref('')
|
||
const formMode = ref('create')
|
||
const formRef = ref()
|
||
const form = ref({
|
||
title: '',
|
||
category: 'field_study',
|
||
start_time: '',
|
||
end_time: '',
|
||
location: '',
|
||
capacity: 30,
|
||
description: '',
|
||
requirements: '',
|
||
cost: 50.00,
|
||
status: 1,
|
||
image_url: null,
|
||
imageUrl: ''
|
||
})
|
||
|
||
// 表单规则
|
||
const formRules = {
|
||
title: [
|
||
{ required: true, message: '请输入活动名称', trigger: 'blur' },
|
||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
|
||
],
|
||
location: [
|
||
{ required: true, message: '请输入活动地点', trigger: 'blur' }
|
||
],
|
||
capacity: [
|
||
{ required: true, message: '请输入人数上限', trigger: 'blur' },
|
||
{ type: 'number', min: 1, message: '人数上限必须大于0', trigger: 'blur' }
|
||
],
|
||
start_time: [
|
||
{ required: true, message: '请选择开始时间', trigger: 'change' }
|
||
],
|
||
end_time: [
|
||
{ required: true, message: '请选择结束时间', trigger: 'change' }
|
||
],
|
||
cost: [
|
||
{ required: true, message: '请输入活动费用', trigger: 'blur' },
|
||
{ type: 'number', min: 0, message: '费用不能小于0', trigger: 'blur' }
|
||
],
|
||
image_url: [
|
||
{ required: true, message: '请上传活动图片', trigger: 'change' }
|
||
]
|
||
}
|
||
|
||
// 处理图片上传前的验证
|
||
const beforeImageUpload = (file) => {
|
||
const isImage = file.type.startsWith('image/')
|
||
const isLt2M = file.size / 1024 / 1024 < 2
|
||
|
||
if (!isImage) {
|
||
ElMessage.error('只能上传图片文件!')
|
||
return false
|
||
}
|
||
if (!isLt2M) {
|
||
ElMessage.error('图片大小不能超过 2MB!')
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// 处理图片变更
|
||
const handleImageChange = (uploadFile) => {
|
||
const file = uploadFile.raw
|
||
if (!file) return
|
||
|
||
form.value.image_url = file
|
||
form.value.imageUrl = URL.createObjectURL(file)
|
||
}
|
||
|
||
// 处理图片移除
|
||
const handleImageRemove = () => {
|
||
form.value.imageUrl = ''
|
||
form.value.image_url = null
|
||
// 重置表单的图片验证状态
|
||
if (formRef.value) {
|
||
formRef.value.validateField('image_url')
|
||
}
|
||
}
|
||
|
||
// 新增研学活动
|
||
const handleAdd = () => {
|
||
formMode.value = 'create'
|
||
dialogTitle.value = '新增研学活动'
|
||
form.value = {
|
||
title: '',
|
||
category: 'field_study',
|
||
start_time: '',
|
||
end_time: '',
|
||
location: '',
|
||
capacity: 30,
|
||
description: '',
|
||
requirements: '',
|
||
cost: 50.00,
|
||
status: 1,
|
||
image_url: null,
|
||
imageUrl: ''
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
// 编辑研学活动
|
||
const handleEdit = (row) => {
|
||
formMode.value = 'edit'
|
||
dialogTitle.value = '编辑研学活动'
|
||
form.value = {
|
||
...row,
|
||
category: row.category || 'field_study',
|
||
imageUrl: row.image || '',
|
||
image_url: null // 重置图片文件
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
// 提交表单
|
||
const handleSubmit = async () => {
|
||
if (!formRef.value) return
|
||
|
||
await formRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
try {
|
||
// 创建 FormData 对象
|
||
const formData = new FormData()
|
||
|
||
// 添加基本字段
|
||
formData.append('title', form.value.title.trim())
|
||
formData.append('category', form.value.category)
|
||
|
||
// 处理日期 - 使用MySQL兼容的日期时间格式
|
||
if (form.value.start_time) {
|
||
const startDate = new Date(form.value.start_time)
|
||
const formattedStartTime = formatDate(startDate)
|
||
formData.append('start_time', formattedStartTime)
|
||
}
|
||
|
||
if (form.value.end_time) {
|
||
const endDate = new Date(form.value.end_time)
|
||
const formattedEndTime = formatDate(endDate)
|
||
formData.append('end_time', formattedEndTime)
|
||
}
|
||
|
||
// 确保数字字段是数字类型
|
||
formData.append('capacity', form.value.capacity.toString())
|
||
formData.append('cost', form.value.cost.toString())
|
||
formData.append('status', form.value.status.toString())
|
||
|
||
// 其他字段
|
||
formData.append('location', form.value.location.trim())
|
||
if (form.value.description) {
|
||
formData.append('description', form.value.description.trim())
|
||
}
|
||
if (form.value.requirements) {
|
||
formData.append('requirements', form.value.requirements.trim())
|
||
}
|
||
|
||
// 添加图片文件
|
||
if (form.value.image_url instanceof File) {
|
||
formData.append('image_url', form.value.image_url)
|
||
}
|
||
|
||
// 验证必填字段
|
||
if (!formData.get('title') || !formData.get('start_time') || !formData.get('end_time') || !formData.get('location')) {
|
||
ElMessage.error('请填写必填字段')
|
||
return
|
||
}
|
||
|
||
// 验证时间
|
||
const startTime = new Date(formData.get('start_time'))
|
||
const endTime = new Date(formData.get('end_time'))
|
||
const now = new Date()
|
||
|
||
// 验证结束时间必须大于开始时间
|
||
if (endTime <= startTime) {
|
||
ElMessage.error('结束时间必须大于开始时间')
|
||
return
|
||
}
|
||
|
||
let res
|
||
if (formMode.value === 'create') {
|
||
res = await createActivity(formData)
|
||
} else {
|
||
const id = form.value.id
|
||
res = await updateActivity(id, formData)
|
||
}
|
||
|
||
if (res.success) {
|
||
ElMessage.success(`${formMode.value === 'create' ? '新增' : '编辑'}成功`)
|
||
dialogVisible.value = false
|
||
getList()
|
||
} else {
|
||
ElMessage.error(res.message || `${formMode.value === 'create' ? '新增' : '编辑'}失败`)
|
||
}
|
||
} catch (error) {
|
||
if (error.response) {
|
||
ElMessage.error(error.response.data.message || '操作失败,请检查输入是否正确')
|
||
} else {
|
||
ElMessage.error('操作失败,请检查网络连接')
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 取消活动
|
||
const handleCancel = (row) => {
|
||
// 检查活动状态
|
||
if (row.status === 0) {
|
||
ElMessage.warning('已结束的活动无法取消')
|
||
return
|
||
}
|
||
|
||
ElMessageBox.confirm('确认取消该研学活动吗?此操作不可恢复', '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const res = await cancelActivity(row.id)
|
||
if (res.success) {
|
||
ElMessage.success('取消成功')
|
||
getList()
|
||
} else {
|
||
ElMessage.error(res.message || '取消失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('取消研学活动错误:', error)
|
||
ElMessage.error('取消失败')
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// 批量取消活动
|
||
const handleBatchCancel = () => {
|
||
if (selectedRows.value.length === 0) {
|
||
ElMessage.warning('请选择要取消的活动')
|
||
return
|
||
}
|
||
|
||
// 检查选中的活动是否有已结束的
|
||
const endedActivities = selectedRows.value.filter(row => row.status === 0)
|
||
if (endedActivities.length > 0) {
|
||
ElMessage.warning('已结束的活动无法取消')
|
||
return
|
||
}
|
||
|
||
// 获取可以取消的活动ID列表
|
||
const cancelableActivities = selectedRows.value.filter(row => row.status !== 0)
|
||
if (cancelableActivities.length === 0) {
|
||
ElMessage.warning('所选活动都无法取消')
|
||
return
|
||
}
|
||
|
||
ElMessageBox.confirm(`确认取消选中的 ${cancelableActivities.length} 个研学活动吗?此操作不可恢复`, '警告', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
try {
|
||
const ids = cancelableActivities.map(row => row.id)
|
||
const res = await batchCancelActivities(ids)
|
||
if (res.success) {
|
||
ElMessage.success('批量取消成功')
|
||
getList()
|
||
} else {
|
||
ElMessage.error(res.message || '批量取消失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('批量取消研学活动错误:', error)
|
||
ElMessage.error('批量取消失败')
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// 更新活动状态
|
||
const handleStatusChange = async (row) => {
|
||
try {
|
||
const res = await updateActivityStatus(row.id, row.status)
|
||
if (res.success) {
|
||
ElMessage.success('状态更新成功')
|
||
getList()
|
||
} else {
|
||
ElMessage.error(res.message || '状态更新失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('更新研学活动状态错误:', error)
|
||
ElMessage.error('状态更新失败')
|
||
}
|
||
}
|
||
|
||
// 报名详情对话框
|
||
const enrollmentDialogVisible = ref(false)
|
||
const enrollmentLoading = ref(false)
|
||
const enrollmentList = ref([])
|
||
const currentActivity = ref(null)
|
||
|
||
// 获取报名详情
|
||
const getEnrollmentDetails = async (row) => {
|
||
if (!row || !row.id) {
|
||
ElMessage.error('无效的活动ID')
|
||
return
|
||
}
|
||
|
||
enrollmentLoading.value = true
|
||
currentActivity.value = row
|
||
enrollmentDialogVisible.value = true
|
||
|
||
try {
|
||
const activityId = row.id
|
||
const res = await getActivityEnrollments(activityId)
|
||
if (res.success) {
|
||
// 确保enrollmentList是一个数组
|
||
enrollmentList.value = Array.isArray(res.data.list) ? res.data.list : []
|
||
} else {
|
||
ElMessage.error(res.message || '获取报名详情失败')
|
||
enrollmentList.value = [] // 确保失败时也是空数组
|
||
}
|
||
} catch (error) {
|
||
console.error('获取报名详情失败:', error)
|
||
ElMessage.error('获取报名详情失败')
|
||
enrollmentList.value = [] // 确保出错时也是空数组
|
||
} finally {
|
||
enrollmentLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取报名状态标签类型
|
||
const getEnrollmentStatusType = (status) => {
|
||
const statusMap = {
|
||
0: 'info',
|
||
1: 'success',
|
||
2: 'warning'
|
||
}
|
||
return statusMap[status] || 'info'
|
||
}
|
||
|
||
// 获取报名状态文本
|
||
const getEnrollmentStatusText = (status) => {
|
||
const statusMap = {
|
||
0: '已取消',
|
||
1: '已报名',
|
||
2: '已完成'
|
||
}
|
||
return statusMap[status] || '未知'
|
||
}
|
||
|
||
// 在 checkCapacity 函数中修改
|
||
const checkCapacity = async (row) => {
|
||
if (!row || !row.id) {
|
||
ElMessage.error('无效的活动ID')
|
||
return
|
||
}
|
||
// 直接传递当前行数据
|
||
getEnrollmentDetails(row)
|
||
}
|
||
|
||
// 表格选择
|
||
const handleSelectionChange = (rows) => {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
// 格式化日期
|
||
const formatDate = (date) => {
|
||
if (!date) return ''
|
||
const d = new Date(date)
|
||
const pad = (num) => (num < 10 ? `0${num}` : num)
|
||
|
||
const year = d.getFullYear()
|
||
const month = pad(d.getMonth() + 1)
|
||
const day = pad(d.getDate())
|
||
const hours = pad(d.getHours())
|
||
const minutes = pad(d.getMinutes())
|
||
const seconds = pad(d.getSeconds())
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||
}
|
||
|
||
// 更新报名状态
|
||
const handleEnrollmentStatusChange = async (row) => {
|
||
try {
|
||
const res = await updateEnrollmentStatus(row.id, row.status)
|
||
if (res.success) {
|
||
ElMessage.success('状态更新成功')
|
||
// 重新获取报名详情
|
||
getEnrollmentDetails(currentActivity.value)
|
||
} else {
|
||
ElMessage.error(res.message || '状态更新失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('更新报名状态失败:', error)
|
||
ElMessage.error('更新报名状态失败')
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
getList()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="study-management">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>研学管理</span>
|
||
<div class="header-btns">
|
||
<el-button type="primary" :icon="Plus" @click="handleAdd">新增活动</el-button>
|
||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||
<el-button
|
||
v-if="selectedRows.length > 0"
|
||
type="danger"
|
||
@click="handleBatchCancel"
|
||
>批量取消</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 搜索表单 -->
|
||
<el-form :model="searchForm" inline class="search-form">
|
||
<el-form-item label="活动名称" label-width="80px">
|
||
<el-input
|
||
v-model="searchForm.title"
|
||
placeholder="请输入活动名称"
|
||
clearable
|
||
@clear="handleClear('title')"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="活动类型" label-width="80px">
|
||
<el-select
|
||
v-model="searchForm.category"
|
||
placeholder="请选择类型"
|
||
clearable
|
||
@clear="handleClear('category')"
|
||
>
|
||
<el-option
|
||
v-for="item in categoryOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态" label-width="80px">
|
||
<el-select
|
||
v-model="searchForm.status"
|
||
placeholder="请选择状态"
|
||
clearable
|
||
@clear="handleClear('status')"
|
||
>
|
||
<el-option
|
||
v-for="item in statusOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item class="search-buttons">
|
||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||
<el-button @click="resetSearch">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 研学活动列表 -->
|
||
<el-table
|
||
v-loading="loading"
|
||
:data="tableData"
|
||
style="width: 100%"
|
||
@selection-change="handleSelectionChange"
|
||
>
|
||
<el-table-column type="selection" :selectable="row => row.status !== 0" width="55" align="center"/>
|
||
<el-table-column type="index" label="序号" width="60" align="center"/>
|
||
<el-table-column prop="title" label="活动名称" min-width="150" show-overflow-tooltip align="center"/>
|
||
<el-table-column prop="category" label="活动类型" width="120" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="row.category === 'field_study' ? 'success' :
|
||
row.category === 'science' ? 'warning' :
|
||
row.category === 'culture' ? 'info' :
|
||
row.category === 'environment' ? 'danger' : ''"
|
||
>
|
||
{{ categoryOptions.find(item => item.value === row.category)?.label || '未知' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="capacity" label="人数上限" width="100" align="center"/>
|
||
<el-table-column prop="enrolled" label="已报名" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" link @click="checkCapacity(row)">
|
||
{{ row.enrolled ?? 0 }}/{{ row.capacity ?? 0 }}
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="location" label="活动地点" width="150" show-overflow-tooltip align="center"/>
|
||
<el-table-column prop="start_time" label="开始时间" width="160" align="center">
|
||
<template #default="{ row }">
|
||
{{ formatDateTime(row.start_time) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="end_time" label="结束时间" width="160" align="center">
|
||
<template #default="{ row }">
|
||
{{ formatDateTime(row.end_time) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="cost" label="费用" width="100" align="center">
|
||
<template #default="{ row }">
|
||
¥{{ row.cost }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 1 ? 'success' :
|
||
row.status === 2 ? 'warning' : 'info'">
|
||
{{ row.status === 1 ? '报名中' :
|
||
row.status === 2 ? '进行中' : '已结束' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="user_id" label="用户昵称" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
{{ userStore.userInfo?.nickname || userStore.userInfo?.username || row.user_id }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="250" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
@click="handleEdit(row)"
|
||
>编辑</el-button>
|
||
<el-button
|
||
v-if="row.status !== 0"
|
||
type="danger"
|
||
link
|
||
@click="handleCancel(row)"
|
||
>取消</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 分页器 -->
|
||
<div class="pagination-container">
|
||
<el-pagination
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="pageSize"
|
||
:total="total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
background
|
||
layout="total, sizes, prev, pager, next"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 新增/编辑对话框 -->
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="800px"
|
||
destroy-on-close
|
||
>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="formRules"
|
||
label-width="100px"
|
||
>
|
||
<el-form-item label="活动名称" prop="title">
|
||
<el-input
|
||
v-model="form.title"
|
||
placeholder="请输入活动名称"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="活动类型" prop="category">
|
||
<el-select
|
||
v-model="form.category"
|
||
placeholder="请选择活动类型"
|
||
>
|
||
<el-option
|
||
v-for="item in categoryOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="活动地点" prop="location">
|
||
<el-input
|
||
v-model="form.location"
|
||
placeholder="请输入活动地点"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="活动时间" required>
|
||
<el-col :span="11">
|
||
<el-form-item prop="start_time">
|
||
<el-date-picker
|
||
v-model="form.start_time"
|
||
type="datetime"
|
||
placeholder="开始时间"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="2" class="text-center">
|
||
<span class="text-gray-500">-</span>
|
||
</el-col>
|
||
<el-col :span="11">
|
||
<el-form-item prop="end_time">
|
||
<el-date-picker
|
||
v-model="form.end_time"
|
||
type="datetime"
|
||
placeholder="结束时间"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-form-item>
|
||
<el-form-item label="活动费用">
|
||
<el-input
|
||
v-model="form.cost"
|
||
placeholder="请输入活动费用"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="活动描述">
|
||
<el-input
|
||
v-model="form.description"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请输入活动描述"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="报名要求">
|
||
<el-input
|
||
v-model="form.requirements"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请输入报名要求"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="活动状态">
|
||
<el-select v-model="form.status">
|
||
<el-option
|
||
v-for="item in statusOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="活动图片" prop="image_url" required>
|
||
<el-upload
|
||
class="activity-image-uploader"
|
||
action="#"
|
||
:show-file-list="false"
|
||
:on-change="handleImageChange"
|
||
:before-upload="beforeImageUpload"
|
||
:auto-upload="false"
|
||
accept="image/*"
|
||
>
|
||
<img v-if="form.imageUrl" :src="form.imageUrl" class="activity-image" />
|
||
<el-icon v-else class="activity-image-uploader-icon"><Plus /></el-icon>
|
||
</el-upload>
|
||
<div v-if="form.imageUrl" class="image-actions">
|
||
<el-button type="danger" link @click="handleImageRemove">移除图片</el-button>
|
||
</div>
|
||
<div class="image-tip">建议尺寸:750x422px,格式:JPG、PNG,大小:不超过2MB</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||
</span>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 报名详情对话框 -->
|
||
<el-dialog
|
||
v-model="enrollmentDialogVisible"
|
||
:title="currentActivity?.title ? `报名详情 - ${currentActivity.title}` : '报名详情'"
|
||
width="900px"
|
||
destroy-on-close
|
||
>
|
||
<div class="enrollment-dialog-content" v-loading="enrollmentLoading">
|
||
<div class="enrollment-summary">
|
||
<div class="summary-item">
|
||
<span class="label">总人数上限:</span>
|
||
<span class="value">{{ currentActivity?.capacity ?? 0 }}</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="label">已报名人数:</span>
|
||
<span class="value">{{ currentActivity?.enrolled ?? 0 }}</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="label">剩余名额:</span>
|
||
<span class="value">{{ (currentActivity?.capacity ?? 0) - (currentActivity?.enrolled ?? 0) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<el-table :data="enrollmentList" style="width: 100%" border>
|
||
<el-table-column type="index" label="序号" width="80" align="center" />
|
||
<el-table-column prop="user_id" label="用户昵称" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
{{ userStore.userInfo?.nickname || userStore.userInfo?.username || row.user_id }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="enrollment_time" label="报名时间" min-width="180" align="center">
|
||
<template #default="{ row }">
|
||
{{ formatDateTime(row.enrollment_time) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="status" label="状态" width="120" align="center">
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-model="row.status"
|
||
placeholder="请选择状态"
|
||
size="small"
|
||
style="width: 100px"
|
||
@change="() => handleEnrollmentStatusChange(row)"
|
||
>
|
||
<el-option label="已取消" :value="0" />
|
||
<el-option label="已报名" :value="1" />
|
||
<el-option label="已完成" :value="2" />
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="feedback" label="活动反馈" min-width="200" show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<el-popover
|
||
placement="top-start"
|
||
trigger="hover"
|
||
:width="300"
|
||
v-if="row.feedback"
|
||
>
|
||
<template #default>
|
||
<div style="max-height: 200px; overflow-y: auto;">{{ row.feedback }}</div>
|
||
</template>
|
||
<template #reference>
|
||
<span>{{ row.feedback }}</span>
|
||
</template>
|
||
</el-popover>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="feedback_time" label="反馈时间" min-width="180" align="center">
|
||
<template #default="{ row }">
|
||
{{ row.feedback_time ? formatDateTime(row.feedback_time) : '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div class="enrollment-empty" v-if="!enrollmentLoading && (!enrollmentList || enrollmentList.length === 0)">
|
||
<el-empty description="暂无报名数据" />
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.study-management {
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
span {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.header-btns {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
.search-form {
|
||
margin-bottom: 24px;
|
||
padding: 24px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 16px;
|
||
margin-right: 24px;
|
||
|
||
&:last-child {
|
||
margin-right: 0;
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
:deep(.el-input),
|
||
:deep(.el-select) {
|
||
width: 220px;
|
||
}
|
||
|
||
.search-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
.pagination-container {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.text-center {
|
||
text-align: center;
|
||
}
|
||
|
||
.activity-image-uploader {
|
||
:deep(.el-upload) {
|
||
border: 1px dashed #d9d9d9;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: border-color 0.3s;
|
||
|
||
&:hover {
|
||
border-color: #409EFF;
|
||
}
|
||
}
|
||
}
|
||
|
||
.activity-image-uploader-icon {
|
||
font-size: 28px;
|
||
color: #8c939d;
|
||
width: 178px;
|
||
height: 178px;
|
||
text-align: center;
|
||
line-height: 178px;
|
||
}
|
||
|
||
.activity-image {
|
||
width: 178px;
|
||
height: 178px;
|
||
display: block;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.image-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 8px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.image-actions {
|
||
margin-top: 8px;
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.enrollment-dialog-content {
|
||
.enrollment-summary {
|
||
display: flex;
|
||
gap: 40px;
|
||
margin-bottom: 20px;
|
||
padding: 15px 20px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
|
||
.summary-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.label {
|
||
color: #606266;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.value {
|
||
color: #409EFF;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
|
||
.enrollment-empty {
|
||
padding: 40px 0;
|
||
}
|
||
}
|
||
}
|
||
</style> |