feat:新增期次管理模块、优化期次字段

zlx
zhoulexin 1 week ago
parent 796cb94bb1
commit 2614a5fba1

@ -52,3 +52,36 @@ export function getAllRegistrations() {
method: 'get'
})
}
// ====== 期次管理 API ======
export function getSessionList(params) {
return request({
url: '/api/period/list',
method: 'get',
params
})
}
export function addSession(data) {
return request({
url: '/api/period',
method: 'post',
data
})
}
export function updateSession(id, data) {
return request({
url: `/api/period/${id}`,
method: 'put',
data
})
}
export function deleteSession(id) {
return request({
url: `/api/period/${id}`,
method: 'delete'
})
}

@ -3,6 +3,7 @@ import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './assets/styles/main.css'
@ -14,5 +15,5 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
}
app.use(router)
app.use(ElementPlus)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')

@ -0,0 +1,21 @@
/**
* 格式化期次显示标签
* 同月第一期 5月22-24
* 不同月第一期 05月22日 - 06月01日
*/
export function formatPeriodLabel(periodName, startDate, endDate) {
if (!startDate || !endDate) return periodName
const startParts = startDate.split('-')
const endParts = endDate.split('-')
if (startParts.length !== 3 || endParts.length !== 3) return periodName
const startMonth = parseInt(startParts[1], 10)
const startDay = parseInt(startParts[2], 10)
const endMonth = parseInt(endParts[1], 10)
const endDay = parseInt(endParts[2], 10)
if (startMonth === endMonth) {
return `${periodName} ${startMonth}${startDay}-${endDay}`
} else {
const pad = (n) => String(n).padStart(2, '0')
return `${periodName} ${pad(startMonth)}${pad(startDay)}日 - ${pad(endMonth)}${pad(endDay)}`
}
}

