This commit is contained in:
李旭明 committed 2025-08-26 10:10:51 +08:00
1 parent 4c2154bfde
commit e1980bd202
12 files changed
+5154

No files matched your search

@@ -0,0 +1,40 @@
package org.dromara.stgl.domain.bo;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Size;
import java.util.List;
/**
* 社团活动批量审核请求对象
*
* @author ruoyi
* @date 2025-01-31
*/
@Data
public class StglBatchApprovalBo {
/**
* 报名ID数组
*/
@NotEmpty(message = "审核ID列表不能为空")
private List<Long> ids;
/**
* 审核状态 1-通过 2-驳回
*/
@NotNull(message = "审核状态不能为空")
@Min(value = 1, message = "审核状态值无效")
@Max(value = 2, message = "审核状态值无效")
private Integer approvalStatus;
/**
* 审核意见
*/
@Size(max = 500, message = "审核意见不能超过500个字符")
private String approvalRemark;
}
@@ -0,0 +1,189 @@
package org.dromara.stgl.utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.system.domain.SysUserRole;
import org.dromara.system.mapper.SysUserRoleMapper;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
/**
* 指导老师角色分配工具类
* 用于在社团、志愿者队伍、活动等模块中自动分配指导老师角色
*
* @author Cursor AI
* @date 2025-01-03
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AdvisorRoleAssigner {
private final SysUserRoleMapper sysUserRoleMapper;
/**
* 指导老师角色ID
*/
private static final Long ADVISOR_ROLE_ID = 1959880215953952769L;
/**
* 为指导老师分配角色
*
* @param advisorUserIds 指导老师用户ID列表
*/
@Transactional(rollbackFor = Exception.class)
public void assignAdvisorRole(List<Long> advisorUserIds) {
if (advisorUserIds == null || advisorUserIds.isEmpty()) {
log.info("没有指导老师需要分配角色");
return;
}
List<SysUserRole> userRoles = new ArrayList<>();
for (Long userId : advisorUserIds) {
if (userId != null) {
// 检查用户是否已有该角色
if (!hasUserRole(userId, ADVISOR_ROLE_ID)) {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(userId);
userRole.setRoleId(ADVISOR_ROLE_ID);
userRoles.add(userRole);
} else {
log.debug("用户 {} 已有指导老师角色,跳过分配", userId);
}
}
}
if (!userRoles.isEmpty()) {
try {
// 批量插入用户角色关系
boolean success = sysUserRoleMapper.insertBatch(userRoles);
if (success) {
log.info("成功为 {} 个指导老师分配角色,角色ID: {}", userRoles.size(), ADVISOR_ROLE_ID);
} else {
log.error("批量分配指导老师角色失败");
}
} catch (Exception e) {
log.error("分配指导老师角色时发生异常", e);
throw new RuntimeException("分配指导老师角色失败", e);
}
}
}
/**
* 为单个指导老师分配角色
*
* @param advisorUserId 指导老师用户ID
*/
@Transactional(rollbackFor = Exception.class)
public void assignAdvisorRole(Long advisorUserId) {
if (advisorUserId == null) {
log.warn("指导老师用户ID为空,跳过角色分配");
return;
}
// 检查用户是否已有该角色
if (!hasUserRole(advisorUserId, ADVISOR_ROLE_ID)) {
try {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(advisorUserId);
userRole.setRoleId(ADVISOR_ROLE_ID);
int result = sysUserRoleMapper.insert(userRole);
if (result > 0) {
log.info("成功为用户 {} 分配指导老师角色,角色ID: {}", advisorUserId, ADVISOR_ROLE_ID);
} else {
log.error("为用户 {} 分配指导老师角色失败", advisorUserId);
}
} catch (Exception e) {
log.error("为用户 {} 分配指导老师角色时发生异常", advisorUserId, e);
throw new RuntimeException("分配指导老师角色失败", e);
}
} else {
log.debug("用户 {} 已有指导老师角色,跳过分配", advisorUserId);
}
}
/**
* 检查用户是否已有指定角色
*
* @param userId 用户ID
* @param roleId 角色ID
* @return 是否已有该角色
*/
private boolean hasUserRole(Long userId, Long roleId) {
try {
// 查询用户角色关系
List<Long> roleIds = sysUserRoleMapper.selectUserIdsByRoleId(roleId);
return roleIds.contains(userId);
} catch (Exception e) {
log.error("检查用户角色关系时发生异常,用户ID: {}, 角色ID: {}", userId, roleId, e);
// 如果查询失败,假设用户没有该角色,继续分配
return false;
}
}
/**
* 移除指导老师角色
*
* @param advisorUserId 指导老师用户ID
*/
@Transactional(rollbackFor = Exception.class)
public void removeAdvisorRole(Long advisorUserId) {
if (advisorUserId == null) {
log.warn("指导老师用户ID为空,跳过角色移除");
return;
}
try {
// 删除用户角色关系
int result = sysUserRoleMapper.delete(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUserRole>()
.eq(SysUserRole::getUserId, advisorUserId)
.eq(SysUserRole::getRoleId, ADVISOR_ROLE_ID)
);
if (result > 0) {
log.info("成功移除用户 {} 的指导老师角色", advisorUserId);
} else {
log.debug("用户 {} 没有指导老师角色,无需移除", advisorUserId);
}
} catch (Exception e) {
log.error("移除用户 {} 的指导老师角色时发生异常", advisorUserId, e);
throw new RuntimeException("移除指导老师角色失败", e);
}
}
/**
* 批量移除指导老师角色
*
* @param advisorUserIds 指导老师用户ID列表
*/
@Transactional(rollbackFor = Exception.class)
public void removeAdvisorRoles(List<Long> advisorUserIds) {
if (advisorUserIds == null || advisorUserIds.isEmpty()) {
log.info("没有指导老师需要移除角色");
return;
}
try {
// 批量删除用户角色关系
int result = sysUserRoleMapper.delete(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUserRole>()
.in(SysUserRole::getUserId, advisorUserIds)
.eq(SysUserRole::getRoleId, ADVISOR_ROLE_ID)
);
if (result > 0) {
log.info("成功移除 {} 个用户的指导老师角色", result);
} else {
log.debug("没有用户需要移除指导老师角色");
}
} catch (Exception e) {
log.error("批量移除指导老师角色时发生异常", e);
throw new RuntimeException("批量移除指导老师角色失败", e);
}
}
}
@@ -0,0 +1,108 @@
<template>
<el-dialog :title="`${activityName}活动报名管理`" :model-value="visible" @update:model-value="handleVisibleChange" width="1400px" append-to-body>
<el-tabs v-model="activeTab" type="border-card">
<!-- 已报名标签页 -->
<el-tab-pane label="已报名" name="registered">
<RegisteredMembersTab
:activity-id="activityId"
:activity-name="activityName"
ref="registeredTabRef"
/>
</el-tab-pane>
<!-- 待审核标签页 -->
<el-tab-pane label="待审核" name="pending">
<PendingMembersTab
:activity-id="activityId"
:activity-name="activityName"
ref="pendingTabRef"
/>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关 闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import RegisteredMembersTab from './RegisteredMembersTab.vue';
import PendingMembersTab from './PendingMembersTab.vue';
interface Props {
visible: boolean;
activityId: string | number;
activityName: string;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const activeTab = ref('registered');
const registeredTabRef = ref();
const pendingTabRef = ref();
// 处理弹窗显示状态变化
const handleVisibleChange = (value: boolean) => {
emit('update:visible', value);
};
// 监听弹窗显示状态,当弹窗打开时刷新数据
watch(() => props.visible, (newVal) => {
if (newVal) {
// 弹窗打开时刷新当前标签页数据
nextTick(() => {
if (activeTab.value === 'registered' && registeredTabRef.value) {
registeredTabRef.value.getList();
} else if (activeTab.value === 'pending' && pendingTabRef.value) {
pendingTabRef.value.getList();
}
});
}
});
// 标签页切换时刷新数据
const handleTabChange = (tabName: string) => {
nextTick(() => {
if (tabName === 'registered' && registeredTabRef.value) {
registeredTabRef.value.getList();
} else if (tabName === 'pending' && pendingTabRef.value) {
pendingTabRef.value.getList();
}
});
};
// 关闭弹窗
const handleClose = () => {
emit('update:visible', false);
};
// 暴露方法给父组件
defineExpose({
refreshData: () => {
if (activeTab.value === 'registered' && registeredTabRef.value) {
registeredTabRef.value.getList();
} else if (activeTab.value === 'pending' && pendingTabRef.value) {
pendingTabRef.value.getList();
}
}
});
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
:deep(.el-tabs__content) {
padding: 20px 0;
}
</style>
@@ -0,0 +1,840 @@
<template>
<div class="activity-registration-view">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="always" style="padding: 8px 16px;">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" style="padding: 0;">
<el-row :gutter="16" align="middle">
<el-col :span="5">
<el-form-item label="姓名" prop="studentName" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系方式" prop="phone" style="margin-bottom: 0;">
<el-input v-model="queryParams.phone" placeholder="请输入联系方式" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5" v-if="!isViewMode">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['stgl:activityRegister:add']">新增</el-button>
</el-col>
<el-col :span="1.5" v-if="!isViewMode">
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['stgl:activityRegister:edit']">修改</el-button>
</el-col>
<el-col :span="1.5" v-if="!isViewMode">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['stgl:activityRegister:remove']">删除</el-button>
</el-col>
<el-col :span="1.5" v-if="!isViewMode">
<el-button type="success" icon="Download" @click="handleExport" v-hasPermi="['stgl:activityRegister:export']">导出</el-button>
</el-col>
<el-col :span="1.5" v-if="!isViewMode">
<el-button type="warning" icon="Upload" @click="handleImport" v-hasPermi="['stgl:activityRegister:import']">导入</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" v-if="!isViewMode" />
<el-table-column label="序号" type="index" width="55" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="approvalStatus" width="80">
<template #default="scope">
<dict-tag :options="stgl_approval_status" :value="scope.row.approvalStatus" v-if="scope.row.approvalStatus !== undefined" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" v-if="scope.row.gender" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="院系" align="center" prop="department" width="120" />
<el-table-column label="专业" align="center" prop="major" width="180" />
<el-table-column label="班级" align="center" prop="className" width="120" />
<el-table-column label="报名类型" align="center" prop="registrationType" width="100">
<template #default="scope">
<dict-tag :options="stgl_registration_type" :value="scope.row.registrationType" />
</template>
</el-table-column>
<el-table-column label="团队名称" align="center" prop="teamName" width="150" show-overflow-tooltip />
<el-table-column label="报名时间" align="center" prop="registrationTime" width="100">
<template #default="scope">
<span>{{ parseTime(scope.row.registrationTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="签到时间" align="center" width="120">
<template #default="scope">
<el-button
v-if="scope.row.latestSignInTime"
link
type="primary"
@click="showSignRecords(scope.row)"
class="time-button"
>
{{ formatSignTime(scope.row.latestSignInTime) }}
</el-button>
<span v-else class="text-gray-400">未签到</span>
</template>
</el-table-column>
<el-table-column label="签退时间" align="center" width="120">
<template #default="scope">
<el-button
v-if="scope.row.latestSignOutTime"
link
type="primary"
@click="showSignRecords(scope.row)"
class="time-button"
>
{{ formatSignTime(scope.row.latestSignOutTime) }}
</el-button>
<span v-else class="text-gray-400">未签退</span>
</template>
</el-table-column>
<el-table-column label="成绩" align="center" prop="score" width="80">
<template #default="scope">
<span>{{ scope.row.score || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['stgl:activityRegister:query']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top" v-if="!isViewMode">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['stgl:activityRegister:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="!isViewMode">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['stgl:activityRegister:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改活动报名对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body v-if="!isViewMode">
<el-form ref="memberFormRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="姓名" prop="selectedStudent">
<StudentSelector v-model="form.selectedStudent" placeholder="请选择学生" :showStudentNo="false" @change="handleStudentChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="学号" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="form.gender" placeholder="性别" disabled>
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"> </el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="院系" prop="department">
<el-input v-model="form.department" placeholder="院系" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="major">
<el-input v-model="form.major" placeholder="专业" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班级" prop="className">
<el-input v-model="form.className" placeholder="班级" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="联系方式" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报名类型" prop="registrationType">
<el-select v-model="form.registrationType" placeholder="请选择报名类型" style="width: 100%">
<el-option v-for="dict in stgl_registration_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="团队名称" prop="teamName" v-if="form.registrationType === '2' || form.registrationType === '3'">
<el-input v-model="form.teamName" placeholder="请输入团队名称" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="申请理由" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入申请理由" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看成员详情对话框 -->
<el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ viewForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="学号">{{ viewForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="sys_user_sex" :value="viewForm.gender" v-if="viewForm.gender" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ viewForm.phone || '-' }}</el-descriptions-item>
<el-descriptions-item label="院系">{{ viewForm.department || '-' }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ viewForm.major || '-' }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ viewForm.className || '-' }}</el-descriptions-item>
<el-descriptions-item label="报名类型">
<dict-tag :options="stgl_registration_type" :value="viewForm.registrationType" />
</el-descriptions-item>
<el-descriptions-item label="团队名称" v-if="viewForm.registrationType === '2' || viewForm.registrationType === '3'">
{{ viewForm.teamName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="报名时间">{{ parseTime(viewForm.registrationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="申请理由" :span="2">
<div class="view-description">{{ viewForm.remark || '无' }}</div>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="viewDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 签到记录弹窗 -->
<el-dialog v-model="signRecordsDialog.visible" :title="signRecordsDialog.title" width="800px" append-to-body>
<div v-loading="signRecordsLoading">
<el-table :data="signRecords" border stripe>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="签到时间" align="center" width="160">
<template #default="scope">
<span v-if="scope.row.signInTime">{{ formatSignTime(scope.row.signInTime) }}</span>
<span v-else class="text-gray-400">未签到</span>
</template>
</el-table-column>
<el-table-column label="签到地点" align="center" prop="signInLocation" width="150" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.signInLocation || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="签退时间" align="center" width="160">
<template #default="scope">
<span v-if="scope.row.signOutTime">{{ formatSignTime(scope.row.signOutTime) }}</span>
<span v-else class="text-gray-400">未签退</span>
</template>
</el-table-column>
<el-table-column label="签退地点" align="center" prop="signOutLocation" width="150" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.signOutLocation || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.overallStatus === 0" type="info">未签到</el-tag>
<el-tag v-else-if="scope.row.overallStatus === 1" type="success">已签到</el-tag>
<el-tag v-else-if="scope.row.overallStatus === 2" type="warning">已签退</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="signRecordsDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body v-if="!isViewMode">
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" />
是否更新已经存在的报名数据
</div>
<span>仅允许导入xls、xlsx格式文件。</span>
<el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" @click="importTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue';
import {
listActivityRegister,
getActivityRegister,
delActivityRegister,
addActivityRegister,
updateActivityRegister,
exportActivityRegister
} from '@/api/stgl/activityregister';
import { ActivityRegisterVO, ActivityRegisterForm, ActivityRegisterQuery } from '@/api/stgl/activityregister/types';
import { StudentVO } from '@/api/xg/student/types';
import StudentSelector from '@/components/StudentSelector/index.vue';
import { parseTime } from '@/utils/ruoyi';
import { globalHeaders } from '@/utils/request';
import { ArrowDown, UploadFilled } from '@element-plus/icons-vue';
import type { UploadInstance } from 'element-plus';
import request from '@/utils/request';
interface Props {
activityId: string | number;
isViewMode?: boolean;
}
// 扩展ActivityRegisterForm以包含选择的学生对象
interface ExtendedActivityRegisterForm extends ActivityRegisterForm {
selectedStudent?: StudentVO | null;
}
// 扩展ActivityRegisterVO以包含签到信息
interface ExtendedActivityRegisterVO extends ActivityRegisterVO {
latestSignInTime?: string;
latestSignOutTime?: string;
}
// 签到记录类型
interface SignRecord {
id: string | number;
signInTime?: string;
signOutTime?: string;
signInLocation?: string;
signOutLocation?: string;
overallStatus: number;
}
const props = defineProps<Props>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex, stgl_registration_type, stgl_approval_status } = toRefs<any>(
proxy?.useDict('sys_user_sex', 'stgl_registration_type', 'stgl_approval_status')
);
const memberList = ref<ExtendedActivityRegisterVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const memberFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const signRecordsDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewForm = reactive<ActivityRegisterVO>({
id: undefined,
activityId: undefined,
studentId: undefined,
studentName: '',
studentNo: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
registrationType: undefined,
teamName: '',
approvalStatus: undefined,
registrationTime: '',
approvalTime: '',
remark: '',
score: '',
scoreTime: ''
});
const initFormData: ExtendedActivityRegisterForm = {
id: undefined,
activityId: props.activityId,
studentId: undefined,
selectedStudent: null,
studentName: '',
studentNo: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
registrationType: '1',
teamName: '',
approvalStatus: 1,
registrationTime: undefined,
approvalTime: undefined,
remark: '',
score: '',
scoreTime: undefined
};
const data = reactive<PageData<ExtendedActivityRegisterForm, ActivityRegisterQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
activityId: props.activityId,
approvalStatus: undefined,
studentName: undefined,
studentNo: undefined,
phone: undefined
},
rules: {
selectedStudent: [{ required: true, message: '请选择学生', trigger: 'change' }],
registrationType: [{ required: true, message: '请选择报名类型', trigger: 'change' }],
teamName: [{ required: true, message: '请输入团队名称', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
// 签到记录相关
const signRecords = ref<SignRecord[]>([]);
const signRecordsLoading = ref(false);
// 上传相关
const uploadRef = ref<UploadInstance>();
const upload = reactive<{
open: boolean;
title: string;
isUploading: boolean;
updateSupport: boolean;
url: string;
headers: { Authorization: string };
}>({
open: false,
title: '',
isUploading: false,
updateSupport: false,
url: import.meta.env.VITE_APP_BASE_API + '/stgl/activity/register/importData',
headers: globalHeaders()
});
/** 查询成员列表 */
const getList = async () => {
loading.value = true;
try {
// 构建查询参数,查询已审核通过的报名记录
const queryData = { ...queryParams.value };
// 如果状态字段为空或未选择,则查询已通过(approvalStatus=1)的报名记录
if (!queryData.approvalStatus) {
queryData.approvalStatus = 1;
}
console.log('ActivityRegistrationView: getList 开始查询,参数:', queryData);
const res = await listActivityRegister(queryData);
console.log('ActivityRegistrationView: 查询结果:', res);
// 为每个成员添加签到信息
const membersWithSignInfo = await Promise.all(
res.rows.map(async (member) => {
try {
// 查询该用户该活动的最新签到记录
const signRes = await request({
url: '/stgl/activityAttendance/getAllByActivityAndUser',
method: 'get',
params: {
activityId: props.activityId,
studentNo: member.studentNo
}
});
if (signRes.code === 200 && signRes.data && signRes.data.length > 0) {
// 获取最新的签到和签退时间
const latestSignIn = signRes.data
.filter((record: any) => record.signInTime)
.sort((a: any, b: any) => new Date(b.signInTime).getTime() - new Date(a.signInTime).getTime())[0];
const latestSignOut = signRes.data
.filter((record: any) => record.signOutTime)
.sort((a: any, b: any) => new Date(b.signOutTime).getTime() - new Date(a.signOutTime).getTime())[0];
return {
...member,
latestSignInTime: latestSignIn?.signInTime,
latestSignOutTime: latestSignOut?.signOutTime
};
}
} catch (error) {
console.warn('获取签到信息失败:', error);
}
return {
...member,
latestSignInTime: undefined,
latestSignOutTime: undefined
};
})
);
memberList.value = membersWithSignInfo;
total.value = res.total;
} catch (error) {
console.error('ActivityRegistrationView: 查询成员列表失败:', error);
proxy?.$modal.msgError('查询成员列表失败');
} finally {
loading.value = false;
}
};
/** 显示签到记录 */
const showSignRecords = async (member: ExtendedActivityRegisterVO) => {
signRecordsDialog.visible = true;
signRecordsDialog.title = `${member.studentName}(${member.studentNo})的签到记录`;
signRecordsLoading.value = true;
try {
const res = await request({
url: '/stgl/activityAttendance/getAllByActivityAndUser',
method: 'get',
params: {
activityId: props.activityId,
studentNo: member.studentNo
}
});
if (res.code === 200) {
signRecords.value = res.data || [];
} else {
signRecords.value = [];
proxy?.$modal.msgError(res.msg || '获取签到记录失败');
}
} catch (error) {
console.error('获取签到记录失败:', error);
signRecords.value = [];
proxy?.$modal.msgError('获取签到记录失败');
} finally {
signRecordsLoading.value = false;
}
};
/** 格式化签到时间 */
const formatSignTime = (timeStr: string): string => {
if (!timeStr) return '';
const date = new Date(timeStr);
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData, activityId: props.activityId };
memberFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
activityId: props.activityId,
approvalStatus: 1,
studentName: undefined,
studentNo: undefined,
phone: undefined
};
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: ExtendedActivityRegisterVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加活动报名';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: ExtendedActivityRegisterVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getActivityRegister(_id);
Object.assign(form.value, res.data);
// 设置学生选择器数据
if (res.data.studentName) {
form.value.selectedStudent = {
id: res.data.studentId || 0,
studentNo: res.data.studentNo || '',
name: res.data.studentName,
gender: res.data.gender || '0',
phone: res.data.phone || '',
collegeName: res.data.department || '',
majorName: res.data.major || '',
className: res.data.className || ''
} as StudentVO;
}
dialog.visible = true;
dialog.title = '修改活动报名';
};
/** 提交按钮 */
const submitForm = () => {
memberFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
// 创建提交数据,排除selectedStudent字段
const { selectedStudent, ...submitData } = form.value;
// 确保包含必要字段
submitData.activityId = props.activityId;
submitData.approvalStatus = 1; // 新增的报名默认为已通过状态
try {
if (form.value.id) {
await updateActivityRegister(submitData as ActivityRegisterForm);
} else {
await addActivityRegister(submitData as ActivityRegisterForm);
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
} catch (error) {
console.error('提交失败:', error);
} finally {
buttonLoading.value = false;
}
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: ExtendedActivityRegisterVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除选中的活动报名记录?');
try {
if (Array.isArray(_ids)) {
await delActivityRegister(_ids.join(','));
} else {
await delActivityRegister(_ids);
}
proxy?.$modal.msgSuccess('删除成功');
await getList();
} catch (error) {
console.error('删除失败:', error);
}
};
/** 查看按钮操作 */
const handleView = async (row: ExtendedActivityRegisterVO) => {
const res = await getActivityRegister(row.id);
Object.assign(viewForm, res.data);
viewDialog.visible = true;
viewDialog.title = '查看活动报名详情';
};
/** 学生选择变化处理 */
const handleStudentChange = (student: StudentVO | null) => {
if (student) {
form.value.studentId = student.id;
form.value.studentNo = student.studentNo;
form.value.studentName = student.name;
form.value.gender = student.gender;
form.value.department = student.collegeName;
form.value.major = student.majorName;
form.value.className = student.className;
form.value.phone = student.phone;
} else {
form.value.studentId = undefined;
form.value.studentNo = '';
form.value.studentName = '';
form.value.gender = '';
form.value.department = '';
form.value.major = '';
form.value.className = '';
form.value.phone = '';
}
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'stgl/activity/register/export',
{
...queryParams.value
},
`activity_register_${new Date().getTime()}.xlsx`
);
};
/** 导入模板下载 */
const importTemplate = () => {
proxy?.download('stgl/activity/register/importTemplate', {}, `活动报名导入模板_${new Date().getTime()}.xlsx`);
};
/** 导入按钮操作 */
const handleImport = () => {
upload.title = '活动报名导入';
upload.open = true;
};
/** 提交上传文件 */
const submitFileForm = () => {
uploadRef.value?.submit();
};
/** 文件上传中处理 */
const handleFileUploadProgress = () => {
upload.isUploading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = (response: any) => {
upload.open = false;
upload.isUploading = false;
uploadRef.value?.clearFiles();
proxy?.$modal.msgSuccess(response.msg);
if (response.msg.includes('成功')) {
getList();
}
};
// 暴露给父组件的方法
defineExpose({
getList
});
onMounted(() => {
getList();
});
</script>
<style scoped>
.activity-registration-view {
padding: 0;
}
.view-description {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 80px;
font-size: 14px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.time-button {
padding: 0;
height: auto;
font-size: 14px;
color: #409eff;
text-decoration: underline;
}
.time-button:hover {
color: #66b1ff;
}
.text-gray-400 {
color: #909399;
}
</style>
@@ -0,0 +1,469 @@
<template>
<div class="pending-members-tab">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="always" style="padding: 8px 16px;">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" style="padding: 0;">
<el-row :gutter="16" align="middle">
<el-col :span="5">
<el-form-item label="姓名" prop="studentName" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系方式" prop="phone" style="margin-bottom: 0;">
<el-input v-model="queryParams.phone" placeholder="请输入联系方式" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="success" plain icon="Check" :disabled="single" @click="handleApprove()" v-hasPermi="['stgl:activityRegister:edit']">批量通过</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Close" :disabled="single" @click="handleReject()" v-hasPermi="['stgl:activityRegister:edit']">批量拒绝</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['stgl:activityRegister:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown class="mt-[1px]">
<el-button>
更多
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Download" @click="handleExport" v-hasPermi="['stgl:activityRegister:export']">导出数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="approvalStatus" width="80">
<template #default="scope">
<dict-tag :options="stgl_approval_status" :value="scope.row.approvalStatus" v-if="scope.row.approvalStatus !== undefined" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" v-if="scope.row.gender" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="院系" align="center" prop="department" width="120" />
<el-table-column label="专业" align="center" prop="major" width="180" />
<el-table-column label="班级" align="center" prop="className" width="120" />
<el-table-column label="报名类型" align="center" prop="registrationType" width="100">
<template #default="scope">
<dict-tag :options="stgl_registration_type" :value="scope.row.registrationType" />
</template>
</el-table-column>
<el-table-column label="团队名称" align="center" prop="teamName" width="150" show-overflow-tooltip />
<el-table-column label="报名时间" align="center" prop="registrationTime" width="100">
<template #default="scope">
<span>{{ parseTime(scope.row.registrationTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="申请理由" align="center" prop="remark" width="200" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['stgl:activityRegister:query']"></el-button>
</el-tooltip>
<el-tooltip content="通过" placement="top">
<el-button link type="success" icon="Check" @click="handleApprove(scope.row)" v-hasPermi="['stgl:activityRegister:edit']"></el-button>
</el-tooltip>
<el-tooltip content="拒绝" placement="top">
<el-button link type="danger" icon="Close" @click="handleReject(scope.row)" v-hasPermi="['stgl:activityRegister:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['stgl:activityRegister:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 查看成员详情对话框 -->
<el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ viewForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="sys_user_sex" :value="viewForm.gender" />
</el-descriptions-item>
<el-descriptions-item label="院系">{{ viewForm.department }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ viewForm.major }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ viewForm.className }}</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ viewForm.phone }}</el-descriptions-item>
<el-descriptions-item label="报名类型">
<dict-tag :options="stgl_registration_type" :value="viewForm.registrationType" />
</el-descriptions-item>
<el-descriptions-item label="团队名称">{{ viewForm.teamName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请理由" :span="2">{{ viewForm.remark || '-' }}</el-descriptions-item>
<el-descriptions-item label="报名时间">{{ parseTime(viewForm.registrationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 审核操作对话框 -->
<el-dialog :title="approvalDialog.title" v-model="approvalDialog.visible" width="500px" append-to-body>
<el-form ref="approvalFormRef" :model="approvalForm" :rules="approvalRules" label-width="80px">
<el-form-item label="审核结果" prop="approvalStatus">
<el-radio-group v-model="approvalForm.approvalStatus">
<el-radio :label="1">通过</el-radio>
<el-radio :label="2">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见" prop="approvalRemark">
<el-input
v-model="approvalForm.approvalRemark"
type="textarea"
:rows="4"
:placeholder="approvalForm.approvalStatus === 1 ? '请输入通过意见(可选)' : '请输入拒绝原因(必填)'"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitApproval">确 定</el-button>
<el-button @click="cancelApproval">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue';
import {
listActivityRegister,
getActivityRegister,
delActivityRegister,
updateActivityRegister,
exportActivityRegister
} from '@/api/stgl/activityregister';
import { StglActivityRegistrationVo, StglActivityRegistrationForm, StglActivityRegistrationQuery } from '@/api/stgl/activityregister/types';
import { parseTime } from '@/utils/ruoyi';
import { ArrowDown } from '@element-plus/icons-vue';
interface Props {
activityId: string | number;
activityName: string;
}
const props = defineProps<Props>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex, stgl_registration_type, stgl_approval_status } = toRefs<any>(
proxy?.useDict('sys_user_sex', 'stgl_registration_type', 'stgl_approval_status')
);
const memberList = ref<StglActivityRegistrationVo[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const approvalFormRef = ref<ElFormInstance>();
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const approvalDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewForm = reactive<StglActivityRegistrationVo>({
id: undefined,
activityId: undefined,
studentId: undefined,
studentName: '',
studentNo: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
registrationType: undefined,
teamName: '',
approvalStatus: undefined,
registrationTime: '',
approvalTime: '',
remark: '',
score: '',
scoreTime: ''
});
const approvalForm = reactive({
ids: [] as Array<string | number>,
approvalStatus: 1,
approvalRemark: ''
});
const approvalRules = reactive({
approvalStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
approvalRemark: [
{
required: true,
message: '请输入拒绝原因',
trigger: 'blur',
validator: (rule: any, value: any, callback: any) => {
if (approvalForm.approvalStatus === 2 && !value) {
callback(new Error('拒绝时必须填写拒绝原因'));
} else {
callback();
}
}
}
]
});
const data = reactive<PageData<StglActivityRegistrationForm, StglActivityRegistrationQuery>>({
queryParams: {
pageNum: 1,
pageSize: 10,
activityId: props.activityId,
approvalStatus: 0, // 待审核状态
studentName: undefined,
studentNo: undefined,
phone: undefined
}
});
const { queryParams } = toRefs(data);
/** 查询成员列表 */
const getList = async () => {
loading.value = true;
try {
// 构建查询参数,查询待审核的报名记录
const queryData = { ...queryParams.value };
// 确保查询待审核状态(approvalStatus=0)
queryData.approvalStatus = 0;
console.log('PendingMembersTab: getList 开始查询,参数:', queryData);
const res = await listActivityRegister(queryData);
console.log('PendingMembersTab: 查询结果:', res);
memberList.value = res.rows;
total.value = res.total;
} catch (error) {
console.error('PendingMembersTab: 查询成员列表失败:', error);
proxy?.$modal.msgError('查询成员列表失败');
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
activityId: props.activityId,
approvalStatus: 0,
studentName: undefined,
studentNo: undefined,
phone: undefined
};
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: ActivityRegisterVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 批量通过操作 */
const handleApprove = (row?: StglActivityRegistrationVo) => {
const _ids = row?.id ? [row.id] : ids.value;
if (_ids.length === 0) {
proxy?.$modal.msgError('请选择要审核的记录');
return;
}
approvalForm.ids = _ids;
approvalForm.approvalStatus = 1;
approvalForm.approvalRemark = '';
approvalDialog.visible = true;
approvalDialog.title = '批量通过审核';
};
/** 批量拒绝操作 */
const handleReject = (row?: StglActivityRegistrationVo) => {
const _ids = row?.id ? [row.id] : ids.value;
if (_ids.length === 0) {
proxy?.$modal.msgError('请选择要审核的记录');
return;
}
approvalForm.ids = _ids;
approvalForm.approvalStatus = 2;
approvalForm.approvalRemark = '';
approvalDialog.visible = true;
approvalDialog.title = '批量拒绝审核';
};
/** 提交审核 */
const submitApproval = () => {
approvalFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
// 批量更新审核状态
for (const id of approvalForm.ids) {
await updateActivityRegister({
id,
approvalStatus: approvalForm.approvalStatus,
approvalRemark: approvalForm.approvalRemark,
approvalTime: new Date()
} as StglActivityRegistrationForm);
}
const actionText = approvalForm.approvalStatus === 1 ? '通过' : '拒绝';
proxy?.$modal.msgSuccess(`批量${actionText}成功`);
approvalDialog.visible = false;
await getList();
} catch (error) {
console.error('审核操作失败:', error);
proxy?.$modal.msgError('审核操作失败');
} finally {
buttonLoading.value = false;
}
}
});
};
/** 取消审核对话框 */
const cancelApproval = () => {
approvalDialog.visible = false;
};
/** 删除按钮操作 */
const handleDelete = async (row?: StglActivityRegistrationVo) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除选中的活动报名记录?');
try {
if (Array.isArray(_ids)) {
await delActivityRegister(_ids.join(','));
} else {
await delActivityRegister(_ids);
}
proxy?.$modal.msgSuccess('删除成功');
await getList();
} catch (error) {
console.error('删除失败:', error);
}
};
/** 查看按钮操作 */
const handleView = async (row: StglActivityRegistrationVo) => {
const res = await getActivityRegister(row.id);
Object.assign(viewForm, res.data);
viewDialog.visible = true;
viewDialog.title = '查看活动报名详情';
};
/** 取消查看对话框 */
const cancelView = () => {
viewDialog.visible = false;
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'stgl/activity/register/export',
{
...queryParams.value
},
`pending_activity_register_${new Date().getTime()}.xlsx`
);
};
// 暴露给父组件的方法
defineExpose({
getList
});
onMounted(() => {
getList();
});
</script>
<style scoped>
.view-description {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 80px;
font-size: 14px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>
@@ -0,0 +1,634 @@
<template>
<div class="registered-members-tab">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="always" style="padding: 8px 16px;">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" style="padding: 0;">
<el-row :gutter="16" align="middle">
<el-col :span="5">
<el-form-item label="姓名" prop="studentName" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系方式" prop="phone" style="margin-bottom: 0;">
<el-input v-model="queryParams.phone" placeholder="请输入联系方式" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['stgl:activityRegister:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['stgl:activityRegister:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['stgl:activityRegister:remove']">删除</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="approvalStatus" width="80">
<template #default="scope">
<dict-tag :options="stgl_approval_status" :value="scope.row.approvalStatus" v-if="scope.row.approvalStatus !== undefined" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" v-if="scope.row.gender" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="院系" align="center" prop="department" width="120" />
<el-table-column label="专业" align="center" prop="major" width="180" />
<el-table-column label="班级" align="center" prop="className" width="120" />
<el-table-column label="报名类型" align="center" prop="registrationType" width="100">
<template #default="scope">
<dict-tag :options="stgl_registration_type" :value="scope.row.registrationType" />
</template>
</el-table-column>
<el-table-column label="团队名称" align="center" prop="teamName" width="150" show-overflow-tooltip />
<el-table-column label="报名时间" align="center" prop="registrationTime" width="100">
<template #default="scope">
<span>{{ parseTime(scope.row.registrationTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="成绩" align="center" prop="score" width="80">
<template #default="scope">
<span>{{ scope.row.score || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['stgl:activityRegister:query']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['stgl:activityRegister:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['stgl:activityRegister:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改活动报名对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-form ref="memberFormRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="姓名" prop="selectedStudent">
<StudentSelector v-model="form.selectedStudent" placeholder="请选择学生" :showStudentNo="false" @change="handleStudentChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="学号" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="form.gender" placeholder="性别" disabled>
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"> </el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="院系" prop="department">
<el-input v-model="form.department" placeholder="院系" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="major">
<el-input v-model="form.major" placeholder="专业" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班级" prop="className">
<el-input v-model="form.className" placeholder="班级" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="联系方式" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报名类型" prop="registrationType">
<el-select v-model="form.registrationType" placeholder="请选择报名类型" style="width: 100%">
<el-option v-for="dict in stgl_registration_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="团队名称" prop="teamName" v-if="form.registrationType === '2' || form.registrationType === '3'">
<el-input v-model="form.teamName" placeholder="请输入团队名称" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="申请理由" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入申请理由" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看成员详情对话框 -->
<el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ viewForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="sys_user_sex" :value="viewForm.gender" />
</el-descriptions-item>
<el-descriptions-item label="院系">{{ viewForm.department }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ viewForm.major }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ viewForm.className }}</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ viewForm.phone }}</el-descriptions-item>
<el-descriptions-item label="报名类型">
<dict-tag :options="stgl_registration_type" :value="viewForm.registrationType" />
</el-descriptions-item>
<el-descriptions-item label="团队名称">{{ viewForm.teamName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请理由">{{ viewForm.remark || '-' }}</el-descriptions-item>
<el-descriptions-item label="报名时间">{{ parseTime(viewForm.registrationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="成绩">{{ viewForm.score || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" />
是否更新已经存在的报名数据
</div>
<span>仅允许导入xls、xlsx格式文件。</span>
<el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" @click="importTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue';
import {
listActivityRegister,
getActivityRegister,
delActivityRegister,
addActivityRegister,
updateActivityRegister,
exportActivityRegister
} from '@/api/stgl/activityregister';
import { ActivityRegisterVO, ActivityRegisterForm, ActivityRegisterQuery } from '@/api/stgl/activityregister/types';
import { StudentVO } from '@/api/xg/student/types';
import StudentSelector from '@/components/StudentSelector/index.vue';
import { parseTime } from '@/utils/ruoyi';
import { globalHeaders } from '@/utils/request';
import { ArrowDown, UploadFilled } from '@element-plus/icons-vue';
import type { UploadInstance } from 'element-plus';
interface Props {
activityId: string | number;
activityName: string;
}
// 扩展ActivityRegisterForm以包含选择的学生对象
interface ExtendedActivityRegisterForm extends ActivityRegisterForm {
selectedStudent?: StudentVO | null;
}
const props = defineProps<Props>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex, stgl_registration_type, stgl_approval_status } = toRefs<any>(
proxy?.useDict('sys_user_sex', 'stgl_registration_type', 'stgl_approval_status')
);
const memberList = ref<ActivityRegisterVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const memberFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewForm = reactive<ActivityRegisterVO>({
id: undefined,
activityId: undefined,
studentId: undefined,
studentName: '',
studentNo: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
registrationType: undefined,
teamName: '',
approvalStatus: undefined,
registrationTime: '',
approvalTime: '',
remark: '',
score: '',
scoreTime: ''
});
const initFormData: ExtendedActivityRegisterForm = {
id: undefined,
activityId: props.activityId,
studentId: undefined,
selectedStudent: null,
studentName: '',
studentNo: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
registrationType: '1',
teamName: '',
approvalStatus: 1,
registrationTime: undefined,
approvalTime: undefined,
remark: '',
score: '',
scoreTime: undefined
};
const data = reactive<PageData<ExtendedActivityRegisterForm, ActivityRegisterQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
activityId: props.activityId,
approvalStatus: undefined,
studentName: undefined,
studentNo: undefined,
phone: undefined
},
rules: {
selectedStudent: [{ required: true, message: '请选择学生', trigger: 'change' }],
registrationType: [{ required: true, message: '请选择报名类型', trigger: 'change' }],
teamName: [{ required: true, message: '请输入团队名称', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
// 上传相关
const uploadRef = ref<UploadInstance>();
const upload = reactive<{
open: boolean;
title: string;
isUploading: boolean;
updateSupport: boolean;
url: string;
headers: { Authorization: string };
}>({
open: false,
title: '',
isUploading: false,
updateSupport: false,
url: import.meta.env.VITE_APP_BASE_API + '/stgl/activity/register/importData',
headers: globalHeaders()
});
/** 查询成员列表 */
const getList = async () => {
loading.value = true;
try {
// 构建查询参数,查询已审核通过的报名记录
const queryData = { ...queryParams.value };
// 如果状态字段为空或未选择,则查询已通过(approvalStatus=1)的报名记录
if (!queryData.approvalStatus) {
queryData.approvalStatus = 1;
}
console.log('RegisteredMembersTab: getList 开始查询,参数:', queryData);
const res = await listActivityRegister(queryData);
console.log('RegisteredMembersTab: 查询结果:', res);
memberList.value = res.rows;
total.value = res.total;
} catch (error) {
console.error('RegisteredMembersTab: 查询成员列表失败:', error);
proxy?.$modal.msgError('查询成员列表失败');
} finally {
loading.value = false;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData, activityId: props.activityId };
memberFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
activityId: props.activityId,
approvalStatus: 1,
studentName: undefined,
studentNo: undefined,
phone: undefined
};
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: ActivityRegisterVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = '添加活动报名';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: ActivityRegisterVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getActivityRegister(_id);
Object.assign(form.value, res.data);
// 设置学生选择器数据
if (res.data.studentName) {
form.value.selectedStudent = {
id: res.data.studentId || 0,
studentNo: res.data.studentNo || '',
name: res.data.studentName,
gender: res.data.gender || '0',
phone: res.data.phone || '',
collegeName: res.data.department || '',
majorName: res.data.major || '',
className: res.data.className || ''
} as StudentVO;
}
dialog.visible = true;
dialog.title = '修改活动报名';
};
/** 提交按钮 */
const submitForm = () => {
memberFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
// 创建提交数据,排除selectedStudent字段
const { selectedStudent, ...submitData } = form.value;
// 确保包含必要字段
submitData.activityId = props.activityId;
submitData.approvalStatus = 1; // 新增的报名默认为已通过状态
try {
if (form.value.id) {
await updateActivityRegister(submitData as ActivityRegisterForm);
} else {
await addActivityRegister(submitData as ActivityRegisterForm);
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
} catch (error) {
console.error('提交失败:', error);
} finally {
buttonLoading.value = false;
}
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: ActivityRegisterVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除选中的活动报名记录?');
try {
if (Array.isArray(_ids)) {
await delActivityRegister(_ids.join(','));
} else {
await delActivityRegister(_ids);
}
proxy?.$modal.msgSuccess('删除成功');
await getList();
} catch (error) {
console.error('删除失败:', error);
}
};
/** 查看按钮操作 */
const handleView = async (row: ActivityRegisterVO) => {
const res = await getActivityRegister(row.id);
Object.assign(viewForm, res.data);
viewDialog.visible = true;
viewDialog.title = '查看活动报名详情';
};
/** 取消查看对话框 */
const cancelView = () => {
viewDialog.visible = false;
};
/** 学生选择变化处理 */
const handleStudentChange = (student: StudentVO | null) => {
if (student) {
form.value.studentId = student.id;
form.value.studentNo = student.studentNo;
form.value.studentName = student.name;
form.value.gender = student.gender;
form.value.department = student.collegeName;
form.value.major = student.majorName;
form.value.className = student.className;
form.value.phone = student.phone;
} else {
form.value.studentId = undefined;
form.value.studentNo = '';
form.value.studentName = '';
form.value.gender = '';
form.value.department = '';
form.value.major = '';
form.value.className = '';
form.value.phone = '';
}
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'stgl/activity/register/export',
{
...queryParams.value
},
`activity_register_${new Date().getTime()}.xlsx`
);
};
/** 导入模板下载 */
const importTemplate = () => {
proxy?.download('stgl/activity/register/importTemplate', {}, `活动报名导入模板_${new Date().getTime()}.xlsx`);
};
/** 导入按钮操作 */
const handleImport = () => {
upload.title = '活动报名导入';
upload.open = true;
};
/** 提交上传文件 */
const submitFileForm = () => {
uploadRef.value?.submit();
};
/** 文件上传中处理 */
const handleFileUploadProgress = () => {
upload.isUploading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = (response: any) => {
upload.open = false;
upload.isUploading = false;
uploadRef.value?.clearFiles();
proxy?.$modal.msgSuccess(response.msg);
if (response.msg.includes('成功')) {
getList();
}
};
// 暴露给父组件的方法
defineExpose({
getList
});
onMounted(() => {
getList();
});
</script>
<style scoped>
.view-description {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 80px;
font-size: 14px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>
@@ -0,0 +1,835 @@
<template>
<div class="joined-members-tab">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="always" style="padding: 8px 16px">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" style="padding: 0">
<el-row :gutter="16" align="middle">
<el-col :span="5">
<el-form-item label="姓名" prop="studentName" style="margin-bottom: 0">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo" style="margin-bottom: 0">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系方式" prop="phone" style="margin-bottom: 0">
<el-input v-model="queryParams.params.phone" placeholder="请输入联系方式" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="margin-bottom: 0">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-tooltip :content="getAddButtonTooltip" placement="top" :disabled="!isAddButtonDisabled">
<el-button type="primary" plain icon="Plus" @click="handleAdd" :disabled="isAddButtonDisabled" v-hasPermi="['stgl:clubmember:add']">
新增
</el-button>
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['stgl:clubmember:edit']"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['stgl:clubmember:remove']"
>删除</el-button
>
</el-col>
<el-col :span="1.5">
<el-dropdown class="mt-[1px]">
<el-button>
更多
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
<el-dropdown-item icon="Top" @click="handleImport" v-hasPermi="['stgl:clubmember:import']">导入数据</el-dropdown-item>
<el-dropdown-item icon="Download" @click="handleExport" v-hasPermi="['stgl:clubmember:export']">导出数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :options="stgl_member_status" :value="scope.row.status" v-if="scope.row.status" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" v-if="scope.row.gender" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="院系" align="center" prop="department" width="120" />
<el-table-column label="专业" align="center" prop="major" width="180" />
<el-table-column label="班级" align="center" prop="className" width="120" />
<el-table-column label="成员角色" align="center" prop="roleType" width="100">
<template #default="scope">
<span v-if="scope.row.roleType">{{ scope.row.roleType }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="加入时间" align="center" prop="joinTime" width="100">
<template #default="scope">
<span>{{ scope.row.joinTime ? parseTime(scope.row.joinTime, '{y}-{m}-{d}') : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="info"
icon="View"
size="default"
style="font-size: 16px; margin-right: 8px; color: blue"
@click="handleView(scope.row)"
v-hasPermi="['stgl:clubmember:query']"
></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button
link
type="primary"
icon="Edit"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleUpdate(scope.row)"
v-hasPermi="['stgl:clubmember:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
link
type="danger"
icon="Delete"
size="default"
style="font-size: 16px"
@click="handleDelete(scope.row)"
v-hasPermi="['stgl:clubmember:remove']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改成员对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
<el-form ref="memberFormRef" :model="form" :rules="rules" label-width="80px">
<el-row>
<el-col :span="12">
<el-form-item label="姓名" prop="selectedStudent">
<StudentSelector v-model="form.selectedStudent" placeholder="请选择学生" :showStudentNo="false" @change="handleStudentChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="学号" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="form.gender" placeholder="性别" disabled>
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"> </el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="院系" prop="department">
<el-input v-model="form.department" placeholder="院系" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="major">
<el-input v-model="form.major" placeholder="专业" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班级" prop="className">
<el-input v-model="form.className" placeholder="班级" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="联系方式" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成员角色" prop="roleType">
<el-input v-model="form.roleType" placeholder="请输入成员角色" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="加入时间" prop="joinTime">
<el-date-picker clearable v-model="form.joinTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择加入时间"> </el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看成员详情对话框 -->
<el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ viewForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="sys_user_sex" :value="viewForm.gender" />
</el-descriptions-item>
<el-descriptions-item label="院系">{{ viewForm.department }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ viewForm.major }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ viewForm.className }}</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ viewForm.phone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="成员角色">
{{ viewForm.roleType || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :options="stgl_member_status" :value="viewForm.status" />
</el-descriptions-item>
<el-descriptions-item label="加入时间">{{ parseTime(viewForm.joinTime, '{y}-{m}-{d}') }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(viewForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">备注信息</el-divider>
<div class="view-description">
{{ viewForm.remark || '暂无备注' }}
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="getImportUrl()"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-error="handleFileError"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload">
<i-ep-upload-filled />
</el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="text-center el-upload__tip">
<div class="el-upload__tip" style="color: red">仅支持新增成员数据,不更新已存在的成员</div>
<div class="el-upload__tip" style="color: #409eff; margin-top: 5px">
<el-icon style="margin-right: 4px"><InfoFilled /></el-icon>
请按照模板格式填写:社团名称、学号、姓名、加入时间
</div>
<span>仅允许导入xls、xlsx格式文件。</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, toRefs, onMounted, getCurrentInstance, nextTick, watch, computed } from 'vue';
import { ArrowDown, User } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listStglMember, getStglMember, delStglMember, addStglMember, updateStglMember } from '@/api/stgl/member';
import { getUser } from '@/api/system/user';
import { StglMemberVO, StglMemberQuery, StglMemberForm } from '@/api/stgl/member/types';
import { StudentVO } from '@/api/xg/student/types';
import StudentSelector from '@/components/StudentSelector/index.vue';
import { parseTime } from '@/utils/ruoyi';
import { globalHeaders } from '@/utils/request';
import type { UploadInstance } from 'element-plus';
interface Props {
clubId: string | number;
clubName: string;
memberLimit?: number;
currentMemberCount?: number;
}
const props = defineProps<Props>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex, stgl_member_status } = toRefs<any>(proxy?.useDict('sys_user_sex', 'stgl_member_status'));
const memberList = ref<StglMemberVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const memberFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewForm = reactive<StglMemberVO>({
id: undefined,
memberType: 1,
relationId: undefined,
clubName: '',
userId: undefined,
studentNo: '',
studentName: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
email: '',
roleType: '普通成员',
roleName: '',
joinTime: '',
status: '2',
remark: '',
createTime: ''
});
const initFormData: StglMemberForm = {
id: undefined,
memberType: 1,
relationId: props.clubId,
userId: undefined,
selectedStudent: null,
studentNo: undefined,
studentName: undefined,
gender: undefined,
department: undefined,
major: undefined,
className: undefined,
phone: undefined,
email: undefined,
roleType: '普通成员',
roleName: undefined,
joinTime: undefined,
remark: undefined
};
const data = reactive<PageData<StglMemberForm, StglMemberQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
memberType: 1,
relationId: props.clubId,
status: undefined, // 初始为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
params: {
phone: undefined
}
},
rules: {
selectedStudent: [{ required: true, message: '请选择学生', trigger: 'change' }],
studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }],
gender: [{ required: true, message: '性别不能为空', trigger: 'change' }],
department: [{ required: true, message: '院系不能为空', trigger: 'blur' }],
major: [{ required: true, message: '专业不能为空', trigger: 'blur' }],
className: [{ required: true, message: '班级不能为空', trigger: 'blur' }],
phone: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
roleType: [{ required: true, message: '成员角色不能为空', trigger: 'blur' }],
joinTime: [{ required: true, message: '加入时间不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
// 计算属性:判断新增按钮是否应该被禁用
const isAddButtonDisabled = computed(() => {
// 使用本地成员数量,确保实时更新
const currentCount = memberList.value.length;
if (!props.memberLimit || props.memberLimit <= 0) {
return false; // 没有设置上限,允许新增
}
return currentCount >= props.memberLimit;
});
// 计算属性:新增按钮的提示信息
const getAddButtonTooltip = computed(() => {
const currentCount = memberList.value.length;
if (!props.memberLimit || props.memberLimit <= 0) {
return '该社团未设置成员上限';
}
return `当前成员:${currentCount}/${props.memberLimit}${currentCount >= props.memberLimit ? ',已达到上限' : ''}`;
});
// 计算属性:成员数量标签的类型
const getMemberCountTagType = computed(() => {
const currentCount = memberList.value.length;
if (!props.memberLimit || props.memberLimit <= 0) {
return 'info'; // 无上限时显示信息标签
}
if (currentCount >= props.memberLimit) {
return 'danger'; // 达到上限时显示危险标签
}
if (currentCount >= props.memberLimit * 0.8) {
return 'warning'; // 接近上限时显示警告标签
}
return 'success'; // 正常范围内显示成功标签
});
// 导入相关
const uploadRef = ref<UploadInstance>();
const upload = reactive<{
open: boolean;
title: string;
isUploading: boolean;
url: string;
headers: { Authorization: string };
}>({
open: false,
title: '',
isUploading: false,
url: import.meta.env.VITE_APP_BASE_API + '/stgl/member/importData',
headers: globalHeaders()
});
/** 查询成员列表 */
const getList = async () => {
loading.value = true;
try {
// 构建查询参数,支持多状态查询
const queryData = { ...queryParams.value };
console.log('JoinedMembersTab: getList 开始查询,参数:', queryData);
// 如果状态字段为空或未选择,则查询已加入(status=2)和已退出(status=3)的成员
if (!queryData.status) {
// 使用params传递多状态查询
queryData.params = { ...queryData.params, statusList: ['2', '3'] } as any;
delete queryData.status; // 删除单个status字段
}
console.log('JoinedMembersTab: 最终查询参数:', queryData);
const res = await listStglMember(queryData);
console.log('JoinedMembersTab: 查询结果:', res);
memberList.value = res.rows;
total.value = res.total;
} catch (error) {
console.error('JoinedMembersTab: 查询成员列表失败:', error);
proxy?.$modal.msgError('查询成员列表失败');
} finally {
loading.value = false;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData, relationId: props.clubId };
memberFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
memberType: 1,
relationId: props.clubId,
status: undefined, // 重置为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
params: {
phone: undefined
}
};
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StglMemberVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
// 检查人员上限
if (props.memberLimit && props.memberLimit > 0) {
if (props.currentMemberCount && props.currentMemberCount >= props.memberLimit) {
proxy?.$modal.msgError(`该社团成员已满,当前成员数量:${props.currentMemberCount},成员上限:${props.memberLimit},无法再新增成员`);
return;
}
}
reset();
form.value.joinTime = parseTime(new Date(), '{y}-{m}-{d}');
dialog.visible = true;
dialog.title = '添加成员';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: StglMemberVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getStglMember(_id);
Object.assign(form.value, res.data);
if (res.data) {
form.value.selectedStudent = {
id: res.data.userId,
studentNo: res.data.studentNo,
name: res.data.studentName,
gender: res.data.gender,
phone: res.data.phone,
collegeId: '',
collegeName: res.data.department,
majorId: '',
majorName: res.data.major,
classId: '',
className: res.data.className,
grade: '',
studentStatus: '',
enrollmentYear: new Date().getFullYear()
};
}
dialog.visible = true;
dialog.title = '修改成员';
};
/** 提交按钮 */
const submitForm = () => {
memberFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateStglMember(form.value);
} else {
await addStglMember(form.value);
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
// 通知父组件成员数量变化
notifyMemberCountChange();
} catch (error) {
console.error('操作失败:', error);
proxy?.$modal.msgError('操作失败');
} finally {
buttonLoading.value = false;
}
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: StglMemberVO) => {
const _ids = row?.id || ids.value;
let confirmMessage = '';
if (row) {
confirmMessage = `是否确认删除成员"${row.studentName}(${row.studentNo})"的数据项?`;
} else {
const selectedItems = memberList.value.filter((item) => ids.value.includes(item.id));
if (selectedItems.length === 1) {
confirmMessage = `是否确认删除成员"${selectedItems[0].studentName}(${selectedItems[0].studentNo})"的数据项?`;
} else {
const names = selectedItems.map((item) => `${item.studentName}(${item.studentNo})`).join('、');
confirmMessage = `是否确认删除成员"${names}"的数据项?`;
}
}
await proxy?.$modal.confirm(confirmMessage);
await delStglMember(_ids);
proxy?.$modal.msgSuccess('删除成功');
await getList();
// 通知父组件成员数量变化
notifyMemberCountChange();
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'stgl/member/export',
{
...queryParams.value
},
`${props.clubName}_成员信息_${new Date().getTime()}.xlsx`
);
};
/** 查看按钮操作 */
const handleView = async (row: StglMemberVO) => {
Object.assign(viewForm, row);
viewDialog.visible = true;
viewDialog.title = '查看成员信息';
};
/** 取消查看对话框 */
const cancelView = () => {
viewDialog.visible = false;
};
/** 学生选择变化处理 */
const handleStudentChange = async (student: StudentVO | null) => {
if (student) {
form.value.userId = student.id;
form.value.studentName = student.name;
form.value.studentNo = student.studentNo;
form.value.gender = student.gender;
form.value.department = student.collegeName;
form.value.major = student.majorName;
form.value.className = student.className;
form.value.phone = student.phone;
try {
const userRes = await getUser(student.id);
if (userRes.data && userRes.data.user) {
form.value.email = userRes.data.user.email || '';
}
} catch (error) {
console.warn('获取用户邮箱失败:', error);
form.value.email = '';
}
} else {
form.value.userId = undefined;
form.value.studentName = undefined;
form.value.studentNo = undefined;
form.value.gender = undefined;
form.value.department = undefined;
form.value.major = undefined;
form.value.className = undefined;
form.value.phone = undefined;
form.value.email = undefined;
}
nextTick(() => {
memberFormRef.value?.validateField(['selectedStudent', 'studentNo']);
});
};
/** 导入按钮操作 */
const handleImport = () => {
upload.title = '成员导入';
upload.open = true;
};
/** 下载模板操作 */
const importTemplate = () => {
proxy?.download('stgl/member/importTemplate', {}, `成员导入模板_${new Date().getTime()}.xlsx`);
};
/** 文件上传中处理 */
const handleFileUploadProgress = () => {
upload.isUploading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = (response: any) => {
upload.open = false;
upload.isUploading = false;
uploadRef.value?.clearFiles();
ElMessage.closeAll();
ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
dangerouslyUseHTMLString: true
});
getList();
// 通知父组件成员数量变化
notifyMemberCountChange();
};
/** 文件上传失败处理 */
const handleFileError = (error: any) => {
upload.isUploading = false;
uploadRef.value?.clearFiles();
ElMessage.closeAll();
console.error('文件上传失败:', error);
ElMessage.error('导入失败,请检查文件格式或网络连接');
};
/** 获取导入URL */
const getImportUrl = () => {
const baseUrl = upload.url;
const params = new URLSearchParams();
params.append('updateSupport', 'false');
return `${baseUrl}?${params.toString()}`;
};
/** 提交上传文件 */
const submitFileForm = () => {
ElMessage({
message: '正在导入数据,请耐心等待...',
type: 'info',
duration: 0,
zIndex: 4000
});
upload.isUploading = true;
uploadRef.value?.submit();
};
// 监听clubId变化,重新获取数据
watch(
() => props.clubId,
(newClubId) => {
console.log('JoinedMembersTab: clubId 变化:', newClubId);
if (newClubId) {
// 重置查询参数
queryParams.value = {
pageNum: 1,
pageSize: 10,
memberType: 1,
relationId: newClubId,
status: undefined, // 重置为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
params: {
phone: undefined
}
};
getList();
}
},
{ immediate: true }
);
onMounted(() => {
getList();
});
// 暴露方法给父组件调用
defineExpose({
getList
});
// 定义事件,用于通知父组件数据变化
const emit = defineEmits<{
memberCountChanged: [newCount: number]
}>();
// 通知父组件成员数量变化
const notifyMemberCountChange = () => {
// 重新获取最新的成员数量
getList().then(() => {
const newCount = memberList.value.length;
console.log('JoinedMembersTab: 成员数量变化,通知父组件:', newCount);
emit('memberCountChanged', newCount);
});
};
</script>
<style scoped>
.view-description {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 60px;
font-size: 14px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>
@@ -0,0 +1,114 @@
<template>
<el-dialog :title="title" :model-value="visible" @update:model-value="handleVisibleChange" width="1200px" append-to-body>
<el-tabs v-model="activeTab" type="border-card" :key="clubId" @tab-change="handleTabChange">
<!-- 已加入标签页 -->
<el-tab-pane label="已加入" name="joined">
<JoinedMembersTab
ref="joinedTabRef"
:club-id="clubId"
:club-name="clubName"
:member-limit="memberLimit"
:current-member-count="currentMemberCount"
:key="`joined-${clubId}`"
/>
</el-tab-pane>
<!-- 待审核标签页 -->
<el-tab-pane label="待审核" name="pending">
<PendingMembersTab
ref="pendingTabRef"
:club-id="clubId"
:club-name="clubName"
:key="`pending-${clubId}`"
/>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关 闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import JoinedMembersTab from './JoinedMembersTab.vue';
import PendingMembersTab from './PendingMembersTab.vue';
interface Props {
visible: boolean;
title: string;
clubId: string | number;
clubName: string;
memberLimit?: number;
currentMemberCount?: number;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const activeTab = ref('joined');
const joinedTabRef = ref();
const pendingTabRef = ref();
// 监听visible变化
watch(() => props.visible, (newVal) => {
if (newVal) {
activeTab.value = 'joined';
// 当弹窗打开时,触发当前标签页的数据加载
nextTick(() => {
if (activeTab.value === 'joined' && joinedTabRef.value) {
joinedTabRef.value.getList();
} else if (activeTab.value === 'pending' && pendingTabRef.value) {
pendingTabRef.value.getList();
}
});
}
});
// 监听clubId变化,重置标签页
watch(() => props.clubId, (newClubId) => {
console.log('MemberManagementDialog: clubId 变化:', newClubId);
if (newClubId) {
activeTab.value = 'joined';
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleVisibleChange = (value: boolean) => {
emit('update:visible', value);
};
/** 标签页切换处理 */
const handleTabChange = (tabName: string) => {
console.log('标签页切换到:', tabName);
// 根据当前激活的标签页,调用对应组件的刷新方法
if (tabName === 'joined' && joinedTabRef.value) {
// 调用已加入成员Tab的刷新方法
if (typeof joinedTabRef.value.getList === 'function') {
joinedTabRef.value.getList();
}
} else if (tabName === 'pending' && pendingTabRef.value) {
// 调用待审核成员Tab的刷新方法
if (typeof pendingTabRef.value.getList === 'function') {
pendingTabRef.value.getList();
}
}
};
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -0,0 +1,514 @@
<template>
<div class="pending-members-tab">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="always" style="padding: 8px 16px;">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" style="padding: 0;">
<el-row :gutter="16" align="middle">
<el-col :span="5">
<el-form-item label="姓名" prop="studentName" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系方式" prop="phone" style="margin-bottom: 0;">
<el-input v-model="queryParams.params.phone" placeholder="请输入联系方式" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8" class="search-btn-container">
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Check"
:disabled="multiple"
@click="handleBatchApprove()"
v-hasPermi="['stgl:clubmember:approve']"
>批量审核</el-button
>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :options="stgl_member_status" :value="scope.row.status" v-if="scope.row.status" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" v-if="scope.row.gender" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="院系" align="center" prop="department" width="120" />
<el-table-column label="专业" align="center" prop="major" width="180" />
<el-table-column label="班级" align="center" prop="className" width="120" />
<el-table-column label="角色类型" align="center" prop="roleType" width="100">
<template #default="scope">
<dict-tag :options="stgl_role_type" :value="scope.row.roleType" v-if="scope.row.roleType" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="申请时间" align="center" prop="createTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" width="200" show-overflow-tooltip />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="info"
icon="View"
size="default"
style="font-size: 16px; margin-right: 8px; color: blue"
@click="handleView(scope.row)"
v-hasPermi="['stgl:clubmember:query']"
></el-button>
</el-tooltip>
<el-tooltip content="审核" placement="top">
<el-button
link
type="success"
icon="Check"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleApprove(scope.row)"
v-hasPermi="['stgl:clubmember:approve']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 查看成员详情对话框 -->
<el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ viewForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="sys_user_sex" :value="viewForm.gender" />
</el-descriptions-item>
<el-descriptions-item label="院系">{{ viewForm.department }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ viewForm.major }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ viewForm.className }}</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ viewForm.phone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="角色类型">
<dict-tag :options="stgl_role_type" :value="viewForm.roleType" />
</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :options="stgl_member_status" :value="viewForm.status" />
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ parseTime(viewForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">备注信息</el-divider>
<div class="view-description">
{{ viewForm.remark || '暂无备注' }}
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 审核对话框 -->
<el-dialog :title="approvalTitle" v-model="approvalOpen" width="500px" append-to-body>
<el-form ref="approvalRef" :model="approvalForm" :rules="approvalRules" label-width="80px">
<el-form-item label="审核结果" prop="approvalStatus">
<el-select v-model="approvalForm.approvalStatus" placeholder="请选择审核结果" style="width: 100%">
<el-option :value="2" label="审核通过">
<span style="display: flex; align-items: center">
<el-icon style="color: #67c23a; margin-right: 8px"><Check /></el-icon>
审核通过
</span>
</el-option>
<el-option :value="3" label="审核不通过">
<span style="display: flex; align-items: center">
<el-icon style="color: #f56c6c; margin-right: 8px"><Close /></el-icon>
审核不通过
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="审核意见" prop="approvalRemark">
<el-input v-model="approvalForm.approvalRemark" type="textarea" placeholder="请输入审核意见" :rows="4" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitApproval">确 定</el-button>
<el-button @click="approvalOpen = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, toRefs, onMounted, getCurrentInstance, watch } from 'vue';
import { ElMessageBox } from 'element-plus';
import { Check, Close } from '@element-plus/icons-vue';
import { listStglMember, getStglMember, approveMember } from '@/api/stgl/member';
import { StglMemberVO, StglMemberQuery } from '@/api/stgl/member/types';
import { parseTime } from '@/utils/ruoyi';
interface Props {
clubId: string | number;
clubName: string;
}
const props = defineProps<Props>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { stgl_role_type, sys_user_sex, stgl_member_status } = toRefs<any>(proxy?.useDict('stgl_role_type', 'sys_user_sex', 'stgl_member_status'));
const memberList = ref<StglMemberVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const approvalRef = ref<ElFormInstance>();
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const approvalOpen = ref(false);
const approvalTitle = ref('');
const viewForm = reactive<StglMemberVO>({
id: undefined,
memberType: 1,
relationId: undefined,
clubName: '',
userId: undefined,
studentNo: '',
studentName: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
email: '',
roleType: '1',
roleName: '',
joinTime: '',
status: '0',
remark: '',
createTime: ''
});
const approvalForm = reactive({
id: undefined,
approvalStatus: undefined,
approvalRemark: ''
});
const approvalRules = reactive({
approvalStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
approvalRemark: [{ required: true, message: '请输入审核意见', trigger: 'blur' }]
});
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
memberType: 1,
relationId: props.clubId,
status: undefined, // 初始为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
params: {
phone: undefined
}
}
});
const { queryParams } = toRefs(data);
/** 查询成员列表 */
const getList = async () => {
loading.value = true;
try {
// 构建查询参数,支持多状态查询
const queryData = { ...queryParams.value };
console.log('PendingMembersTab: getList 开始查询,参数:', queryData);
// 如果状态字段为空或未选择,则查询待审核(status=0)和已驳回(status=4)的成员
if (!queryData.status) {
// 使用params传递多状态查询
queryData.params = { ...queryData.params, statusList: ['0', '4'] } as any;
delete queryData.status; // 删除单个status字段
}
console.log('PendingMembersTab: 最终查询参数:', queryData);
const res = await listStglMember(queryData);
console.log('PendingMembersTab: 查询结果:', res);
memberList.value = res.rows;
total.value = res.total;
} catch (error) {
console.error('PendingMembersTab: 查询成员列表失败:', error);
proxy?.$modal.msgError('查询成员列表失败');
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
memberType: 1,
relationId: props.clubId,
status: undefined, // 重置为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
params: {
phone: undefined
}
};
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StglMemberVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 查看按钮操作 */
const handleView = async (row: StglMemberVO) => {
Object.assign(viewForm, row);
viewDialog.visible = true;
viewDialog.title = '查看申请信息';
};
/** 取消查看对话框 */
const cancelView = () => {
viewDialog.visible = false;
};
/** 验证成员数据完整性 */
const validateMemberData = (member: StglMemberVO, operation: string): boolean => {
if (!member.userId) {
proxy?.$modal.msgError(`${operation}失败:用户ID不能为空`);
console.error(`${operation}失败 - 用户ID缺失:`, member);
return false;
}
if (!member.relationId && !props.clubId) {
proxy?.$modal.msgError(`${operation}失败:关联ID(社团ID)不能为空`);
console.error(`${operation}失败 - 关联ID缺失:`, member);
return false;
}
if (!member.memberType) {
proxy?.$modal.msgError(`${operation}失败:成员类型不能为空`);
console.error(`${operation}失败 - 成员类型缺失:`, member);
return false;
}
if (!member.studentNo || !member.studentName) {
proxy?.$modal.msgError(`${operation}失败:学号和姓名不能为空`);
console.error(`${operation}失败 - 基础信息缺失:`, member);
return false;
}
return true;
};
/** 审核按钮操作 */
const handleApprove = (row: StglMemberVO) => {
// 验证数据完整性
if (!validateMemberData(row, '审核')) {
return;
}
approvalForm.id = row.id;
approvalForm.approvalStatus = undefined;
approvalForm.approvalRemark = '';
approvalTitle.value = '审核';
approvalOpen.value = true;
};
/** 批量审核操作 */
const handleBatchApprove = () => {
if (ids.value.length === 0) {
proxy.$modal.msgError('请选择要审核的数据');
return;
}
// 验证所有选中成员的数据完整性
const invalidMembers = memberList.value
.filter(member => ids.value.includes(member.id))
.filter(member => !validateMemberData(member, '批量审核'));
if (invalidMembers.length > 0) {
const invalidNames = invalidMembers.map(m => m.studentName || '未知成员').join('、');
proxy.$modal.msgError(`以下成员数据不完整,无法审核:${invalidNames}`);
return;
}
approvalForm.id = undefined; // 标识为批量审核
approvalForm.approvalStatus = undefined;
approvalForm.approvalRemark = '';
approvalTitle.value = '批量审核';
approvalOpen.value = true;
};
/** 提交审核 */
const submitApproval = () => {
const validateFields = ['approvalStatus', 'approvalRemark'];
approvalRef.value.validateField(validateFields, (valid) => {
if (valid) {
if (approvalForm.id) {
// 单个审核 - 再次验证数据完整性
const member = memberList.value.find(m => m.id === approvalForm.id);
if (member && !validateMemberData(member, '审核')) {
return;
}
approveMember(approvalForm.id, approvalForm.approvalStatus === 2).then(() => {
proxy.$modal.msgSuccess('审核成功');
approvalOpen.value = false;
getList();
}).catch((error) => {
console.error('审核失败:', error);
proxy.$modal.msgError('审核失败,请检查数据完整性');
});
} else {
// 批量审核 - 再次验证数据完整性
const selectedMembers = memberList.value.filter(m => ids.value.includes(m.id));
const invalidMembers = selectedMembers.filter(member => !validateMemberData(member, '批量审核'));
if (invalidMembers.length > 0) {
const invalidNames = invalidMembers.map(m => m.studentName || '未知成员').join('、');
proxy.$modal.msgError(`以下成员数据不完整,无法审核:${invalidNames}`);
return;
}
const promises = ids.value.map(id =>
approveMember(id, approvalForm.approvalStatus === 2)
);
Promise.all(promises).then(() => {
proxy.$modal.msgSuccess('批量审核成功');
approvalOpen.value = false;
getList();
}).catch((error) => {
console.error('批量审核失败:', error);
proxy.$modal.msgError('批量审核失败,请检查数据完整性');
});
}
}
});
};
// 监听clubId变化,重新获取数据
watch(
() => props.clubId,
(newClubId) => {
console.log('PendingMembersTab: clubId 变化:', newClubId);
if (newClubId) {
// 重置查询参数
queryParams.value = {
pageNum: 1,
pageSize: 10,
memberType: 1,
relationId: newClubId,
status: undefined, // 重置为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
params: {
phone: undefined
}
};
getList();
}
},
{ immediate: true }
);
onMounted(() => {
getList();
});
// 暴露方法给父组件调用
defineExpose({
getList
});
</script>
<style scoped>
.view-description {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 60px;
font-size: 14px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>
@@ -0,0 +1,735 @@
<template>
<div class="joined-members-tab">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="always" style="padding: 8px 16px;">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" style="padding: 0;">
<el-row :gutter="16" align="middle">
<el-col :span="5">
<el-form-item label="姓名" prop="studentName" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系方式" prop="phone" style="margin-bottom: 0;">
<el-input v-model="queryParams.phone" placeholder="请输入联系方式" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-tooltip :content="getAddButtonTooltip" placement="top" :disabled="!isAddButtonDisabled">
<el-button type="primary" plain icon="Plus" @click="handleAdd" :disabled="isAddButtonDisabled" v-hasPermi="['stgl:volunteermember:add']">
新增
</el-button>
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['stgl:volunteermember:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['stgl:volunteermember:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-dropdown class="mt-[1px]">
<el-button>
更多
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
<el-dropdown-item icon="Top" @click="handleImport" v-hasPermi="['stgl:volunteermember:import']">导入数据</el-dropdown-item>
<el-dropdown-item icon="Download" @click="handleExport" v-hasPermi="['stgl:volunteermember:export']">导出数据</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :options="stgl_member_status" :value="scope.row.status" v-if="scope.row.status !== undefined" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" v-if="scope.row.gender" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="院系" align="center" prop="department" width="120" />
<el-table-column label="专业" align="center" prop="major" width="180" />
<el-table-column label="班级" align="center" prop="className" width="120" />
<el-table-column label="成员角色" align="center" prop="roleType" width="100">
<template #default="scope">
<span v-if="scope.row.roleType">{{ scope.row.roleType }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="加入时间" align="center" prop="joinTime" width="100">
<template #default="scope">
<span>{{ parseTime(scope.row.joinTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['stgl:volunteermember:query']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['stgl:volunteermember:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['stgl:volunteermember:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改志愿者成员对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-form ref="memberFormRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="姓名" prop="selectedStudent">
<StudentSelector v-model="form.selectedStudent" placeholder="请选择学生" :showStudentNo="false" @change="handleStudentChange" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="form.studentNo" placeholder="学号" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-select v-model="form.gender" placeholder="性别" disabled>
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"> </el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="院系" prop="department">
<el-input v-model="form.department" placeholder="院系" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="major">
<el-input v-model="form.major" placeholder="专业" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="班级" prop="className">
<el-input v-model="form.className" placeholder="班级" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="联系方式" readonly />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="成员角色" prop="roleType">
<el-input v-model="form.roleType" placeholder="请输入成员角色" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option v-for="dict in stgl_member_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="加入时间" prop="joinTime">
<el-date-picker clearable v-model="form.joinTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择加入时间"> </el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 查看成员详情对话框 -->
<el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ viewForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="sys_user_sex" :value="viewForm.gender" />
</el-descriptions-item>
<el-descriptions-item label="院系">{{ viewForm.department }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ viewForm.major }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ viewForm.className }}</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ viewForm.phone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="成员角色">
{{ viewForm.roleType || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :options="stgl_member_status" :value="viewForm.status" />
</el-descriptions-item>
<el-descriptions-item label="加入时间">{{ parseTime(viewForm.joinTime, '{y}-{m}-{d}') }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(viewForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">备注信息</el-divider>
<div class="view-description">
{{ viewForm.remark || '暂无备注' }}
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 导入对话框 -->
<el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
<el-upload
ref="uploadRef"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:auto-upload="false"
drag
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<div class="el-upload__tip">
<el-checkbox v-model="upload.updateSupport" />
是否更新已经存在的志愿者成员数据
</div>
<span>仅允许导入xls、xlsx格式文件。</span>
<el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" @click="importTemplate">下载模板</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">确 定</el-button>
<el-button @click="upload.open = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref, toRefs, computed } from 'vue';
import {
listVolunteerMember,
getVolunteerMember,
delVolunteerMember,
addVolunteerMember,
updateVolunteerMember,
exportVolunteerMember
} from '@/api/stgl/volunteermember';
import { VolunteerMemberVO, VolunteerMemberForm, VolunteerMemberQuery } from '@/api/stgl/volunteermember/types';
import { StudentVO } from '@/api/xg/student/types';
import StudentSelector from '@/components/StudentSelector/index.vue';
import { parseTime } from '@/utils/ruoyi';
import { globalHeaders } from '@/utils/request';
import { ArrowDown, UploadFilled } from '@element-plus/icons-vue';
import type { UploadInstance } from 'element-plus';
interface Props {
teamId: string | number;
teamName: string;
memberLimit?: number;
currentMemberCount?: number;
}
// 扩展VolunteerMemberForm以包含选择的学生对象
interface ExtendedVolunteerMemberForm extends VolunteerMemberForm {
selectedStudent?: StudentVO | null;
userId?: string | number; // 添加userId字段
memberType?: number; // 添加memberType字段
}
const props = defineProps<Props>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex, sys_normal_disable, stgl_member_status } = toRefs<any>(proxy?.useDict('sys_user_sex', 'sys_normal_disable', 'stgl_member_status'));
const memberList = ref<VolunteerMemberVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const memberFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewForm = reactive<VolunteerMemberVO>({
id: undefined,
relationId: undefined,
studentNo: '',
studentName: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
email: '',
roleType: '',
joinTime: '',
status: '0',
remark: '',
createTime: ''
});
const initFormData: ExtendedVolunteerMemberForm = {
id: undefined,
relationId: props.teamId,
selectedStudent: null,
userId: undefined,
memberType: 2, // 志愿者成员类型
studentNo: '',
studentName: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
email: '',
roleType: '队员',
joinTime: undefined,
status: '2', // 已加入状态
remark: undefined
};
const data = reactive<PageData<ExtendedVolunteerMemberForm, VolunteerMemberQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
relationId: props.teamId,
status: undefined, // 初始为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
phone: undefined
},
rules: {
selectedStudent: [{ required: true, message: '请选择学生', trigger: 'change' }],
studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }],
gender: [{ required: true, message: '性别不能为空', trigger: 'change' }],
department: [{ required: true, message: '院系不能为空', trigger: 'blur' }],
major: [{ required: true, message: '专业不能为空', trigger: 'blur' }],
className: [{ required: true, message: '班级不能为空', trigger: 'blur' }],
phone: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
roleType: [{ required: true, message: '成员角色不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
joinTime: [{ required: true, message: '加入时间不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
// 计算属性:判断新增按钮是否应该被禁用
const isAddButtonDisabled = computed(() => {
// 使用本地成员数量,确保实时更新
const currentCount = memberList.value.length;
if (!props.memberLimit || props.memberLimit <= 0) {
return false; // 没有设置上限,允许新增
}
return currentCount >= props.memberLimit;
});
// 计算属性:新增按钮的提示信息
const getAddButtonTooltip = computed(() => {
const currentCount = memberList.value.length;
if (!props.memberLimit || props.memberLimit <= 0) {
return '该团队未设置成员上限';
}
return `当前成员:${currentCount}/${props.memberLimit}${currentCount >= props.memberLimit ? ',已达到上限' : ''}`;
});
// 上传相关
const uploadRef = ref<UploadInstance>();
const upload = reactive<{
open: boolean;
title: string;
isUploading: boolean;
updateSupport: boolean;
url: string;
headers: { Authorization: string };
}>({
open: false,
title: '',
isUploading: false,
updateSupport: false,
url: import.meta.env.VITE_APP_BASE_API + '/stgl/volunteer/member/importData',
headers: globalHeaders()
});
/** 查询成员列表 */
const getList = async () => {
loading.value = true;
try {
// 构建查询参数,支持多状态查询
const queryData = { ...queryParams.value };
console.log('JoinedMembersTab: getList 开始查询,参数:', queryData);
// 如果状态字段为空或未选择,则查询已加入(status=2)和已退出(status=4)的成员
if (!queryData.status) {
// 使用params传递多状态查询
queryData.params = { statusList: ['2', '4'] } as any;
delete queryData.status; // 删除单个status字段
}
console.log('JoinedMembersTab: 最终查询参数:', queryData);
const res = await listVolunteerMember(queryData);
console.log('JoinedMembersTab: 查询结果:', res);
memberList.value = res.rows;
total.value = res.total;
} catch (error) {
console.error('JoinedMembersTab: 查询成员列表失败:', error);
proxy?.$modal.msgError('查询成员列表失败');
} finally {
loading.value = false;
}
};
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
};
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData, relationId: props.teamId, memberType: 2, status: '2' };
memberFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
relationId: props.teamId,
status: undefined, // 重置为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
phone: undefined
};
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: VolunteerMemberVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
// 检查人员上限 - 使用本地成员数量,确保实时更新
const currentCount = memberList.value.length;
if (props.memberLimit && props.memberLimit > 0) {
if (currentCount >= props.memberLimit) {
proxy?.$modal.msgError(`该团队成员已满,当前成员数量:${currentCount},成员上限:${props.memberLimit},无法再新增成员`);
return;
}
}
reset();
form.value.joinTime = parseTime(new Date(), '{y}-{m}-{d}');
dialog.visible = true;
dialog.title = '添加志愿者成员';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: VolunteerMemberVO) => {
reset();
const _id = row?.id || ids.value[0];
const res = await getVolunteerMember(_id);
Object.assign(form.value, res.data);
// 设置学生选择器数据
if (res.data.studentName) {
form.value.selectedStudent = {
id: res.data.userId || 0, // 使用实际的userId
studentNo: res.data.studentNo || '',
name: res.data.studentName,
gender: res.data.gender || '0',
phone: res.data.phone || '',
collegeName: res.data.department || '',
majorName: res.data.major || '',
className: res.data.className || ''
} as StudentVO;
}
dialog.visible = true;
dialog.title = '修改志愿者成员';
};
/** 提交按钮 */
const submitForm = () => {
memberFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
// 如果是新增操作,再次检查成员数量上限
if (!form.value.id) {
const currentCount = memberList.value.length;
if (props.memberLimit && props.memberLimit > 0 && currentCount >= props.memberLimit) {
proxy?.$modal.msgError(`该团队成员已满,当前成员数量:${currentCount},成员上限:${props.memberLimit},无法再新增成员`);
return;
}
}
buttonLoading.value = true;
// 创建提交数据,排除selectedStudent字段,确保包含userId和memberType
const { selectedStudent, ...submitData } = form.value;
// 确保memberType为2(志愿者成员)
submitData.memberType = 2;
// 确保status为2(已加入状态)
if (!submitData.status) {
submitData.status = '2';
}
if (form.value.id) {
await updateVolunteerMember(submitData as VolunteerMemberForm).finally(() => (buttonLoading.value = false));
} else {
await addVolunteerMember(submitData as VolunteerMemberForm).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
await getList();
// 通知父组件成员数量变化
notifyMemberCountChange();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: VolunteerMemberVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除选中的志愿者成员?');
// 处理单个删除或批量删除
if (Array.isArray(_ids)) {
await delVolunteerMember(_ids.join(','));
} else {
await delVolunteerMember(_ids);
}
proxy?.$modal.msgSuccess('删除成功');
await getList();
// 通知父组件成员数量变化
notifyMemberCountChange();
};
/** 查看按钮操作 */
const handleView = async (row: VolunteerMemberVO) => {
const res = await getVolunteerMember(row.id);
Object.assign(viewForm, res.data);
viewDialog.visible = true;
viewDialog.title = '查看志愿者成员详情';
};
/** 取消查看对话框 */
const cancelView = () => {
viewDialog.visible = false;
};
/** 学生选择变化处理 */
const handleStudentChange = (student: StudentVO | null) => {
if (student) {
form.value.userId = student.id; // 设置userId为学生的ID
form.value.studentNo = student.studentNo;
form.value.studentName = student.name;
form.value.gender = student.gender;
form.value.department = student.collegeName;
form.value.major = student.majorName;
form.value.className = student.className;
form.value.phone = student.phone;
} else {
form.value.userId = undefined;
form.value.studentNo = '';
form.value.studentName = '';
form.value.gender = '';
form.value.department = '';
form.value.major = '';
form.value.className = '';
form.value.phone = '';
}
};
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download(
'stgl/volunteer/member/export',
{
...queryParams.value
},
`volunteer_member_${new Date().getTime()}.xlsx`
);
};
/** 导入模板下载 */
const importTemplate = () => {
proxy?.download('stgl/volunteer/member/importTemplate', {}, `志愿者成员导入模板_${new Date().getTime()}.xlsx`);
};
/** 导入按钮操作 */
const handleImport = () => {
upload.title = '志愿者成员导入';
upload.open = true;
};
/** 提交上传文件 */
const submitFileForm = () => {
uploadRef.value?.submit();
};
/** 文件上传中处理 */
const handleFileUploadProgress = () => {
upload.isUploading = true;
};
/** 文件上传成功处理 */
const handleFileSuccess = (response: any) => {
upload.open = false;
upload.isUploading = false;
uploadRef.value?.clearFiles();
proxy?.$modal.msgSuccess(response.msg);
if (response.msg.includes('成功')) {
getList();
// 通知父组件成员数量变化
notifyMemberCountChange();
// 检查导入后是否超过成员上限
setTimeout(() => {
const currentCount = memberList.value.length;
if (props.memberLimit && props.memberLimit > 0 && currentCount > props.memberLimit) {
proxy?.$modal.msgWarning(`导入完成,但当前成员数量(${currentCount})已超过设定上限(${props.memberLimit}),建议及时调整成员数量`);
}
}, 1000);
}
};
// 暴露给父组件的方法
defineExpose({
getList
});
// 定义事件,用于通知父组件数据变化
const emit = defineEmits<{
memberCountChanged: [newCount: number]
}>();
// 通知父组件成员数量变化
const notifyMemberCountChange = () => {
// 重新获取最新的成员数量
getList().then(() => {
const newCount = memberList.value.length;
console.log('JoinedMembersTab: 成员数量变化,通知父组件:', newCount);
emit('memberCountChanged', newCount);
});
};
onMounted(() => {
getList();
});
</script>
<style scoped>
.view-description {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 80px;
font-size: 14px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>
@@ -0,0 +1,121 @@
<template>
<el-dialog :title="title" :model-value="visible" @update:model-value="handleVisibleChange" width="1200px" append-to-body>
<el-tabs v-model="activeTab" type="border-card" :key="teamId" @tab-change="handleTabChange">
<!-- 已加入标签页 -->
<el-tab-pane label="已加入" name="joined">
<JoinedMembersTab
ref="joinedTabRef"
:team-id="teamId"
:team-name="teamName"
:member-limit="memberLimit"
:current-member-count="currentMemberCount"
:key="`joined-${teamId}`"
@member-count-changed="handleMemberCountChanged"
/>
</el-tab-pane>
<!-- 待审核标签页 -->
<el-tab-pane label="待审核" name="pending">
<PendingMembersTab
ref="pendingTabRef"
:team-id="teamId"
:team-name="teamName"
:key="`pending-${teamId}`"
/>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关 闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import JoinedMembersTab from './JoinedMembersTab.vue';
import PendingMembersTab from './PendingMembersTab.vue';
interface Props {
visible: boolean;
title: string;
teamId: string | number;
teamName: string;
memberLimit?: number;
currentMemberCount?: number;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const activeTab = ref('joined');
const joinedTabRef = ref();
const pendingTabRef = ref();
// 监听visible变化
watch(() => props.visible, (newVal) => {
if (newVal) {
activeTab.value = 'joined';
// 当弹窗打开时,触发当前标签页的数据加载
nextTick(() => {
if (activeTab.value === 'joined' && joinedTabRef.value) {
joinedTabRef.value.getList();
} else if (activeTab.value === 'pending' && pendingTabRef.value) {
pendingTabRef.value.getList();
}
});
}
});
// 监听teamId变化,重置标签页
watch(() => props.teamId, (newTeamId) => {
console.log('MemberManagementDialog: teamId 变化:', newTeamId);
if (newTeamId) {
activeTab.value = 'joined';
}
});
const handleClose = () => {
emit('update:visible', false);
};
const handleVisibleChange = (value: boolean) => {
emit('update:visible', value);
};
/** 处理成员数量变化 */
const handleMemberCountChanged = (newCount: number) => {
console.log('MemberManagementDialog: 成员数量变化:', newCount);
// 这里可以添加额外的处理逻辑,比如更新父组件的数据
};
/** 标签页切换处理 */
const handleTabChange = (tabName: string) => {
console.log('标签页切换到:', tabName);
// 根据当前激活的标签页,调用对应组件的刷新方法
if (tabName === 'joined' && joinedTabRef.value) {
// 调用已加入成员Tab的刷新方法
if (typeof joinedTabRef.value.getList === 'function') {
joinedTabRef.value.getList();
}
} else if (tabName === 'pending' && pendingTabRef.value) {
// 调用待审核成员Tab的刷新方法
if (typeof pendingTabRef.value.getList === 'function') {
pendingTabRef.value.getList();
}
}
};
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -0,0 +1,555 @@
<template>
<div class="pending-members-tab">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="always" style="padding: 8px 16px;">
<el-form ref="queryFormRef" :model="queryParams" label-width="80px" style="padding: 0;">
<el-row :gutter="16" align="middle">
<el-col :span="5">
<el-form-item label="姓名" prop="studentName" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentName" placeholder="请输入姓名" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="学号" prop="studentNo" style="margin-bottom: 0;">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系方式" prop="phone" style="margin-bottom: 0;">
<el-input v-model="queryParams.phone" placeholder="请输入联系方式" clearable size="small" @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="margin-bottom: 0;">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格区域 -->
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="success" plain icon="Select" :disabled="multiple" @click="handleBatchApprove" v-hasPermi="['stgl:volunteermember:edit']">批量通过</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Close" :disabled="multiple" @click="handleBatchReject" v-hasPermi="['stgl:volunteermember:edit']">批量拒绝</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="memberList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center">
<template #default="scope">
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :options="stgl_member_status" :value="scope.row.status" v-if="scope.row.status !== undefined" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
<el-table-column label="姓名" align="center" prop="studentName" width="80" />
<el-table-column label="性别" align="center" prop="gender" width="60">
<template #default="scope">
<dict-tag :options="sys_user_sex" :value="scope.row.gender" v-if="scope.row.gender" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="联系方式" align="center" prop="phone" width="120" />
<el-table-column label="院系" align="center" prop="department" width="120" />
<el-table-column label="专业" align="center" prop="major" width="180" />
<el-table-column label="班级" align="center" prop="className" width="120" />
<el-table-column label="申请时间" align="center" prop="createTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['stgl:volunteermember:query']"></el-button>
</el-tooltip>
<el-tooltip content="通过" placement="top">
<el-button link type="success" icon="Select" @click="handleApprove(scope.row)" v-hasPermi="['stgl:volunteermember:edit']"></el-button>
</el-tooltip>
<el-tooltip content="拒绝" placement="top">
<el-button link type="danger" icon="Close" @click="handleReject(scope.row)" v-hasPermi="['stgl:volunteermember:edit']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 查看成员详情对话框 -->
<el-dialog :title="viewDialog.title" v-model="viewDialog.visible" width="800px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学号">{{ viewForm.studentNo }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ viewForm.studentName }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="sys_user_sex" :value="viewForm.gender" />
</el-descriptions-item>
<el-descriptions-item label="院系">{{ viewForm.department }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ viewForm.major }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ viewForm.className }}</el-descriptions-item>
<el-descriptions-item label="联系方式">{{ viewForm.phone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :options="stgl_member_status" :value="viewForm.status" />
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ parseTime(viewForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">备注信息</el-divider>
<div class="view-description">
{{ viewForm.remark || '暂无备注' }}
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelView">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance, onMounted, reactive, ref, toRefs } from 'vue';
import {
listVolunteerMember,
getVolunteerMember,
updateVolunteerMember
} from '@/api/stgl/volunteermember';
import { VolunteerMemberVO, VolunteerMemberQuery } from '@/api/stgl/volunteermember/types';
import { parseTime } from '@/utils/ruoyi';
interface Props {
teamId: string | number;
teamName: string;
}
const props = defineProps<Props>();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_user_sex, sys_normal_disable, stgl_member_status } = toRefs<any>(proxy?.useDict('sys_user_sex', 'sys_normal_disable', 'stgl_member_status'));
const memberList = ref<VolunteerMemberVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const viewDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const viewForm = reactive<VolunteerMemberVO>({
id: undefined,
relationId: undefined,
studentNo: '',
studentName: '',
gender: '',
department: '',
major: '',
className: '',
phone: '',
email: '',
roleType: '',
joinTime: '',
status: '1', // 待审核状态
remark: '',
createTime: ''
});
const data = reactive<PageData<any, VolunteerMemberQuery>>({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
relationId: props.teamId,
status: undefined, // 初始为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
phone: undefined
},
rules: {}
});
const { queryParams } = toRefs(data);
/** 验证成员数据完整性 */
const validateMemberData = (member: VolunteerMemberVO, operation: string): boolean => {
if (!member.userId) {
proxy?.$modal.msgError(`${operation}失败:用户ID不能为空`);
console.error(`${operation}失败 - 用户ID缺失:`, member);
return false;
}
if (!member.relationId && !props.teamId) {
proxy?.$modal.msgError(`${operation}失败:关联ID(志愿者队伍ID)不能为空`);
console.error(`${operation}失败 - 关联ID缺失:`, member);
return false;
}
if (!member.studentNo || !member.studentName) {
proxy?.$modal.msgError(`${operation}失败:学号和姓名不能为空`);
console.error(`${operation}失败 - 基础信息缺失:`, member);
return false;
}
return true;
};
/** 查询成员列表 */
const getList = async () => {
loading.value = true;
try {
// 构建查询参数,支持多状态查询
const queryData = { ...queryParams.value };
console.log('PendingMembersTab: getList 开始查询,参数:', queryData);
// 如果状态字段为空或未选择,则查询待审核(status=0)和已驳回(status=3)的成员
if (!queryData.status) {
// 使用params传递多状态查询
queryData.params = { statusList: ['0', '3'] } as any;
delete queryData.status; // 删除单个status字段
}
console.log('PendingMembersTab: 最终查询参数:', queryData);
const res = await listVolunteerMember(queryData);
console.log('PendingMembersTab: 查询结果:', res);
memberList.value = res.rows;
total.value = res.total;
// 调试:检查数据完整性
if (memberList.value.length > 0) {
console.log('PendingMembersTab: 第一条成员数据:', memberList.value[0]);
console.log('PendingMembersTab: props.teamId:', props.teamId);
}
} catch (error) {
console.error('PendingMembersTab: 查询成员列表失败:', error);
proxy?.$modal.msgError('查询成员列表失败');
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value = {
pageNum: 1,
pageSize: 10,
relationId: props.teamId,
status: undefined, // 重置为空,让getList方法处理多状态查询
studentName: undefined,
studentNo: undefined,
phone: undefined
};
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: VolunteerMemberVO[]) => {
ids.value = selection.map((item) => item.id);
multiple.value = !selection.length;
};
/** 查看按钮操作 */
const handleView = async (row: VolunteerMemberVO) => {
const res = await getVolunteerMember(row.id);
Object.assign(viewForm, res.data);
viewDialog.visible = true;
viewDialog.title = '查看志愿者成员申请详情';
};
/** 取消查看对话框 */
const cancelView = () => {
viewDialog.visible = false;
};
/** 通过申请 */
const handleApprove = async (row: VolunteerMemberVO) => {
// 验证数据完整性
if (!validateMemberData(row, '审核通过')) {
return;
}
await proxy?.$modal.confirm(`确认通过"${row.studentName}"的入队申请?`);
// 构建完整的更新数据,包含所有必要字段
const updateData = {
id: row.id,
userId: row.userId, // 添加用户ID字段
memberType: 2, // 志愿者成员类型(必需字段)
relationId: row.relationId || props.teamId, // 优先使用行数据中的relationId,否则使用props中的teamId
studentNo: row.studentNo,
studentName: row.studentName,
gender: row.gender,
department: row.department,
major: row.major,
className: row.className,
phone: row.phone,
email: row.email,
roleType: row.roleType || '队员', // 默认为队员
status: '2', // 设置为已加入状态
joinTime: new Date().toISOString().split('T')[0], // 设置加入时间为当前日期
remark: row.remark
};
console.log('审核通过 - 更新数据:', updateData);
await updateVolunteerMember(updateData as any);
proxy?.$modal.msgSuccess('审核通过成功');
await getList();
};
/** 拒绝申请 */
const handleReject = async (row: VolunteerMemberVO) => {
// 验证数据完整性
if (!validateMemberData(row, '审核拒绝')) {
return;
}
await proxy?.$modal.confirm(`确认拒绝"${row.studentName}"的入队申请?`);
// 构建完整的更新数据,包含所有必要字段
const updateData = {
id: row.id,
userId: row.userId, // 添加用户ID字段
memberType: 2, // 志愿者成员类型(必需字段)
relationId: row.relationId || props.teamId, // 优先使用行数据中的relationId,否则使用props中的teamId
studentNo: row.studentNo,
studentName: row.studentName,
gender: row.gender,
department: row.department,
major: row.major,
className: row.className,
phone: row.phone,
email: row.email,
roleType: '', // 拒绝审核后没有角色
status: '3', // 设置为已驳回状态
remark: row.remark
};
console.log('审核拒绝 - 更新数据:', updateData);
await updateVolunteerMember(updateData as any);
proxy?.$modal.msgSuccess('审核拒绝成功');
await getList();
};
/** 批量通过 */
const handleBatchApprove = async () => {
if (ids.value.length === 0) {
proxy?.$modal.msgWarning('请选择要审核的成员');
return;
}
await proxy?.$modal.confirm(`确认批量通过所选成员的入队申请?`);
const promises = ids.value.map(id => {
// 根据ID找到对应的成员记录,获取relationId
const member = memberList.value.find(m => m.id === id);
if (!member) {
proxy?.$modal.msgError(`未找到ID为${id}的成员记录`);
return Promise.reject(new Error(`未找到ID为${id}的成员记录`));
}
// 验证数据完整性
if (!validateMemberData(member, '批量审核通过')) {
return Promise.reject(new Error(`成员${member.studentName}数据不完整`));
}
// 构建完整的更新数据,包含所有必要字段
const updateData = {
id: id,
userId: member.userId, // 添加用户ID字段
memberType: 2, // 志愿者成员类型(必需字段)
relationId: member.relationId || props.teamId, // 优先使用行数据中的relationId,否则使用props中的teamId
studentNo: member.studentNo,
studentName: member.studentName,
gender: member.gender,
department: member.department,
major: member.major,
className: member.className,
phone: member.phone,
email: member.email,
roleType: member.roleType || '队员', // 默认为队员
status: '2', // 设置为已加入状态
joinTime: new Date().toISOString().split('T')[0],
remark: member.remark
};
console.log(`批量审核通过 - ID ${id} 更新数据:`, updateData);
return updateVolunteerMember(updateData as any);
});
await Promise.all(promises);
proxy?.$modal.msgSuccess('批量审核通过成功');
await getList();
};
/** 批量拒绝 */
const handleBatchReject = async () => {
if (ids.value.length === 0) {
proxy?.$modal.msgWarning('请选择要审核的成员');
return;
}
await proxy?.$modal.confirm(`确认批量拒绝所选成员的入队申请?`);
const promises = ids.value.map(id => {
// 根据ID找到对应的成员记录,获取relationId
const member = memberList.value.find(m => m.id === id);
if (!member) {
proxy?.$modal.msgError(`未找到ID为${id}的成员记录`);
return Promise.reject(new Error(`未找到ID为${id}的成员记录`));
}
// 验证数据完整性
if (!validateMemberData(member, '批量审核拒绝')) {
return Promise.reject(new Error(`成员${member.studentName}数据不完整`));
}
// 构建完整的更新数据,包含所有必要字段
const updateData = {
id: id,
userId: member.userId, // 添加用户ID字段
memberType: 2, // 志愿者成员类型(必需字段)
relationId: member.relationId || props.teamId, // 优先使用行数据中的relationId,否则使用props中的teamId
studentNo: member.studentNo,
studentName: member.studentName,
gender: member.gender,
department: member.department,
major: member.major,
className: member.className,
phone: member.phone,
email: member.email,
roleType: '', // 拒绝审核后没有角色
status: '3', // 设置为已驳回状态
remark: member.remark
};
console.log(`批量审核拒绝 - ID ${id} 更新数据:`, updateData);
return updateVolunteerMember(updateData as any);
});
await Promise.all(promises);
proxy?.$modal.msgSuccess('批量审核拒绝成功');
await getList();
};
// 暴露给父组件的方法
defineExpose({
getList
});
onMounted(() => {
getList();
});
</script>
<style scoped>
.view-description {
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
min-height: 80px;
font-size: 14px;
color: #303133;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
/* 表格固定列样式优化 */
.pending-members-tab .el-table {
/* 移除固定最小宽度,让表格自适应容器 */
width: 100%;
}
.pending-members-tab .el-table__fixed-right {
/* 固定右侧列的样式 */
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
}
.pending-members-tab .el-table__fixed-right::before {
/* 固定列左侧边框 */
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1px;
background-color: #ebeef5;
}
/* 操作按钮组样式 */
.pending-members-tab .el-button + .el-button {
margin-left: 4px;
}
/* 操作列按钮样式,与JoinedMembersTab保持一致 */
.pending-members-tab .el-table .el-button {
padding: 4px 8px;
font-size: 14px;
}
.pending-members-tab .el-table .el-button--link {
height: 28px;
line-height: 1;
}
/* 确保固定列在滚动时保持可见 */
.pending-members-tab .el-table__body-wrapper {
overflow-x: auto;
}
/* 响应式表格布局 */
.pending-members-tab .el-card {
overflow: hidden;
}
.pending-members-tab .el-table {
/* 确保表格在卡片内正常显示 */
max-width: 100%;
}
/* 在小屏幕下优化列宽 */
@media (max-width: 1400px) {
.pending-members-tab .el-table .el-table__column {
min-width: auto;
}
}
</style>