update
This commit is contained in:
1 parent
37997203b2
commit
b4fac2bd3c
2 files changed
+30
-6
No files matched your search
@@ -8,12 +8,13 @@ type ProjectStatus = 'Draft' | 'Published' | 'Closed'
|
||||
type ApplicationStatus = 'PendingReview' | 'PendingPayment' | 'Confirmed' | 'Waitlisted' | 'Rejected' | 'Cancelled'
|
||||
interface Project { id: string; code: string; name: string; description?: string; registrationStartsAt: string; registrationEndsAt: string; capacity: number; feeAmount: number; requiresReview: boolean; status: ProjectStatus; contactName?: string; contactPhone?: string; notes?: string; collegeName: string; confirmedCount: number; isAvailableNow: boolean }
|
||||
interface Application { id: string; projectId: string; projectCode: string; projectName: string; status: ApplicationStatus; applicantNote?: string; createdAt: string; reviewComment?: string; studentId: string; studentNumber: string; studentName: string; order?: { id: string; orderNumber: string; amount: number; paymentMethod: 'Free' | 'Offline' | 'Balance'; status: 'Pending' | 'Paid' | 'Cancelled'; referenceNumber?: string } }
|
||||
interface PagedResult<T> { items: T[]; total: number; page: number; pageSize: number }
|
||||
|
||||
const auth = useAuthStore()
|
||||
const isStudent = computed(() => auth.user?.roles.includes('Student') ?? false)
|
||||
const canManage = computed(() => auth.user?.roles.some(x => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(x)) ?? false)
|
||||
const tab = ref(isStudent.value ? 'projects' : 'manage')
|
||||
const projects = ref<Project[]>([]); const mine = ref<Application[]>([]); const applications = ref<Application[]>([]); const loading = ref(false); const dialog = ref(false); const editingId = ref<string | null>(null)
|
||||
const projects = ref<Project[]>([]); const mine = ref<Application[]>([]); const applications = ref<Application[]>([]); const applicationsPage = ref(1); const applicationsPageSize = ref(20); const applicationsTotal = ref(0); const loading = ref(false); const dialog = ref(false); const editingId = ref<string | null>(null)
|
||||
const balance = ref(0)
|
||||
const form = reactive({ code: '', name: '', description: '', registrationStartsAt: '', registrationEndsAt: '', capacity: 50, feeAmount: 0, requiresReview: true, status: 'Draft' as ProjectStatus, contactName: '', contactPhone: '', notes: '' })
|
||||
const statusLabel: Record<ApplicationStatus, string> = { PendingReview: '待审核', PendingPayment: '待缴费', Confirmed: '已确认', Waitlisted: '候补中', Rejected: '已驳回', Cancelled: '已取消' }
|
||||
@@ -28,7 +29,9 @@ function resetForm(project?: Project) { editingId.value = project?.id ?? null; O
|
||||
async function loadProjects() { const { data } = await http.get('/campus-registrations/projects'); projects.value = data }
|
||||
async function loadMine() { if (isStudent.value) { const { data } = await http.get('/campus-registrations/mine'); mine.value = data } }
|
||||
async function loadWallet() { if (isStudent.value) { const { data } = await http.get('/campus-registrations/wallet/mine'); balance.value = data.balance } }
|
||||
async function loadApplications() { if (canManage.value) { const { data } = await http.get('/campus-registrations/applications'); applications.value = data } }
|
||||
async function loadApplications() { if (canManage.value) { const { data } = await http.get<PagedResult<Application>>('/campus-registrations/applications', { params: { page: applicationsPage.value, pageSize: applicationsPageSize.value } }); applications.value = data.items; applicationsTotal.value = data.total; if (applications.value.length === 0 && applicationsTotal.value > 0 && applicationsPage.value > 1) { applicationsPage.value--; await loadApplications() } } }
|
||||
async function changeApplicationsPage(page: number) { applicationsPage.value = page; await loadApplications() }
|
||||
async function changeApplicationsPageSize(pageSize: number) { applicationsPageSize.value = pageSize; applicationsPage.value = 1; await loadApplications() }
|
||||
async function saveProject() { try { const payload = { ...form, registrationStartsAt: new Date(form.registrationStartsAt).toISOString(), registrationEndsAt: new Date(form.registrationEndsAt).toISOString(), collegeId: null }; if (editingId.value) await http.put(`/campus-registrations/projects/${editingId.value}`, payload); else await http.post('/campus-registrations/projects', payload); ElMessage.success('项目已保存'); dialog.value = false; await loadProjects() } catch (e) { ElMessage.error(apiErrorMessage(e)) } }
|
||||
async function apply(project: Project) { try { const { value } = await ElMessageBox.prompt('可选:填写报名说明', `报名:${project.name}`, { inputPlaceholder: '报名说明' }); await http.post(`/campus-registrations/projects/${project.id}/apply`, { note: value || null }); ElMessage.success('报名已提交'); await Promise.all([loadProjects(), loadMine()]) } catch (e) { if (e !== 'cancel' && e !== 'close') ElMessage.error(apiErrorMessage(e)) } }
|
||||
async function review(item: Application, approved: boolean) { try { const { value } = await ElMessageBox.prompt(approved ? '审核通过后将生成缴费订单或直接确认免费报名。' : '请填写驳回原因', approved ? '通过报名' : '驳回报名', { inputPlaceholder: approved ? '可选审核意见' : '驳回原因' }); await http.post(`/campus-registrations/applications/${item.id}/review`, { approved, comment: value || null }); ElMessage.success('处理完成'); await loadApplications() } catch (e) { if (e !== 'cancel' && e !== 'close') ElMessage.error(apiErrorMessage(e)) } }
|
||||
@@ -44,11 +47,11 @@ onMounted(async () => { loading.value = true; try { await Promise.all([loadProje
|
||||
<header class="page-heading"><div><p class="eyebrow">CAMPUS SERVICES</p><h2>校园支付与报名</h2><p>报名、审核、线下缴费确认和名单导出在同一闭环内完成。</p></div><el-button v-if="canManage" type="primary" @click="resetForm()">新建报名项目</el-button></header>
|
||||
<el-tabs v-model="tab"><el-tab-pane label="报名项目" name="projects"><div class="project-grid"><el-card v-for="project in projects" :key="project.id" class="project-card"><template #header><div class="card-head"><b>{{ project.name }}</b><el-tag :type="project.status === 'Published' ? 'success' : 'info'">{{ projectStatusLabel[project.status] }}</el-tag></div></template><p class="code">{{ project.code }} · {{ project.collegeName }}</p><p class="description">{{ project.description || '暂无项目说明。' }}</p><dl><div><dt>报名时间</dt><dd>{{ localDateTime(project.registrationStartsAt) }} 至 {{ localDateTime(project.registrationEndsAt) }}</dd></div><div><dt>名额 / 已确认</dt><dd>{{ project.capacity }} / {{ project.confirmedCount }} 人</dd></div><div><dt>费用</dt><dd>{{ amount(project.feeAmount) }} {{ project.requiresReview ? '· 需审核' : '· 自动受理' }}</dd></div></dl><div class="card-actions"><el-button v-if="isStudent" type="primary" :disabled="!project.isAvailableNow" @click="apply(project)">{{ project.isAvailableNow ? '立即报名' : '当前不可报名' }}</el-button><el-button v-if="canManage" @click="resetForm(project)">编辑项目</el-button></div></el-card></div><el-empty v-if="projects.length === 0" description="暂无可见报名项目" /></el-tab-pane>
|
||||
<el-tab-pane v-if="isStudent" label="我的报名与订单" name="mine"><div class="wallet"><span>校园余额</span><b>{{ amount(balance) }}</b><small>余额由管理员调整,可用于支付待缴费报名。</small></div><el-table :data="mine"><el-table-column prop="projectName" label="项目" min-width="190"/><el-table-column label="状态" width="120"><template #default="{ row }"><el-tag :type="applicationStatus(row.status).type">{{ applicationStatus(row.status).label }}</el-tag></template></el-table-column><el-table-column label="订单" min-width="210"><template #default="{ row }"><span v-if="application(row).order">{{ amount(application(row).order!.amount) }} · {{ application(row).order!.paymentMethod === 'Offline' ? '线下缴费' : application(row).order!.paymentMethod === 'Balance' ? '余额支付' : '免费' }} · {{ application(row).order!.status === 'Paid' ? '已确认' : '待确认' }}</span><span v-else>—</span></template></el-table-column><el-table-column prop="reviewComment" label="审核意见" min-width="160"/><el-table-column label="操作" width="160"><template #default="{ row }"><el-button v-if="application(row).status === 'PendingPayment' && application(row).order?.status === 'Pending'" link type="primary" @click="payBalance(application(row))">余额支付</el-button><el-button v-if="['PendingReview','PendingPayment','Waitlisted'].includes(application(row).status)" link type="danger" @click="cancel(application(row))">撤销</el-button></template></el-table-column></el-table><el-empty v-if="mine.length === 0" description="尚未提交报名" /></el-tab-pane>
|
||||
<el-tab-pane v-if="canManage" label="审核与收款" name="manage"><div class="toolbar"><span>所有操作均按项目所属学院的数据范围限制。</span><el-button @click="exportApplications">导出当前可见报名</el-button></div><el-table :data="applications"><el-table-column label="项目与学生" min-width="220"><template #default="{ row }"><b>{{ application(row).projectName }}</b><small>{{ application(row).projectCode }} · {{ application(row).studentNumber }} {{ application(row).studentName }}</small></template></el-table-column><el-table-column label="报名状态" width="120"><template #default="{ row }"><el-tag :type="applicationStatus(row.status).type">{{ applicationStatus(row.status).label }}</el-tag></template></el-table-column><el-table-column label="订单" min-width="190"><template #default="{ row }"><span v-if="application(row).order">{{ application(row).order!.orderNumber }}<small>{{ amount(application(row).order!.amount) }} · {{ application(row).order!.status }}</small></span><span v-else>—</span></template></el-table-column><el-table-column prop="createdAt" label="提交时间" width="170"><template #default="{ row }">{{ localDateTime(application(row).createdAt) }}</template></el-table-column><el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button v-if="application(row).status === 'PendingReview'" link type="success" @click="review(application(row), true)">通过</el-button><el-button v-if="application(row).status === 'PendingReview'" link type="danger" @click="review(application(row), false)">驳回</el-button><el-button v-if="application(row).status === 'PendingPayment' && application(row).order?.paymentMethod === 'Offline'" link type="primary" @click="confirmOffline(application(row))">确认收款</el-button></template></el-table-column></el-table><el-empty v-if="applications.length === 0" description="暂无待处理或历史报名" /></el-tab-pane></el-tabs>
|
||||
<el-tab-pane v-if="canManage" label="审核与收款" name="manage"><div class="toolbar"><span>所有操作均按项目所属学院的数据范围限制。</span><el-button @click="exportApplications">导出当前可见报名</el-button></div><el-table :data="applications"><el-table-column label="项目与学生" min-width="220"><template #default="{ row }"><b>{{ application(row).projectName }}</b><small>{{ application(row).projectCode }} · {{ application(row).studentNumber }} {{ application(row).studentName }}</small></template></el-table-column><el-table-column label="报名状态" width="120"><template #default="{ row }"><el-tag :type="applicationStatus(row.status).type">{{ applicationStatus(row.status).label }}</el-tag></template></el-table-column><el-table-column label="订单" min-width="190"><template #default="{ row }"><span v-if="application(row).order">{{ application(row).order!.orderNumber }}<small>{{ amount(application(row).order!.amount) }} · {{ application(row).order!.status }}</small></span><span v-else>—</span></template></el-table-column><el-table-column prop="createdAt" label="提交时间" width="170"><template #default="{ row }">{{ localDateTime(application(row).createdAt) }}</template></el-table-column><el-table-column label="操作" width="190" fixed="right"><template #default="{ row }"><el-button v-if="application(row).status === 'PendingReview'" link type="success" @click="review(application(row), true)">通过</el-button><el-button v-if="application(row).status === 'PendingReview'" link type="danger" @click="review(application(row), false)">驳回</el-button><el-button v-if="application(row).status === 'PendingPayment' && application(row).order?.paymentMethod === 'Offline'" link type="primary" @click="confirmOffline(application(row))">确认收款</el-button></template></el-table-column></el-table><el-pagination v-if="applicationsTotal > 0" class="pagination" background layout="total, sizes, prev, pager, next" :current-page="applicationsPage" :page-size="applicationsPageSize" :page-sizes="[20, 50, 100]" :total="applicationsTotal" @update:current-page="changeApplicationsPage" @update:page-size="changeApplicationsPageSize"/><el-empty v-if="applications.length === 0" description="暂无待处理或历史报名" /></el-tab-pane></el-tabs>
|
||||
<el-dialog v-model="dialog" :title="editingId ? '编辑报名项目' : '新建报名项目'" width="680px"><el-form label-position="top"><div class="form-grid"><el-form-item label="项目编码" required><el-input v-model="form.code" placeholder="如 CAMPUS-2026-001"/></el-form-item><el-form-item label="项目名称" required><el-input v-model="form.name"/></el-form-item><el-form-item label="报名开始" required><el-date-picker v-model="form.registrationStartsAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%"/></el-form-item><el-form-item label="报名结束" required><el-date-picker v-model="form.registrationEndsAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" style="width:100%"/></el-form-item><el-form-item label="名额" required><el-input-number v-model="form.capacity" :min="1" style="width:100%"/></el-form-item><el-form-item label="收费金额(元)"><el-input-number v-model="form.feeAmount" :min="0" :precision="2" style="width:100%"/></el-form-item></div><el-form-item label="项目说明"><el-input v-model="form.description" type="textarea" :rows="3"/></el-form-item><el-form-item label="受理方式"><el-switch v-model="form.requiresReview" active-text="人工审核后受理" inactive-text="直接受理"/></el-form-item><el-form-item label="发布状态"><el-radio-group v-model="form.status"><el-radio-button value="Draft">草稿</el-radio-button><el-radio-button value="Published">发布</el-radio-button><el-radio-button value="Closed">关闭</el-radio-button></el-radio-group></el-form-item><div class="form-grid"><el-form-item label="联系人"><el-input v-model="form.contactName"/></el-form-item><el-form-item label="联系电话"><el-input v-model="form.contactPhone"/></el-form-item></div><el-form-item label="管理备注"><el-input v-model="form.notes" type="textarea" :rows="2"/></el-form-item></el-form><template #footer><el-button @click="dialog=false">取消</el-button><el-button type="primary" @click="saveProject">保存</el-button></template></el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.registration-page { display:grid; gap:20px }.page-heading,.card-head,.toolbar { display:flex; align-items:center; justify-content:space-between; gap:16px }.page-heading h2{margin:0;color:#183b56}.page-heading p{margin:5px 0;color:#6c7f90}.eyebrow{color:#17867c!important;font-size:12px;font-weight:800;letter-spacing:.16em}.project-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:16px}.project-card{border:1px solid #dfe9ec}.card-head b{color:#183b56}.code,dt,small{color:#738596;font-size:13px}.description{min-height:42px;color:#385368}dl{display:grid;gap:8px;margin:16px 0}dl div{display:grid;gap:2px}dd{margin:0;color:#29495f}.card-actions{display:flex;justify-content:flex-end;border-top:1px solid #edf1f2;padding-top:12px}.toolbar{margin-bottom:16px;color:#6c7f90}.wallet{display:flex;align-items:baseline;gap:12px;padding:14px 18px;margin-bottom:16px;border:1px solid #cfe8e1;border-radius:10px;background:#f1faf7;color:#486575}.wallet b{font-size:24px;color:#17867c}.el-table b,.el-table small{display:block}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}@media(max-width:720px){.page-heading,.toolbar{align-items:flex-start;flex-direction:column}.page-heading .el-button,.toolbar .el-button{width:100%}.form-grid{grid-template-columns:1fr}.wallet{align-items:flex-start;flex-direction:column;gap:3px}}
|
||||
.registration-page { display:grid; gap:20px }.page-heading,.card-head,.toolbar { display:flex; align-items:center; justify-content:space-between; gap:16px }.page-heading h2{margin:0;color:#183b56}.page-heading p{margin:5px 0;color:#6c7f90}.eyebrow{color:#17867c!important;font-size:12px;font-weight:800;letter-spacing:.16em}.project-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:16px}.project-card{border:1px solid #dfe9ec}.card-head b{color:#183b56}.code,dt,small{color:#738596;font-size:13px}.description{min-height:42px;color:#385368}dl{display:grid;gap:8px;margin:16px 0}dl div{display:grid;gap:2px}dd{margin:0;color:#29495f}.card-actions{display:flex;justify-content:flex-end;border-top:1px solid #edf1f2;padding-top:12px}.toolbar{margin-bottom:16px;color:#6c7f90}.pagination{justify-content:flex-end;margin-top:16px}.wallet{display:flex;align-items:baseline;gap:12px;padding:14px 18px;margin-bottom:16px;border:1px solid #cfe8e1;border-radius:10px;background:#f1faf7;color:#486575}.wallet b{font-size:24px;color:#17867c}.el-table b,.el-table small{display:block}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}@media(max-width:720px){.page-heading,.toolbar{align-items:flex-start;flex-direction:column}.page-heading .el-button,.toolbar .el-button{width:100%}.form-grid{grid-template-columns:1fr}.wallet{align-items:flex-start;flex-direction:column;gap:3px}}
|
||||
</style>
|
||||
Reference in new issue
Block a user