@ -25,8 +25,27 @@
</div>
</div>
<!-- 主内容 -->
<div class="admin-main">
<!-- 主体区域侧边栏 + 内容区 -->
<div class="admin-body">
<!-- 侧边栏 -->
<el-aside class="admin-aside" width="220px">
<el-menu
:default-active="activeMenu"
@select="handleMenuSelect"
class="side-menu"
>
<el-menu-item index="registration">
<el-icon><Document /></el-icon>
<span>报名管理</span>
</el-menu-item>
<el-menu-item index="session">
<el-icon><List /></el-icon>
<span>期次管理</span>
</el-menu-item>
</el-menu>
</el-aside>
<div class="admin-content">
<div v-if="activeMenu === 'registration'" class="admin-main">
<!-- 统计卡片 -->
<div class="admin-stats">
<div class="stat-card">
@ -76,11 +95,15 @@
<el-option label="待支付" value="submitted" />
</el-select>
<el-select v-model="filterPeriod" placeholder="报名期次" clearable style="width: 200px;">
<el-option label="第一期 5月22-24日" value="第一期 5月22-24日" />
<el-option label="第二期 6月12-14日" value="第二期 6月12-14日" />
<el-option
v-for="item in formattedSessionOptions"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
<el-button type="primary" icon="Search" @click="handleSearch"></el-button>
<el-button icon="Refresh" @click="refreshData"></el-button>
<el-button icon="RefreshLeft" @click="resetData"></el-button>
</div>
<!-- 数据表格 -->
@ -124,7 +147,11 @@
<el-table-column prop="department" label="部门" width="140" show-overflow-tooltip />
<el-table-column prop="title" label="职务" width="100" align="center" />
<el-table-column prop="phone" label="手机号" width="120" align="center" />
<el-table-column prop="period" label="期次" width="180" align="center" />
<el-table-column label="期次" width="200" align="center">
<template #default="{ row }">
{{ getFormattedPeriodName(row.periodName) }}
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" width="90" align="center">
<template #default="{ row }">
<span class="amount-text">¥{{ row.amount }}</span>
@ -169,6 +196,9 @@
/>
</div>
</div>
</div>
<SessionManage v-else-if="activeMenu === 'session'" />
</div>
</div>
<!-- 详情弹窗 -->
@ -190,7 +220,7 @@
<el-descriptions-item label="手机号码">{{ currentDetail.phone }}</el-descriptions-item>
<el-descriptions-item label="电子邮箱">{{ currentDetail.email }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ currentDetail.idCard }}</el-descriptions-item>
<el-descriptions-item label="报名期次">{{ currentDetail.period }}</el-descriptions-item>
<el-descriptions-item label="报名期次">{{ getFormattedPeriodName(currentDetail.periodName) }}</el-descriptions-item>
<el-descriptions-item label="报名金额">¥{{ currentDetail.amount }}</el-descriptions-item>
<el-descriptions-item label="报名状态">
<el-tag :type="currentDetail.status === 'paid' ? 'success' : 'warning'">
@ -223,7 +253,9 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getRegistrationList, getRegistrationStatistics, updateRegistrationStatus, getRegistrationDetail, getAllRegistrations } from '../../api/registration'
import { getRegistrationList, getRegistrationStatistics, updateRegistrationStatus, getRegistrationDetail, getAllRegistrations, getSessionList } from '../../api/registration'
import { formatPeriodLabel } from '../../utils/format'
import SessionManage from './SessionManage.vue'
const router = useRouter()
//
@ -245,6 +277,12 @@ const adminUser = reactive({
username: 'admin'
})
//
const activeMenu = ref('registration')
const handleMenuSelect = (index) => {
activeMenu.value = index
}
//
const statistics = reactive({
total: 0,
@ -255,12 +293,35 @@ const statistics = reactive({
//
const allData = ref([])
const sessionOptions = ref([])
//
const filteredData = computed(() => {
return allData.value
})
//
const periodLabelMap = computed(() => {
const map = {}
sessionOptions.value.forEach(item => {
map[item.periodName] = formatPeriodLabel(item.periodName, item.startDate, item.endDate)
})
return map
})
//
const formattedSessionOptions = computed(() => {
return sessionOptions.value.map(item => ({
...item,
label: formatPeriodLabel(item.periodName, item.startDate, item.endDate)
}))
})
// periodName
const getFormattedPeriodName = (periodName) => {
return periodLabelMap.value[periodName] || periodName
}
//
const loadData = async () => {
loading.value = true
@ -270,7 +331,7 @@ const loadData = async () => {
pageNum: currentPage.value,
pageSize: pageSize.value,
status: filterStatus.value,
period: filterPeriod.value
periodId: filterPeriod.value
})
//
@ -281,7 +342,7 @@ const loadData = async () => {
department: item.department,
title: item.title,
phone: item.phone,
period: item.period,
periodName: item.periodName,
amount: item.fee,
status: item.status,
createTime: item.submitTime,
@ -313,11 +374,29 @@ const loadStatistics = async () => {
}
}
//
const refreshData = () => {
//
const loadSessionOptions = async () => {
try {
const res = await getSessionList({
pageNum:1,
pageSize: 9999
})
// res.data res.data.records
const list = Array.isArray(res.data) ? res.data : (res.data.records || [])
sessionOptions.value = list
} catch (e) {
console.error('加载期次列表失败', e)
}
}
//
const resetData = () => {
searchKeyword.value = ''
filterStatus.value = ''
filterPeriod.value = ''
currentPage.value = 1
loadData()
ElMessage.success('数据已刷新')
ElMessage.success('筛选条件已重置')
}
//
@ -418,7 +497,7 @@ const exportAllData = async () => {
手机号: item.phone,
邮箱: item.email,
身份证号: item.idCard,
期次: item.period,
期次: getFormattedPeriodName(item.periodName),
金额: item.fee,
状态: item.status === 'paid' ? '已支付' : '待支付',
报名时间: formatTime(item.submitTime),
@ -464,7 +543,7 @@ const exportData = () => {
手机号: item.phone,
邮箱: item.email,
身份证号: item.idCard,
期次: item.period,
期次: getFormattedPeriodName(item.periodName),
金额: item.amount,
状态: item.status === 'paid' ? '已支付' : '待支付',
报名时间: formatTime(item.createTime),
@ -512,12 +591,14 @@ onMounted(() => {
}
loadStatistics()
loadData()
loadSessionOptions()
})
</script>
<style scoped>
.admin-container {
min-height: 100vh;
height: 100vh;
overflow: hidden;
background: var(--bg-color);
}
@ -556,10 +637,36 @@ onMounted(() => {
background: var(--bg-color);
}
.admin-main {
.admin-body {
display: flex;
height: calc(100vh - 60px);
overflow: hidden;
}
.admin-aside {
background: #fff;
border-right: 1px solid #e8e8e8;
padding-top: 8px;
flex-shrink: 0;
height: 100%;
position: sticky;
top: 0;
}
.side-menu {
border-right: none;
height: 100%;
}
.admin-content {
flex: 1;
padding: 24px 32px;
overflow-y: auto;
background: var(--bg-color);
}
.admin-main {
max-width: 1400px;
margin: 0 auto;
}
.admin-stats {
@ -683,43 +790,61 @@ onMounted(() => {
.admin-header {
padding: 12px 16px;
}
.admin-main {
.admin-body {
flex-direction: column;
}
.admin-aside {
width: 100% !important;
border-right: none;
border-bottom: 1px solid #e8e8e8;
}
.side-menu .el-menu-item {
padding-left: 24px !important;
}
.admin-content {
padding: 16px;
}
.admin-main {
padding: 0;
}
.admin-stats {
grid-template-columns: repeat(2, 1fr);
}
.stat-card {
padding: 16px;
}
.stat-value {
font-size: 22px;
}
.search-bar {
flex-direction: column;
align-items: stretch;
}
.search-bar .el-input,
.search-bar .el-select {
width: 100% !important;
}
.table-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.el-table {
font-size: 12px;
}
.pagination-wrapper {
justify-content: center;
}

@ -0,0 +1,385 @@
<template>
<div class="session-manage">
<!-- 工具栏 -->
<div class="session-toolbar">
<h3>期次管理</h3>
<el-button type="primary" icon="Plus" @click="showAddDialog"></el-button>
</div>
<!-- 搜索筛选 -->
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="搜索期次名称、期次描述"
prefix-icon="Search"
clearable
style="max-width: 300px;"
@clear="handleSearch"
@keyup.enter="handleSearch"
/>
<el-select v-model="filterStatus" placeholder="筛选状态" clearable style="width: 150px;">
<el-option label="全部" value="" />
<el-option label="启用" value="active" />
<el-option label="停用" value="inactive" />
</el-select>
<el-button type="primary" icon="Search" @click="handleSearch"></el-button>
<el-button icon="Refresh" @click="handleRefresh"></el-button>
</div>
<!-- 数据表格 -->
<div class="session-table-card">
<el-table
:data="sessionList"
stripe
border
v-loading="loading"
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: '600' }"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="periodName" label="期次名称" width="140" align="center" />
<el-table-column prop="description" label="期次描述" min-width="200" show-overflow-tooltip />
<el-table-column label="培训日期" width="200" align="center">
<template #default="{ row }">
{{ formatDateRange(row.startDate, row.endDate) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
{{ row.status === 'active' ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" align="center">
<template #default="{ row }">
{{ formatTime(row.createTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link icon="Edit" @click="showEditDialog(row)"></el-button>
<el-button type="danger" link icon="Delete" @click="handleDelete(row)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
:current-page="currentPage"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑期次' : '新增期次'"
width="520px"
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
size="default"
>
<el-form-item label="期次名称" prop="periodName">
<el-input v-model="form.periodName" placeholder="如:第一期" maxlength="50" />
</el-form-item>
<el-form-item label="期次描述" prop="description">
<el-input
v-model="form.description"
type="textarea"
:rows="3"
placeholder="请输入期次描述"
maxlength="500"
show-word-limit
/>
</el-form-item>
<el-form-item label="培训日期" prop="dateRange">
<el-date-picker
v-model="form.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="form.status"
active-value="active"
inactive-value="inactive"
active-text="启用"
inactive-text="停用"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getSessionList, addSession, updateSession, deleteSession } from '../../api/registration'
//
const loading = ref(false)
const sessionList = ref([])
const dialogVisible = ref(false)
const isEdit = ref(false)
const editId = ref(null)
const submitLoading = ref(false)
const formRef = ref(null)
//
const searchKeyword = ref('')
const filterStatus = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
//
const form = reactive({
periodName: '',
description: '',
dateRange: [],
status: 'active'
})
//
const rules = {
periodName: [
{ required: true, message: '请输入期次名称', trigger: 'blur' }
],
description: [
{ max: 500, message: '期次描述不能超过500个字符', trigger: 'blur' }
],
dateRange: [
{ required: true, message: '请选择培训日期', trigger: 'change' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
]
}
//
const formatTime = (time) => {
if (!time) return '-'
const date = new Date(time)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
// 522-24522 - 61
const formatDateRange = (startDate, endDate) => {
if (!startDate || !endDate) return '-'
const sMM = parseInt(startDate.slice(5, 7), 10)
const sDD = parseInt(startDate.slice(8, 10), 10)
const eMM = parseInt(endDate.slice(5, 7), 10)
const eDD = parseInt(endDate.slice(8, 10), 10)
if (eMM !== sMM) {
return `${sMM}${sDD}日 - ${eMM}${eDD}`
}
return `${sMM}${sDD}-${eDD}`
}
//
const loadSessionList = async () => {
loading.value = true
try {
const params = {
pageNum: currentPage.value,
pageSize: pageSize.value,
status: filterStatus.value || undefined
}
if (searchKeyword.value) {
params.keyword = searchKeyword.value
}
const res = await getSessionList(params)
sessionList.value = res.data.records || []
total.value = res.data.total || 0
} catch (e) {
console.error('加载期次列表失败', e)
sessionList.value = []
total.value = 0
}
loading.value = false
}
//
const handleSearch = () => {
currentPage.value = 1
loadSessionList()
}
//
const handleRefresh = () => {
searchKeyword.value = ''
filterStatus.value = ''
currentPage.value = 1
loadSessionList()
}
//
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1
loadSessionList()
}
const handleCurrentChange = (val) => {
currentPage.value = val
loadSessionList()
}
//
const resetForm = () => {
form.periodName = ''
form.description = ''
form.dateRange = []
form.status = 'active'
}
//
const showAddDialog = () => {
isEdit.value = false
editId.value = null
resetForm()
dialogVisible.value = true
}
//
const showEditDialog = (row) => {
isEdit.value = true
editId.value = row.id
form.periodName = row.periodName || ''
form.description = row.description || ''
form.dateRange = row.startDate && row.endDate ? [row.startDate, row.endDate] : []
form.status = row.status || 'active'
dialogVisible.value = true
}
//
const submitForm = async () => {
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
const [startDate, endDate] = form.dateRange || []
const data = {
periodName: form.periodName,
description: form.description,
startDate,
endDate,
status: form.status
}
if (isEdit.value) {
await updateSession(editId.value, data)
ElMessage.success('期次更新成功!')
} else {
await addSession(data)
ElMessage.success('期次新增成功!')
}
dialogVisible.value = false
loadSessionList()
} catch (e) {
console.error('保存期次失败', e)
}
submitLoading.value = false
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除期次"${row.periodName}"吗?删除后不可恢复。`,
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
await deleteSession(row.id)
ElMessage.success('期次已删除!')
loadSessionList()
} catch (e) {
console.error('删除期次失败', e)
}
}).catch(() => {})
}
//
onMounted(() => {
loadSessionList()
})
</script>
<style scoped>
.session-manage {
padding: 0;
}
.session-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.session-toolbar h3 {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.search-bar {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
background: #fff;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.session-table-card {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
}
.pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>

@ -177,17 +177,17 @@
<div class="form-section-title">培训信息</div>
<div class="form-row">
<div class="form-item">
<el-form-item label="报名期次" prop="period">
<el-radio-group v-model="formData.period" disabled>
<el-radio-button label="第一期 5月22-24日">第一期 5月22-24</el-radio-button>
</el-radio-group>
<el-radio-group v-model="formData.period">
<el-radio-button label="第二期 6月12-14日">第二期 6月12-14</el-radio-button>
</el-radio-group>
<!-- <el-radio-group v-model="formData.period">
<el-radio-button label="河北 ">河北 </el-radio-button>
</el-radio-group> -->
</el-form-item>
<el-form-item label="报名期次" prop="periodId">
<el-radio-group v-model="formData.periodId">
<el-radio-button
v-for="item in sessionOptions"
:key="item.id"
:value="item.id"
>
{{ formatPeriodLabel(item.periodName, item.startDate, item.endDate) }}
</el-radio-button>
</el-radio-group>
</el-form-item>
</div>
</div>
@ -232,17 +232,30 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { submitRegistration, sendSmsCode } from '../../api/registration'
import { submitRegistration, sendSmsCode, getSessionList } from '../../api/registration'
import { formatPeriodLabel } from '../../utils/format'
const router = useRouter()
const formRef = ref(null)
const sessionOptions = ref([])
const smsCountdown = ref(0)
const submitLoading = ref(false)
let smsTimer = null
//
const loadSessionOptions = async () => {
try {
const res = await getSessionList({ pageNum: 1, pageSize: 9999 })
const list = Array.isArray(res.data) ? res.data : (res.data.records || [])
sessionOptions.value = list
} catch (e) {
console.error('加载期次失败', e)
}
}
//
const formData = reactive({
name: '',
@ -255,7 +268,7 @@ const formData = reactive({
smsCode: '',
email: '',
idCard: '',
period: '第一期 5月22-24日',
periodId: '',
remark: ''
})
@ -333,7 +346,7 @@ const formRules = {
idCard: [
{ required: true, validator: validateIdCard, trigger: 'blur' }
],
period: [
periodId: [
{ required: true, message: '请选择报名期次', trigger: 'change' }
]
}
@ -383,7 +396,7 @@ const submitForm = async () => {
name: formData.name,
referrer: formData.referrer,
referrerCompany: formData.referrerCompany,
period: formData.period,
periodId: formData.periodId,
phone: formData.phone,
remark: formData.remark,
title: formData.title
@ -401,7 +414,7 @@ const submitForm = async () => {
referrer: res.data.referrer,
referrerCompany: res.data.referrerCompany,
company: res.data.company,
period: res.data.period
periodId: formData.periodId
}
})
}, 1500)
@ -416,10 +429,19 @@ const submitForm = async () => {
}
})
}
//
onMounted(() => {
loadSessionOptions()
})
</script>
<style scoped>
:deep(.el-radio-group) {
gap: 10px;
}
:deep(.el-form-item__content){
gap: 10px;
}

@ -31,7 +31,7 @@
</div>
<div class="info-item bottomBr">
<span class="info-label">报名期次</span>
<span class="info-value">{{ registrationData.period }}</span>
<span class="info-value">{{ formattedPeriodName }}</span>
</div>
</div>
<div class="info-row">
@ -119,18 +119,42 @@ import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import payCode from '@/assets/payCode.png'
import { getSessionList } from '../../api/registration'
import { formatPeriodLabel } from '../../utils/format'
const route = useRoute()
const router = useRouter()
const registrationData = ref(null)
const sessionOptions = ref([])
// ID
const formattedPeriodName = computed(() => {
const periodId = Number(registrationData.value?.periodId)
if (!periodId) return registrationData.value?.periodId || ''
const found = sessionOptions.value.find(item => item.id === periodId)
if (found) {
return formatPeriodLabel(found.periodName, found.startDate, found.endDate)
}
return registrationData.value?.periodId || ''
})
//
onMounted(() => {
onMounted(async () => {
//
try {
const res = await getSessionList({ pageNum: 1, pageSize: 9999 })
const list = Array.isArray(res.data) ? res.data : (res.data.records || [])
sessionOptions.value = list
} catch (e) {
console.error('加载期次失败', e)
}
const query = route.query
registrationData.value = {
bmId: query.bmId,
name: query.name,
company: query.company,
period: query.period,
periodId: query.periodId,
referrer: query.referrer,
referrerCompany: query.referrerCompany
}

Loading…
Cancel
Save