2025-03-08 15:42:36 +08:00

1077 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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格式JPGPNG大小不超过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>