This commit is contained in:
李旭明 committed 2025-09-09 11:35:35 +08:00
1 parent bb2a04ffb9
commit de3ed43b6d
63 files changed
+8724 -328

No files matched your search

+60
View File
@@ -44,6 +44,26 @@ export interface StglActivityVO {
*/
posterUrl?: string;
/**
* 关联部门ID(多个部门时用逗号分隔)
*/
relatedDeptId?: string | number;
/**
* 关联部门ID列表
*/
relatedDeptIds?: (string | number)[];
/**
* 关联部门名称(多个部门时用逗号分隔)
*/
relatedDeptName?: string;
/**
* 关联部门名称列表
*/
relatedDeptNames?: string[];
/**
* 关联社团ID
*/
@@ -272,6 +292,26 @@ export interface StglActivityForm {
*/
posterUrl?: string;
/**
* 关联部门ID(多个部门时用逗号分隔)
*/
relatedDeptId?: string | number;
/**
* 关联部门ID列表
*/
relatedDeptIds?: (string | number)[];
/**
* 关联部门对象(用于选择器绑定)
*/
relatedDept?: DeptOption | null;
/**
* 关联部门对象列表(多选时使用)
*/
relatedDepts?: DeptOption[] | null;
/**
* 关联社团ID
*/
@@ -454,6 +494,11 @@ export interface StglActivityQuery extends PageQuery {
*/
location?: string;
/**
* 关联部门ID
*/
relatedDeptId?: string | number;
/**
* 关联社团ID
*/
@@ -505,6 +550,21 @@ export interface StglActivityQuery extends PageQuery {
params?: any;
}
/**
* 部门选择器选项
*/
export interface DeptOption {
/**
* 部门ID
*/
deptId: string | number;
/**
* 部门名称
*/
deptName: string;
}
/**
* 社团/志愿者队伍选择器选项
*/
@@ -45,6 +45,26 @@ export interface StglActivityVo {
*/
posterUrl: string;
/**
* 关联部门ID(多个部门时用逗号分隔)
*/
relatedDeptId?: string | number;
/**
* 关联部门ID列表
*/
relatedDeptIds?: (string | number)[];
/**
* 关联部门名称(多个部门时用逗号分隔)
*/
relatedDeptName?: string;
/**
* 关联部门名称列表
*/
relatedDeptNames?: string[];
/**
* 关联社团ID
*/
+276
View File
@@ -0,0 +1,276 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
export interface StglClubAdvisorHistoryVO {
/**
* 主键ID
*/
id?: string | number;
/**
* 社团ID
*/
clubId?: string | number;
/**
* 社团名称
*/
clubName?: string;
/**
* 指导老师姓名
*/
advisorName?: string;
/**
* 指导老师部门
*/
advisorDept?: string;
/**
* 指导老师电话
*/
advisorPhone?: string;
/**
* 指导老师邮箱
*/
advisorEmail?: string;
/**
* 指导老师用户ID
*/
advisorUserId?: string | number;
/**
* 在任状态:1-当前在任,0-历史(已离任)
*/
status?: number;
/**
* 上任时间
*/
startTime?: string;
/**
* 离任时间
*/
endTime?: string;
/**
* 创建者
*/
createBy?: string | number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: string | number;
/**
* 更新时间
*/
updateTime?: string;
}
export interface StglClubAdvisorHistoryForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 社团ID
*/
clubId?: string | number;
/**
* 指导老师姓名
*/
advisorName?: string;
/**
* 指导老师部门
*/
advisorDept?: string;
/**
* 指导老师电话
*/
advisorPhone?: string;
/**
* 指导老师邮箱
*/
advisorEmail?: string;
/**
* 指导老师用户ID
*/
advisorUserId?: string | number;
/**
* 在任状态:1-当前在任,0-历史(已离任)
*/
status?: number;
/**
* 上任时间
*/
startTime?: string;
/**
* 离任时间
*/
endTime?: string;
}
export interface StglClubAdvisorHistoryQuery extends PageQuery {
/**
* 社团ID
*/
clubId?: string | number;
/**
* 指导老师姓名
*/
advisorName?: string;
/**
* 指导老师部门
*/
advisorDept?: string;
/**
* 指导老师电话
*/
advisorPhone?: string;
/**
* 指导老师邮箱
*/
advisorEmail?: string;
/**
* 指导老师用户ID
*/
advisorUserId?: string | number;
/**
* 在任状态:1-当前在任,0-历史(已离任)
*/
status?: number;
/**
* 上任时间
*/
startTime?: string;
/**
* 离任时间
*/
endTime?: string;
/**
* 日期范围参数
*/
params?: any;
}
/**
* 查询社团指导老师任职历史列表
* @param query
* @returns {*}
*/
export const listStglClubAdvisorHistory = (query?: StglClubAdvisorHistoryQuery): AxiosPromise<StglClubAdvisorHistoryVO[]> => {
return request({
url: '/stgl/club/advisor/history/list',
method: 'get',
params: query
});
};
/**
* 查询社团指导老师任职历史详细
* @param id
*/
export const getStglClubAdvisorHistory = (id: string | number): AxiosPromise<StglClubAdvisorHistoryVO> => {
return request({
url: '/stgl/club/advisor/history/' + id,
method: 'get'
});
};
/**
* 查询指定社团的当前在任指导老师
* @param clubId
*/
export const getCurrentAdvisors = (clubId: string | number): AxiosPromise<StglClubAdvisorHistoryVO[]> => {
return request({
url: '/stgl/club/advisor/history/current/' + clubId,
method: 'get'
});
};
/**
* 查询指定社团的历史指导老师(已离任)
* @param clubId
*/
export const getHistoryAdvisors = (clubId: string | number): AxiosPromise<StglClubAdvisorHistoryVO[]> => {
return request({
url: '/stgl/club/advisor/history/history/' + clubId,
method: 'get'
});
};
/**
* 查询指定社团的所有指导老师(包含当前和历史)
* @param clubId
*/
export const getAllAdvisors = (clubId: string | number): AxiosPromise<StglClubAdvisorHistoryVO[]> => {
return request({
url: '/stgl/club/advisor/history/all/' + clubId,
method: 'get'
});
};
/**
* 新增社团指导老师任职历史
* @param data
*/
export const addStglClubAdvisorHistory = (data: StglClubAdvisorHistoryForm) => {
return request({
url: '/stgl/club/advisor/history',
method: 'post',
data: data
});
};
/**
* 修改社团指导老师任职历史
* @param data
*/
export const updateStglClubAdvisorHistory = (data: StglClubAdvisorHistoryForm) => {
return request({
url: '/stgl/club/advisor/history',
method: 'put',
data: data
});
};
/**
* 删除社团指导老师任职历史
* @param id
*/
export const delStglClubAdvisorHistory = (id: string | number | Array<string | number>) => {
return request({
url: '/stgl/club/advisor/history/' + id,
method: 'delete'
});
};
+222
View File
@@ -0,0 +1,222 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StglClubAnnualReviewVO, StglClubAnnualReviewForm, StglClubAnnualReviewQuery } from './type';
/**
* 查询社团年审记录列表
* @param query
* @returns {*}
*/
export const listStglClubAnnualReview = (query?: StglClubAnnualReviewQuery): AxiosPromise<StglClubAnnualReviewVO[]> => {
return request({
url: '/stgl/club/annualReview/list',
method: 'get',
params: query,
headers: {
isEncrypt: 'true'
}
});
};
/**
* 查询我的社团年审记录列表
* @param query
* @returns {*}
*/
export const listMyStglClubAnnualReview = (query?: StglClubAnnualReviewQuery): AxiosPromise<StglClubAnnualReviewVO[]> => {
return request({
url: '/stgl/club/annualReview/myList',
method: 'get',
params: query,
headers: {
isEncrypt: 'true'
}
});
};
/**
* 查询社团年审记录详细
* @param id
*/
export const getStglClubAnnualReview = (id: string | number): AxiosPromise<StglClubAnnualReviewVO> => {
return request({
url: '/stgl/club/annualReview/' + id,
method: 'get',
headers: {
isEncrypt: 'true'
}
});
};
/**
* 新增社团年审记录
* @param data
*/
export const addStglClubAnnualReview = (data: StglClubAnnualReviewForm) => {
return request({
url: '/stgl/club/annualReview',
method: 'post',
data: data,
headers: {
isEncrypt: 'true'
}
});
};
/**
* 修改社团年审记录
* @param data
*/
export const updateStglClubAnnualReview = (data: StglClubAnnualReviewForm) => {
return request({
url: '/stgl/club/annualReview',
method: 'put',
data: data,
headers: {
isEncrypt: 'true'
}
});
};
/**
* 删除社团年审记录
* @param id
*/
export const delStglClubAnnualReview = (id: string | number | Array<string | number>) => {
return request({
url: '/stgl/club/annualReview/' + id,
method: 'delete',
headers: {
isEncrypt: 'true'
}
});
};
/**
* 提交年审记录
* @param id
*/
export const submitStglClubAnnualReview = (id: string | number) => {
return request({
url: '/stgl/club/annualReview/submit/' + id,
method: 'post',
headers: {
isEncrypt: 'true'
}
});
};
/**
* 撤回年审记录
* @param id
*/
export const withdrawStglClubAnnualReview = (id: string | number) => {
return request({
url: '/stgl/club/annualReview/withdraw/' + id,
method: 'post',
headers: {
isEncrypt: 'true'
}
});
};
/**
* 审核年审记录
* @param params
*/
export const auditStglClubAnnualReview = (params: {
id: string | number;
auditResult: string;
auditOpinion?: string;
auditType: string;
}) => {
return request({
url: '/stgl/club/annualReview/audit',
method: 'post',
params: params,
headers: {
isEncrypt: 'true'
}
});
};
/**
* 检查是否可以创建年审记录
* @param clubId
* @param reviewYear
*/
export const checkCanCreateStglClubAnnualReview = (clubId: string | number, reviewYear: number): AxiosPromise<boolean> => {
return request({
url: '/stgl/club/annualReview/check',
method: 'get',
params: {
clubId,
reviewYear
},
headers: {
isEncrypt: 'true'
}
});
};
/**
* 获取年审状态选项
*/
export const getAnnualReviewStatusOptions = (): AxiosPromise<string[]> => {
return request({
url: '/stgl/club/annualReview/statusOptions',
method: 'get',
headers: {
isEncrypt: 'true'
}
});
};
/**
* 导出社团年审记录
* @param query
*/
export const exportStglClubAnnualReview = (query?: StglClubAnnualReviewQuery) => {
return request({
url: '/stgl/club/annualReview/export',
method: 'post',
data: query,
responseType: 'blob',
headers: {
isEncrypt: 'true'
}
});
};
/**
* 根据社团ID和年审年度获取该年度的活动数据
* @param clubId 社团ID
* @param reviewYear 年审年度 (例如:2024)
*/
export const getClubActivitiesByReviewYear = (clubId: string | number, reviewYear: number) => {
return request({
url: '/stgl/club/annualReview/clubActivities',
method: 'get',
params: {
clubId,
reviewYear
},
headers: {
isEncrypt: 'true'
}
});
};
/**
* 获取年审记录的活动数据(从stgl_club_annual_review_activity表中获取)
* @param annualReviewId 年审记录ID
*/
export const getAnnualReviewActivities = (annualReviewId: string | number) => {
return request({
url: '/stgl/club/annualReview/' + annualReviewId + '/activities',
method: 'get',
headers: {
isEncrypt: 'true'
}
});
};
+323
View File
@@ -0,0 +1,323 @@
export interface StglClubAnnualReviewVO {
/**
* 主键ID
*/
id: string | number;
/**
* 社团ID
*/
clubId: string | number;
/**
* 年审年度
*/
reviewYear: number;
/**
* 年审开始日期
*/
reviewStartDate: string;
/**
* 年审结束日期
*/
reviewEndDate: string;
/**
* 年审时间
*/
reviewTime: string;
/**
* 社团名称
*/
clubName: string;
/**
* 成立日期
*/
establishDate: string;
/**
* 社长姓名
*/
presidentName: string;
/**
* 社团类型
*/
clubType: string;
/**
* 指导老师姓名
*/
advisorName: string;
/**
* 指导老师部门
*/
advisorDept: string;
/**
* 是否有章程
*/
hasCharter: string;
/**
* 章程文件URL
*/
charterUrl: string;
/**
* 是否有资金
*/
hasFunds: string;
/**
* 资金数额
*/
fundAmount: number;
/**
* 成员数量
*/
memberCount: number;
/**
* 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过)
*/
status: string;
/**
* 指导老师意见
*/
advisorOpinion: string;
/**
* 学院意见
*/
collegeOpinion: string;
/**
* 团委意见
*/
youthLeagueOpinion: string;
/**
* 创建者
*/
createBy: string | number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: string | number;
/**
* 更新时间
*/
updateTime: string;
}
export interface StglClubAnnualReviewForm {
/**
* 主键ID
*/
id?: string | number;
/**
* 社团ID
*/
clubId: string | number;
/**
* 年审年度
*/
reviewYear: number;
/**
* 年审开始日期
*/
reviewStartDate: string;
/**
* 年审结束日期
*/
reviewEndDate: string;
/**
* 年审时间
*/
reviewTime: string;
/**
* 社团名称
*/
clubName: string;
/**
* 成立日期
*/
establishDate?: string;
/**
* 社团类型
*/
clubType?: string;
/**
* 指导老师姓名
*/
advisorName?: string;
/**
* 指导老师部门
*/
advisorDept?: string;
/**
* 是否有章程
*/
hasCharter?: string;
/**
* 章程文件URL
*/
charterUrl?: string;
/**
* 是否有资金
*/
hasFunds?: string;
/**
* 资金数额
*/
fundAmount?: number;
/**
* 成员数量
*/
memberCount?: number;
/**
* 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过)
*/
status?: string;
/**
* 指导老师意见
*/
advisorOpinion?: string;
/**
* 学院意见
*/
collegeOpinion?: string;
/**
* 团委意见
*/
youthLeagueOpinion?: string;
/**
* 成员信息JSON
*/
membersJson?: string;
/**
* 活动信息JSON
*/
activitiesJson?: string;
}
export interface StglClubAnnualReviewQuery extends PageQuery {
/**
* 社团ID
*/
clubId?: string | number;
/**
* 年审年度
*/
reviewYear?: number;
/**
* 年审开始日期范围
*/
beginReviewStartDate?: string;
endReviewStartDate?: string;
/**
* 年审结束日期范围
*/
beginReviewEndDate?: string;
endReviewEndDate?: string;
/**
* 社团名称
*/
clubName?: string;
/**
* 社团类型
*/
clubType?: string;
/**
* 状态(0-草稿 1-已提交 2-审核中 3-审核通过 4-审核不通过)
*/
status?: string;
/**
* 其他查询参数
*/
params?: { [key: string]: any };
}
/**
* 分页查询接口
*/
export interface PageQuery {
/**
* 分页大小
*/
pageSize?: number;
/**
* 当前页数
*/
pageNum?: number;
/**
* 排序列
*/
orderByColumn?: string;
/**
* 排序的方向desc或者asc
*/
isAsc?: string;
}
/**
* 年审状态选项
*/
export const ANNUAL_REVIEW_STATUS_OPTIONS = [
{ label: '草稿', value: '0' },
{ label: '已提交', value: '1' },
{ label: '审核中', value: '2' },
{ label: '审核通过', value: '3' },
{ label: '审核不通过', value: '4' }
];
/**
* 是否选项
*/
export const YES_NO_OPTIONS = [
{ label: '是', value: 'Y' },
{ label: '否', value: 'N' }
];
+93
View File
@@ -0,0 +1,93 @@
import request from '@/utils/request'
// 社团活动权限检查结果类型定义
export interface ClubActivityPermissionResult {
canCreateActivity: boolean
status: 'NORMAL' | 'TEMPORARILY_LIMITED' | 'PERMANENTLY_LIMITED'
message: string
deadlineTime?: string
remainingHours?: number
remainingDays?: number
annualReviewStartTime?: string
}
// 权限详情类型定义
export interface ClubPermissionDetails {
permissionResult: ClubActivityPermissionResult
isInAnnualReviewWindow: boolean
daysUntilNextAnnualReview: number
hasSubmittedCurrentYearAnnualReview: boolean
}
/**
* 检查社团活动权限
* @param clubId 社团ID (支持字符串和数字类型,避免大整数精度丢失)
* @returns 权限检查结果
*/
export function checkClubActivityPermission(clubId: number | string) {
return request<ClubActivityPermissionResult>({
url: `/stgl/club/permission/checkActivityPermission/${clubId}`,
method: 'get'
})
}
/**
* 批量检查多个社团的权限状态
* @param clubIds 社团ID列表
* @returns 权限检查结果映射
*/
export function batchCheckClubPermission(clubIds: number[]) {
return request<Record<number, ClubActivityPermissionResult>>({
url: '/stgl/club/permission/batchCheckPermission',
method: 'post',
data: clubIds
})
}
/**
* 检查社团是否在年审窗口期内
* @param clubId 社团ID
* @returns 是否在年审窗口期
*/
export function isInAnnualReviewWindow(clubId: number) {
return request<boolean>({
url: `/stgl/club/permission/isInAnnualReviewWindow/${clubId}`,
method: 'get'
})
}
/**
* 获取社团距离下次年审的天数
* @param clubId 社团ID
* @returns 距离年审的天数
*/
export function getDaysUntilNextAnnualReview(clubId: number) {
return request<number>({
url: `/stgl/club/permission/getDaysUntilNextAnnualReview/${clubId}`,
method: 'get'
})
}
/**
* 检查社团是否已在当前年审期内提交年审
* @param clubId 社团ID
* @returns 是否已提交年审
*/
export function hasSubmittedCurrentYearAnnualReview(clubId: number) {
return request<boolean>({
url: `/stgl/club/permission/hasSubmittedCurrentYearAnnualReview/${clubId}`,
method: 'get'
})
}
/**
* 获取社团权限详细信息(综合接口)
* @param clubId 社团ID
* @returns 权限详细信息
*/
export function getClubPermissionDetails(clubId: number) {
return request<ClubPermissionDetails>({
url: `/stgl/club/permission/getPermissionDetails/${clubId}`,
method: 'get'
})
}
+4
View File
@@ -97,6 +97,10 @@ export interface MyActivityVO {
undertakerName?: string;
/** 关联类型(1-社团 2-志愿者队伍) */
relatedType?: number;
/** 关联部门名称 */
relatedDeptName?: string;
/** 关联部门名称列表 */
relatedDeptNames?: string;
/** 关联组织名称(社团名称或志愿者团队名称) */
organizationName?: string;
/** 指导老师 */
+15
View File
@@ -74,6 +74,21 @@ export interface StglClassVO {
* 参与活动数量
*/
activityCount: number;
/**
* 今日活动名称(审核通过且时间为今天的活动)
*/
todayActivityName?: string;
/**
* 今日活动开始时间
*/
todayActivityStartTime?: string;
/**
* 今日活动结束时间
*/
todayActivityEndTime?: string;
}
/**
@@ -0,0 +1,399 @@
<template>
<el-dialog
:model-value="visible"
:title="title"
width="1000px"
append-to-body
@update:model-value="handleClose"
>
<div class="advisor-history-container">
<!-- 当前指导老师 -->
<div class="current-advisors-section">
<h4 class="section-title">
<el-icon><User /></el-icon>
当前指导老师
</h4>
<div v-if="currentAdvisors.length === 0" class="no-data">
<el-empty description="暂无当前指导老师" :image-size="60" />
</div>
<div v-else class="advisor-cards">
<div
v-for="advisor in currentAdvisors"
:key="advisor.id"
class="advisor-card current"
>
<div class="advisor-header">
<span class="advisor-name">{{ advisor.advisorName }}</span>
<el-tag type="success" size="small">在任</el-tag>
</div>
<div class="advisor-info">
<div class="info-item">
<el-icon><OfficeBuilding /></el-icon>
<span>{{ advisor.advisorDept || '暂无' }}</span>
</div>
<div class="info-item">
<el-icon><Phone /></el-icon>
<span>{{ advisor.advisorPhone || '暂无' }}</span>
</div>
<div class="info-item">
<el-icon><Message /></el-icon>
<span>{{ advisor.advisorEmail || '暂无' }}</span>
</div>
<div class="info-item">
<el-icon><Calendar /></el-icon>
<span>上任时间:{{ formatDate(advisor.startTime) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 历史记录 -->
<div class="history-section">
<h4 class="section-title">
<el-icon><Clock /></el-icon>
历史记录
<span class="record-count">({{ historyAdvisors.length }}条)</span>
</h4>
<div v-if="historyAdvisors.length === 0" class="no-data">
<el-empty description="暂无历史记录" :image-size="60" />
</div>
<div v-else>
<el-table
:data="historyAdvisors"
style="width: 100%"
max-height="300"
stripe
>
<el-table-column label="姓名" prop="advisorName" width="180" />
<el-table-column label="部门" prop="advisorDept" min-width="120" show-overflow-tooltip />
<el-table-column label="联系方式" min-width="120">
<template #default="scope">
<div class="contact-info">
<div v-if="scope.row.advisorPhone" class="contact-item">
<el-icon><Phone /></el-icon>
{{ scope.row.advisorPhone }}
</div>
<div v-if="scope.row.advisorEmail" class="contact-item email">
<el-icon><Message /></el-icon>
{{ scope.row.advisorEmail }}
</div>
</div>
</template>
</el-table-column>
<el-table-column label="上任时间" width="100">
<template #default="scope">
{{ formatDate(scope.row.startTime) }}
</template>
</el-table-column>
<el-table-column label="离任时间" width="100">
<template #default="scope">
{{ formatDate(scope.row.endTime) }}
</template>
</el-table-column>
<el-table-column label="任期" width="130">
<template #default="scope">
{{ calculateDuration(scope.row.startTime, scope.row.endTime) }}
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关 闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { User, OfficeBuilding, Phone, Message, Calendar, Clock } from '@element-plus/icons-vue';
import { getCurrentAdvisors, getHistoryAdvisors } from '@/api/stgl/club/advisor-history';
import type { StglClubAdvisorHistoryVO } from '@/api/stgl/club/advisor-history';
interface Props {
visible: boolean;
title: string;
clubId?: string | number;
clubName?: string;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
title: '指导老师历史记录',
clubId: undefined,
clubName: ''
});
const emit = defineEmits<{
'update:visible': [value: boolean];
}>();
// 响应式数据
const currentAdvisors = ref<StglClubAdvisorHistoryVO[]>([]);
const historyAdvisors = ref<StglClubAdvisorHistoryVO[]>([]);
const loading = ref(false);
// 计算属性
const dialogTitle = computed(() => {
return props.clubName ? `${props.clubName} - ${props.title}` : props.title;
});
// 监听弹窗显示状态
watch(() => props.visible, (newVal) => {
if (newVal && props.clubId) {
loadAdvisorData();
}
});
// 监听社团ID变化
watch(() => props.clubId, (newVal) => {
if (newVal && props.visible) {
loadAdvisorData();
}
});
// 加载指导老师数据
const loadAdvisorData = async () => {
if (!props.clubId) {
ElMessage.warning('社团ID不能为空');
return;
}
loading.value = true;
try {
// 并行加载当前和历史指导老师数据
const [currentRes, historyRes] = await Promise.all([
getCurrentAdvisors(props.clubId),
getHistoryAdvisors(props.clubId)
]);
currentAdvisors.value = currentRes.data || [];
historyAdvisors.value = historyRes.data || [];
console.log('指导老师数据加载成功:', {
current: currentAdvisors.value.length,
history: historyAdvisors.value.length
});
} catch (error) {
console.error('加载指导老师数据失败:', error);
ElMessage.error('加载指导老师数据失败');
} finally {
loading.value = false;
}
};
// 关闭弹窗
const handleClose = () => {
emit('update:visible', false);
};
// 格式化日期
const formatDate = (dateStr?: string) => {
if (!dateStr) return '暂无';
const date = new Date(dateStr);
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
};
// 计算任期
const calculateDuration = (startTime?: string, endTime?: string) => {
if (!startTime || !endTime) return '暂无';
const start = new Date(startTime);
const end = new Date(endTime);
const diffTime = Math.abs(end.getTime() - start.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 30) {
return `${diffDays}天`;
} else if (diffDays < 365) {
const months = Math.floor(diffDays / 30);
const days = diffDays % 30;
return days > 0 ? `${months}个月${days}天` : `${months}个月`;
} else {
const years = Math.floor(diffDays / 365);
const months = Math.floor((diffDays % 365) / 30);
const days = (diffDays % 365) % 30;
let result = `${years}年`;
if (months > 0) result += `${months}个月`;
if (days > 0) result += `${days}天`;
return result;
}
};
</script>
<style scoped>
.advisor-history-container {
padding: 10px 0;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
border-bottom: 2px solid #409eff;
padding-bottom: 8px;
}
.record-count {
font-size: 14px;
font-weight: normal;
color: #909399;
}
.current-advisors-section {
margin-bottom: 30px;
}
.history-section {
margin-bottom: 20px;
}
.no-data {
text-align: center;
padding: 20px 0;
color: #909399;
}
/* 当前指导老师卡片样式 */
.advisor-cards {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.advisor-card {
flex: 1;
min-width: 300px;
padding: 16px;
border-radius: 8px;
border: 1px solid #e4e7ed;
background: #fff;
transition: all 0.3s ease;
}
.advisor-card.current {
border-color: #67c23a;
background: linear-gradient(135deg, #f0f9ff 0%, #f8fafc 100%);
}
.advisor-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.advisor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.advisor-name {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.advisor-info {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #606266;
}
.info-item .el-icon {
color: #909399;
font-size: 16px;
}
/* 历史记录表格样式 */
.contact-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.contact-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #606266;
}
.contact-item.email {
color: #409eff;
}
.contact-item .el-icon {
font-size: 12px;
color: #909399;
}
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 6px;
overflow: hidden;
width: 100% !important;
table-layout: auto;
}
:deep(.el-table th) {
background-color: #fafafa;
color: #606266;
font-weight: 600;
}
:deep(.el-table td) {
border-bottom: 1px solid #f0f0f0;
}
:deep(.el-table__body tr:hover > td) {
background-color: #f5f7fa;
}
/* 确保表格充满容器 */
:deep(.el-table__header-wrapper),
:deep(.el-table__body-wrapper) {
width: 100% !important;
}
:deep(.el-table__header),
:deep(.el-table__body) {
width: 100% !important;
table-layout: auto;
}
/* 响应式设计 */
@media (max-width: 768px) {
.advisor-cards {
flex-direction: column;
}
.advisor-card {
min-width: auto;
}
}
</style>
+75 -21
View File
@@ -64,10 +64,14 @@ export interface AdvisorVO {
dept: string;
email: string;
userId?: string;
startTime?: string; // 上任时间
endTime?: string; // 离任时间
}
interface Props {
modelValue: AdvisorVO[];
establishDate?: string; // 社团成立日期
isNewClub?: boolean; // 是否为新建社团
}
interface Emits {
@@ -78,42 +82,74 @@ interface Emits {
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const advisors = ref<AdvisorVO[]>([...props.modelValue]);
const advisors = ref<AdvisorVO[]>([]);
// 监听props变化
// 使用computed来避免双重监听导致的递归更新
const advisorsData = computed({
get: () => props.modelValue || [],
set: (newValue: AdvisorVO[]) => {
emit('update:modelValue', newValue);
emit('change', newValue);
}
});
// 初始化数据
advisors.value = advisorsData.value.map((item) => ({ ...item }));
// 只监听props变化来同步内部数据
watch(
() => props.modelValue,
(newValue) => {
advisors.value = newValue ? newValue.map((item) => ({ ...item })) : [];
console.log('AdvisorManager: props.modelValue 变化:', newValue);
if (newValue) {
advisors.value = newValue.map((item) => ({ ...item }));
console.log('AdvisorManager: 设置后的 advisors.value:', advisors.value);
} else {
advisors.value = [];
}
},
{ deep: true }
);
// 监听内部数据变化,同步到父组件
watch(
advisors,
(newValue) => {
emit('update:modelValue', newValue);
emit('change', newValue);
},
{ deep: true }
{ deep: true, immediate: true }
);
// 添加指导老师
const addAdvisor = () => {
advisors.value.push({
// 智能设置上任时间:
// 1. 如果是新建社团的首任指导老师且有社团成立日期,使用成立日期
// 2. 否则使用当前日期
let startTime = '';
const isFirstAdvisor = advisors.value.length === 0;
const isNewClub = props.isNewClub !== false; // 默认认为是新建社团,除非明确传入false
if (isFirstAdvisor && isNewClub && props.establishDate) {
startTime = props.establishDate;
console.log('AdvisorManager: 新建社团的首任指导老师,上任时间设置为社团成立日期:', startTime);
} else {
const today = new Date();
startTime = today.getFullYear() + '-' +
String(today.getMonth() + 1).padStart(2, '0') + '-' +
String(today.getDate()).padStart(2, '0');
console.log('AdvisorManager: 后续新增指导老师,上任时间设置为当前日期:', startTime);
}
const newAdvisors = [...advisors.value, {
teacher: null,
name: '',
phone: '',
dept: '',
email: '',
userId: ''
});
userId: '',
startTime: startTime,
endTime: undefined
}];
advisors.value = newAdvisors;
advisorsData.value = newAdvisors;
};
// 删除指导老师
const removeAdvisor = (index: number) => {
advisors.value.splice(index, 1);
const newAdvisors = advisors.value.filter((_, i) => i !== index);
advisors.value = newAdvisors;
advisorsData.value = newAdvisors;
};
// 监听teacher选择变化,自动填充相关信息
@@ -121,28 +157,46 @@ const handleTeacherChange = (advisor: AdvisorVO, teacher: TeacherVO | null) => {
console.log('handleTeacherChange called with teacher:', teacher);
if (teacher) {
// 自动填充老师信息
// 自动填充老师信息,保留时间字段
const originalStartTime = advisor.startTime;
const originalEndTime = advisor.endTime;
advisor.teacher = teacher;
advisor.name = teacher.nickName || '';
advisor.phone = teacher.phonenumber || '暂无电话';
advisor.dept = teacher.deptName || '';
advisor.email = teacher.email || '暂无邮箱';
advisor.userId = teacher.userId?.toString() || '';
advisor.startTime = originalStartTime; // 保留原有的上任时间
advisor.endTime = originalEndTime; // 保留原有的离任时间
console.log('Updated advisor:', {
name: advisor.name,
phone: advisor.phone,
dept: advisor.dept,
email: advisor.email,
userId: advisor.userId
userId: advisor.userId,
startTime: advisor.startTime,
endTime: advisor.endTime
});
} else {
// 清空选择时重置所有信息
// 清空选择时重置所有信息,但保留时间字段
const originalStartTime = advisor.startTime;
const originalEndTime = advisor.endTime;
advisor.teacher = null;
advisor.name = '';
advisor.phone = '';
advisor.dept = '';
advisor.email = '';
advisor.userId = '';
advisor.startTime = originalStartTime; // 保留原有的上任时间
advisor.endTime = originalEndTime; // 保留原有的离任时间
}
// 触发更新
const newAdvisors = [...advisors.value];
advisorsData.value = newAdvisors;
};
// 移除有问题的深度监听,改为在TeacherSelector的change事件中处理
@@ -0,0 +1,136 @@
<template>
<el-select
v-model="selectedDepts"
placeholder="请选择关联部门"
clearable
filterable
multiple
style="width: 100%; height: 32px;"
size="default"
@change="handleChange"
@clear="handleClear"
>
<el-option
v-for="dept in deptOptions"
:key="dept.deptId"
:label="dept.deptName"
:value="dept.deptId"
/>
</el-select>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { listDept } from '@/api/system/dept';
import { DeptVO } from '@/api/system/dept/types';
interface DeptOption {
deptId: string | number;
deptName: string;
}
interface Props {
modelValue?: DeptOption[] | null;
}
interface Emits {
(e: 'update:modelValue', value: DeptOption[] | null): void;
(e: 'change', value: DeptOption[] | null): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const selectedDepts = ref<(string | number)[]>([]);
const deptOptions = ref<DeptOption[]>([]);
// 获取部门列表数据
const loadDeptOptions = async () => {
try {
const response = await listDept({ parentId: 101 } as any);
if (response.data) {
// 过滤出parent_id=101的部门
const filteredDepts = response.data.filter((dept: DeptVO) => dept.parentId === 101);
deptOptions.value = filteredDepts.map((dept: DeptVO) => ({
deptId: dept.deptId,
deptName: dept.deptName
}));
}
} catch (error) {
console.error('获取部门列表失败:', error);
deptOptions.value = [];
}
};
// 处理选择变化
const handleChange = (values: (string | number)[]) => {
if (values && values.length > 0) {
const selectedOptions = values
.map(value => deptOptions.value.find(dept => dept.deptId === value))
.filter(Boolean) as DeptOption[];
emit('update:modelValue', selectedOptions);
emit('change', selectedOptions);
} else {
emit('update:modelValue', null);
emit('change', null);
}
};
// 处理清空
const handleClear = () => {
emit('update:modelValue', null);
emit('change', null);
};
// 监听外部传入的值变化
watch(
() => props.modelValue,
(newValue) => {
if (newValue && Array.isArray(newValue)) {
selectedDepts.value = newValue.map(dept => dept.deptId);
} else {
selectedDepts.value = [];
}
},
{ immediate: true }
);
onMounted(() => {
loadDeptOptions();
});
</script>
<style scoped>
/* 多选部门选择器样式 - 参考系统统一高度设置 */
:deep(.el-select) {
height: 32px !important;
}
:deep(.el-select .el-input__wrapper) {
min-height: 32px !important;
height: 32px !important;
}
/* 多选标签容器 - 参考系统设置 */
:deep(.el-select__tags) {
max-height: 28px !important;
overflow: hidden !important;
}
/* 标签样式 - 显示完整部门名称 */
:deep(.el-tag) {
height: 20px !important;
line-height: 18px !important;
margin: 4px 6px 4px 0 !important;
font-size: 12px !important;
white-space: nowrap !important;
display: inline-flex !important;
align-items: center !important;
}
/* 标签关闭按钮 */
:deep(.el-tag .el-tag__close) {
margin-left: 4px !important;
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<template>
<el-select
v-model="selectedDept"
placeholder="请选择关联部门"
clearable
filterable
style="width: 100%"
@change="handleChange"
@clear="handleClear"
>
<el-option
v-for="dept in deptOptions"
:key="dept.deptId"
:label="dept.deptName"
:value="dept.deptId"
/>
</el-select>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { listDept } from '@/api/system/dept';
import { DeptVO } from '@/api/system/dept/types';
interface DeptOption {
deptId: string | number;
deptName: string;
}
interface Props {
modelValue?: DeptOption | null;
}
interface Emits {
(e: 'update:modelValue', value: DeptOption | null): void;
(e: 'change', value: DeptOption | null): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const selectedDept = ref<string | number>();
const deptOptions = ref<DeptOption[]>([]);
// 获取部门列表数据
const loadDeptOptions = async () => {
try {
const response = await listDept({ parentId: 101 } as any);
if (response.data) {
// 过滤出parent_id=101的部门
const filteredDepts = response.data.filter((dept: DeptVO) => dept.parentId === 101);
deptOptions.value = filteredDepts.map((dept: DeptVO) => ({
deptId: dept.deptId,
deptName: dept.deptName
}));
}
} catch (error) {
console.error('获取部门列表失败:', error);
deptOptions.value = [];
}
};
// 处理选择变化
const handleChange = (value: string | number) => {
if (value) {
const selectedOption = deptOptions.value.find(dept => dept.deptId === value);
if (selectedOption) {
emit('update:modelValue', selectedOption);
emit('change', selectedOption);
}
} else {
emit('update:modelValue', null);
emit('change', null);
}
};
// 处理清空
const handleClear = () => {
selectedDept.value = undefined;
emit('update:modelValue', null);
emit('change', null);
};
// 监听外部传入的值变化
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
selectedDept.value = newValue.deptId;
} else {
selectedDept.value = undefined;
}
},
{ immediate: true }
);
onMounted(() => {
loadDeptOptions();
});
</script>
+17 -2
View File
@@ -38,7 +38,7 @@
<!-- </el-link> -->
<div class="ele-upload-list__item-content-action">
<el-button type="danger" v-if="!disabled" link @click="handleDelete(index)">删除</el-button>
<el-button type="danger" v-if="!disabled" link @click="handleDownloadAttachment(file)">下载</el-button>
<el-button type="primary" link @click="handleDownloadAttachment(file)">下载</el-button>
</div>
</li>
</transition-group>
@@ -155,7 +155,7 @@ const handleDownloadAttachment = async (file: any) => {
});
// 由于axios响应拦截器对blob类型响应直接返回blob数据
// 这里response直接就是blob对象,而不是包含data属性的响应对象
const blob = response;
const blob = response as unknown as Blob;
// 验证响应数据
if (!blob) {
@@ -306,4 +306,19 @@ const listToString = (list: any[], separator?: string) => {
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
.ele-upload-list__item-content-action {
margin-left: 15px;
}
.ele-upload-list__item-content-action .el-button--primary.is-link {
border: none !important;
padding: 0 !important;
}
.ele-upload-list__item-content-action .el-button--danger.is-link {
border: none !important;
padding: 0 !important;
margin-right: 10px;
}
</style>
@@ -157,10 +157,13 @@ const treeProps = {
watch(
() => props.modelValue,
(newValue) => {
console.log('TeacherSelector: props.modelValue 变化:', newValue);
if (newValue) {
displayValue.value = newValue.nickName;
console.log('TeacherSelector: 设置 displayValue 为:', displayValue.value);
} else {
displayValue.value = '';
console.log('TeacherSelector: 清空 displayValue');
}
},
{ immediate: true }
@@ -14,10 +14,24 @@
<el-descriptions-item label="活动地点">{{ activity.location || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ activity.startTime || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ activity.endTime || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="关联部门">
<div v-if="getRelatedDeptNames(activity).length > 0" class="dept-list">
<el-tag
v-for="(deptName, index) in getRelatedDeptNames(activity)"
:key="index"
type="info"
size="small"
class="dept-tag"
>
{{ deptName }}
</el-tag>
</div>
<span v-else>暂无</span>
</el-descriptions-item>
<el-descriptions-item label="关联社团/队伍">{{ activity.relatedClubName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="活动状态">
<dict-tag :options="stgl_activity_status" :value="activity.activityStatus" />
</el-descriptions-item>
<el-descriptions-item label="关联社团/队伍">{{ activity.relatedClubName || '暂无' }}</el-descriptions-item>
</el-descriptions>
<!-- 活动海报区域 -->
@@ -36,14 +50,17 @@
</div>
<!-- 主办方信息区域 -->
<!--
<el-divider content-position="left">主办方信息</el-divider>
<el-descriptions :column="3" border class="organizer-info-section custom-width-organizer">
<el-descriptions-item label="主办方名称" :label-style="{ width: '13%' }" :content-style="{ width: '35%' }">{{ activity.organizerName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="联系人" :label-style="{ width: '10%' }" :content-style="{ width: '15%' }">{{ activity.organizerContact || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="联系电话" :label-style="{ width: '10%' }" :content-style="{ width: '17%' }">{{ activity.organizerPhone || '暂无' }}</el-descriptions-item>
</el-descriptions>
-->
<!-- 承办方信息区域 -->
<!--
<el-divider content-position="left">承办方信息</el-divider>
<div v-if="coOrganizerList.length > 0">
<div v-for="(organizer, index) in coOrganizerList" :key="index" class="co-organizer-item custom-width-co-organizer">
@@ -55,8 +72,10 @@
</div>
</div>
<div v-else class="no-data">暂无承办方信息</div>
-->
<!-- 指导老师信息区域 -->
<!--
<el-divider content-position="left">指导老师信息</el-divider>
<div v-if="instructorList.length > 0">
<div v-for="(instructor, index) in instructorList" :key="index" class="instructor-item custom-width-instructor">
@@ -69,6 +88,7 @@
</div>
</div>
<div v-else class="no-data">暂无指导老师信息</div>
-->
</el-tab-pane>
<!-- 报名设置标签页 -->
@@ -235,6 +255,37 @@ const instructorList = computed<InstructorInfo[]>(() => {
}
});
// 获取关联部门名称列表
const getRelatedDeptNames = (activity: StglActivityVO | StglActivityVo): string[] => {
// 优先检查relatedDeptNames字段(多部门支持)
if ('relatedDeptNames' in activity && activity.relatedDeptNames) {
// 如果是数组类型,直接使用
if (Array.isArray(activity.relatedDeptNames)) {
return activity.relatedDeptNames.filter(name => name);
}
// 如果是字符串类型(逗号分隔),进行分割
const deptNamesStr = activity.relatedDeptNames as unknown as string;
if (typeof deptNamesStr === 'string' && deptNamesStr.includes(',')) {
return deptNamesStr.split(',').map(name => name.trim()).filter(name => name);
}
// 如果是单个字符串
if (typeof deptNamesStr === 'string' && deptNamesStr.trim()) {
return [deptNamesStr.trim()];
}
}
// 兼容旧的单部门字段
if (activity.relatedDeptName) {
if (activity.relatedDeptName.includes(',')) {
return activity.relatedDeptName.split(',').map(name => name.trim()).filter(name => name);
} else {
return [activity.relatedDeptName];
}
}
return [];
};
// 加载附件信息
const loadAttachments = async () => {
// 重置附件列表
@@ -543,6 +594,20 @@ onMounted(() => {
margin-bottom: 20px;
}
/* 部门列表显示样式 */
.dept-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.dept-tag {
margin: 0;
font-size: 12px;
border-radius: 4px;
}
/* 附件显示区域样式 */
.attachment-list {
display: flex;
@@ -641,3 +706,4 @@ onMounted(() => {
}
}
</style>
+87 -5
View File
@@ -18,8 +18,8 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="主办方" prop="organizerName">
<el-input v-model="queryParams.organizerName" placeholder="请输入主办方名称" clearable @keyup.enter="handleQuery" />
<el-form-item label="关联部门" prop="relatedDeptId">
<DeptSelector v-model="queryParams.relatedDept" @change="handleQueryRelatedDeptChange" />
</el-form-item>
</el-col>
<el-col :span="6">
@@ -61,7 +61,23 @@
<dict-tag :options="stgl_activity_type" :value="scope.row.activityType" />
</template>
</el-table-column>
<el-table-column label="主办方" align="center" prop="organizerName" min-width="150" show-overflow-tooltip />
<el-table-column label="关联部门" align="center" width="150" show-overflow-tooltip>
<template #default="scope">
<div v-if="getTableRelatedDeptNames(scope.row).length > 0" class="dept-list-table">
<el-tag
v-for="(deptName, index) in getTableRelatedDeptNames(scope.row)"
:key="index"
type="info"
size="small"
class="dept-tag-table"
:title="deptName"
>
{{ deptName }}
</el-tag>
</div>
<span v-else class="text-gray-400">未关联</span>
</template>
</el-table-column>
<el-table-column label="关联社团/队伍" align="center" min-width="150" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.relatedClubName">{{ scope.row.relatedClubName }}</span>
@@ -355,6 +371,7 @@ import type { StudentMsgForm } from '@/api/xg/xsjcxx/studentMsg/studentMsg/types
import { StudentVO } from '@/api/xg/student/types';
import { VicePresidentVO } from '@/components/VicePresidentManager/index.vue';
import { getUser } from '@/api/system/user';
import DeptSelector from '@/components/DeptSelector/index.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { stgl_activity_type, stgl_registration_type, stgl_registration_status } = toRefs<any>(
@@ -387,7 +404,7 @@ const viewForm = ref<StglActivityVo>({} as StglActivityVo);
// 用户报名状态缓存
const userRegistrationStatus = ref<Record<number, boolean>>({});
const data = reactive<PageData<StglActivityRegistrationForm, StglActivityQuery>>({
const data = reactive<PageData<StglActivityRegistrationForm, StglActivityQuery & { relatedDept?: any | null }>>({
form: {
activityId: undefined,
registrationType: undefined,
@@ -399,7 +416,8 @@ const data = reactive<PageData<StglActivityRegistrationForm, StglActivityQuery>>
pageSize: 10,
activityName: undefined,
activityType: undefined,
organizerName: undefined,
relatedDeptId: undefined,
relatedDept: null,
registrationType: undefined,
params: {}
},
@@ -506,10 +524,20 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
// 重置关联部门对象
queryParams.value.relatedDept = null;
queryFormRef.value?.resetFields();
handleQuery();
};
/** 关联部门搜索条件变化 */
const handleQueryRelatedDeptChange = (dept: any | null) => {
queryParams.value.relatedDept = dept;
queryParams.value.relatedDeptId = dept?.deptId;
queryParams.value.pageNum = 1;
getList();
};
/** 查看按钮操作 */
const handleView = async (row: StglActivityVo) => {
const res = await getActivityDetail(row.id);
@@ -799,6 +827,37 @@ const formatDate = (dateStr: string): string => {
});
};
/** 获取活动关联部门名称列表 */
const getTableRelatedDeptNames = (activity: StglActivityVo): string[] => {
// 优先检查relatedDeptNames字段(多部门支持)
if (activity.relatedDeptNames) {
// 如果是数组类型,直接使用
if (Array.isArray(activity.relatedDeptNames)) {
return activity.relatedDeptNames.filter(name => name);
}
// 如果是字符串类型(逗号分隔),进行分割
const deptNamesStr = activity.relatedDeptNames as unknown as string;
if (typeof deptNamesStr === 'string' && deptNamesStr.includes(',')) {
return deptNamesStr.split(',').map(name => name.trim()).filter(name => name);
}
// 如果是单个字符串
if (typeof deptNamesStr === 'string' && deptNamesStr.trim()) {
return [deptNamesStr.trim()];
}
}
// 兼容旧的单部门字段
if (activity.relatedDeptName) {
if (activity.relatedDeptName.includes(',')) {
return activity.relatedDeptName.split(',').map(name => name.trim()).filter(name => name);
} else {
return [activity.relatedDeptName];
}
}
return [];
};
onMounted(() => {
getList();
});
@@ -856,4 +915,27 @@ onMounted(() => {
margin-bottom: 0 !important;
}
}
/* 部门标签样式 */
.dept-list-table {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
align-items: center;
justify-content: center;
width: 100%;
}
.dept-tag-table {
margin: 0;
font-size: 12px;
line-height: 1.3;
padding: 2px 6px;
border-radius: 2px;
white-space: nowrap;
overflow: visible;
text-overflow: unset;
flex-shrink: 0;
max-width: none;
}
</style>
+388 -117
View File
@@ -33,18 +33,23 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-col :span="6" v-if="false">
<el-form-item label="主办方" prop="organizerName">
<el-input v-model="queryParams.organizerName" placeholder="请输入主办方名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-col :span="6" v-if="false">
<el-form-item label="是否已办" prop="isFinished">
<el-select v-model="queryParams.isFinished" placeholder="请选择是否已办" clearable style="width: 100%">
<el-option v-for="dict in sys_yes_no" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="关联部门" prop="relatedDeptId">
<DeptSelector v-model="queryParams.relatedDept" @change="handleQueryRelatedDeptChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="关联社团/队伍" prop="relatedClubId">
<ClubSelector v-model="queryParams.relatedClub" @change="handleQueryRelatedClubChange" />
@@ -78,11 +83,22 @@
</div>
</transition>
<el-card shadow="never" class="button-card table-card">
<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:activity:add']">新增</el-button>
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
:disabled="clubPermissionInfo && !clubPermissionInfo.canCreateActivity"
:loading="isLoadingPermission"
v-hasPermi="['stgl:activity:add']">
新增
</el-button>
</el-col>
<el-col :span="1.5" v-if="!single && selectedActivity && ['0', '7', '6'].includes(getOriginalActivityStatus(selectedActivity)) && hasActivityOperationPermission(selectedActivity)">
<el-button icon="Edit" @click="handleUpdate()" v-hasPermi="['stgl:activity:edit']">修改</el-button>
@@ -124,14 +140,31 @@
<dict-tag :options="stgl_activity_type" :value="scope.row.activityType" />
</template>
</el-table-column>
<el-table-column label="主办方" align="center" prop="organizerName" width="120" show-overflow-tooltip />
<el-table-column label="关联社团/队伍" align="center" width="120" show-overflow-tooltip>
<el-table-column label="主办方" align="center" prop="organizerName" width="120" show-overflow-tooltip v-if="false" />
<el-table-column label="关联部门" align="center" width="150" show-overflow-tooltip>
<template #default="scope">
<div v-if="getTableRelatedDeptNames(scope.row).length > 0" class="dept-list-table">
<el-tag
v-for="(deptName, index) in getTableRelatedDeptNames(scope.row)"
:key="index"
type="info"
size="small"
class="dept-tag-table"
:title="deptName"
>
{{ deptName }}
</el-tag>
</div>
<span v-else class="text-gray-400">未关联</span>
</template>
</el-table-column>
<el-table-column label="关联社团/队伍" align="center" width="180" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.relatedClubName">{{ scope.row.relatedClubName }}</span>
<span v-else class="text-gray-400">未关联</span>
</template>
</el-table-column>
<el-table-column label="指导老师" align="center" width="180" :show-overflow-tooltip="false">
<el-table-column label="指导老师" align="center" width="180" :show-overflow-tooltip="false" v-if="false">
<template #default="scope">
<div class="advisor-cell">
<span v-if="getAdvisorNames(scope.row).length === 0" class="no-advisor">暂无</span>
@@ -143,7 +176,7 @@
</div>
</template>
</el-table-column>
<el-table-column label="活动时间" align="center" width="160">
<el-table-column label="活动时间" align="center" width="140">
<template #default="scope">
<div v-if="scope.row.startTime && scope.row.endTime">
<div>{{ formatDate(scope.row.startTime) }}</div>
@@ -152,7 +185,7 @@
<span v-else class="text-gray-400">未设置</span>
</template>
</el-table-column>
<el-table-column label="报名时间" align="center" width="160">
<el-table-column label="报名时间" align="center" width="140">
<template #default="scope">
<div v-if="scope.row.registrationStart && scope.row.registrationEnd">
<div>{{ formatDate(scope.row.registrationStart) }}</div>
@@ -362,6 +395,15 @@
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="关联部门" prop="relatedDeptIds" required>
<MultiDeptSelector
v-model="form.relatedDepts"
@change="handleRelatedDeptsChange"
class="consistent-height-selector"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="关联社团/队伍" prop="relatedClubId">
<ClubSelector v-model="form.relatedClub" @change="handleRelatedClubChange" :disabled="isRelatedClubDisabled" />
@@ -376,6 +418,7 @@
</el-form-item>
<!-- 主办方信息 -->
<!--
<div class="section-divider">
<h4 class="section-title">主办方信息</h4>
</div>
@@ -398,14 +441,18 @@
</el-form-item>
</el-col>
</el-row>
-->
<!-- 承办方信息 -->
<!--
<div class="section-divider">
<h4 class="section-title">承办方信息</h4>
</div>
<CoOrganizerManager v-model="form.coOrganizerList" @change="handleCoOrganizersChange" />
-->
<!-- 指导老师信息 -->
<!--
<div class="section-divider">
<h4 class="section-title">指导老师信息</h4>
</div>
@@ -415,6 +462,7 @@
@change="handleAdvisorsChange"
:initial-advisors="form.advisors"
/>
-->
</el-tab-pane>
<el-tab-pane label="报名设置" name="registration">
@@ -606,7 +654,7 @@
</template>
<script setup name="StglActivity" lang="ts">
import { nextTick } from 'vue';
import { nextTick, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElNotification } from 'element-plus';
import {
@@ -619,8 +667,10 @@ import {
copyStglActivity,
summarizeActivity
} from '@/api/stgl/activity';
import { StglActivityVO, StglActivityQuery, StglActivityForm, ClubOption, CoOrganizerInfo, InstructorInfo } from '@/api/stgl/activity/types';
import { StglActivityVO, StglActivityQuery, StglActivityForm, ClubOption, DeptOption, CoOrganizerInfo, InstructorInfo } from '@/api/stgl/activity/types';
import ClubSelector from '@/components/ClubSelector/index.vue';
import DeptSelector from '@/components/DeptSelector/index.vue';
import MultiDeptSelector from '@/components/DeptSelector/MultiDeptSelector.vue';
import CoOrganizerManager from '@/components/CoOrganizerManager/index.vue';
import InstructorManager from '@/components/InstructorManager/index.vue';
import AdvisorManager from '@/components/AdvisorManager/index.vue';
@@ -630,6 +680,7 @@ import ActivityMembersDialog from './components/ActivityMembersDialog.vue';
import ActivityRegistrationView from './components/ActivityRegistrationView.vue'; // Added import
import { AdvisorVO } from '@/components/AdvisorManager/index.vue';
import { useUserStore } from '@/store/modules/user';
import { checkClubActivityPermission, type ClubActivityPermissionResult } from '@/api/stgl/clubPermission';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
@@ -652,6 +703,10 @@ const dateRange = ref<[string, string]>();
const selectedActivity = ref<StglActivityVO | null>(null);
const formKey = ref(0);
// 社团权限相关
const clubPermissionInfo = ref<ClubActivityPermissionResult | null>(null);
const isLoadingPermission = ref(false);
const queryFormRef = ref<ElFormInstance>();
const activityFormRef = ref<ElFormInstance>();
const copyFormRef = ref<ElFormInstance>();
@@ -715,6 +770,10 @@ const initFormData: StglActivityForm = {
location: undefined,
description: undefined,
posterUrl: undefined,
relatedDeptId: undefined,
relatedDeptIds: [],
relatedDept: null,
relatedDepts: null,
relatedClubId: undefined,
relatedType: undefined,
relatedClub: null,
@@ -727,7 +786,7 @@ const initFormData: StglActivityForm = {
instructorList: [],
instructors: '[]',
advisors: [],
registrationType: undefined,
registrationType: '1', // 默认为个人报名
registrationStart: undefined,
registrationEnd: undefined,
registrationStatus: undefined,
@@ -736,7 +795,7 @@ const initFormData: StglActivityForm = {
teamSizeMin: undefined,
teamSizeMax: undefined,
needApproval: 1,
activityRange: 1, // 默认为1-本组织成员
activityRange: 0, // 默认为无限制
activityStatus: '0',
isFinished: 0,
summary: undefined,
@@ -747,7 +806,7 @@ const initFormData: StglActivityForm = {
remark: undefined
};
const data = reactive<PageData<StglActivityForm, StglActivityQuery & { relatedClub?: ClubOption | null }>>({
const data = reactive<PageData<StglActivityForm, StglActivityQuery & { relatedClub?: ClubOption | null; relatedDept?: DeptOption | null }>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
@@ -758,6 +817,8 @@ const data = reactive<PageData<StglActivityForm, StglActivityQuery & { relatedCl
startTime: undefined,
endTime: undefined,
location: undefined,
relatedDeptId: undefined,
relatedDept: null,
relatedClubId: undefined,
relatedClub: null,
organizerName: undefined,
@@ -777,10 +838,23 @@ const data = reactive<PageData<StglActivityForm, StglActivityQuery & { relatedCl
activityType: [{ required: true, message: '请选择活动类型', trigger: 'change' }],
startTime: [{ required: true, message: '请选择活动开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择活动结束时间', trigger: 'change' }],
organizerName: [
{ required: true, message: '主办方名称不能为空', trigger: 'blur' },
{ min: 2, max: 50, message: '主办方名称长度在2到50个字符', trigger: 'blur' }
relatedDeptIds: [
{
required: true,
validator: (rule: any, value: any, callback: any) => {
if (!value || !Array.isArray(value) || value.length === 0) {
callback(new Error('请选择关联部门'));
} else {
callback();
}
},
trigger: 'change'
}
],
// organizerName: [
// { required: true, message: '主办方名称不能为空', trigger: 'blur' },
// { min: 2, max: 50, message: '主办方名称长度在2到50个字符', trigger: 'blur' }
// ],
registrationType: [{ required: true, message: '请选择报名方式', trigger: 'change' }],
activityRange: [{ required: true, message: '请选择报名范围', trigger: 'change' }]
}
@@ -806,11 +880,7 @@ const initUrlParams = () => {
uniqueKey: `1-${relatedClubId}`
};
console.log('初始化URL参数 - 社团过滤:', {
relatedClubId,
relatedClubName,
queryParams: queryParams.value.relatedClub
});
} else if (relatedTeamId && relatedTeamName && from === 'team') {
// 设置志愿者队伍查询参数
queryParams.value.relatedClubId = relatedTeamId as string | number;
@@ -821,11 +891,7 @@ const initUrlParams = () => {
uniqueKey: `2-${relatedTeamId}`
};
console.log('初始化URL参数 - 志愿者队伍过滤:', {
relatedTeamId,
relatedTeamName,
queryParams: queryParams.value.relatedClub
});
}
};
@@ -897,6 +963,11 @@ const reset = () => {
form.value.advisors = [];
form.value.instructorList = [];
form.value.instructors = '[]';
// 确保关联部门字段被正确初始化
form.value.relatedDept = null;
form.value.relatedDepts = null;
form.value.relatedDeptId = undefined;
form.value.relatedDeptIds = [];
// 重置关联社团禁用状态
isRelatedClubDisabled.value = false;
@@ -912,11 +983,7 @@ const reset = () => {
activeTab.value = 'basic';
console.log('表单重置后的状态:', {
advisors: form.value.advisors,
instructorList: form.value.instructorList,
instructors: form.value.instructors
});
// 延迟重置表单字段,避免递归更新
setTimeout(() => {
@@ -933,6 +1000,9 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
dateRange.value = undefined;
// 重置关联部门和关联社团对象
queryParams.value.relatedDept = null;
queryParams.value.relatedClub = null;
queryFormRef.value?.resetFields();
handleQuery();
};
@@ -945,8 +1015,57 @@ const handleSelectionChange = (selection: StglActivityVO[]) => {
selectedActivity.value = selection.length === 1 ? selection[0] : null;
};
/** 检查社团活动权限 */
const checkClubPermission = async (clubId: number | string) => {
if (!clubId) {
return null;
}
try {
isLoadingPermission.value = true;
const response = await checkClubActivityPermission(clubId);
clubPermissionInfo.value = response.data;
return response.data;
} catch (error) {
console.error('检查社团权限失败:', error);
proxy?.$modal.msgError('检查社团权限失败');
return null;
} finally {
isLoadingPermission.value = false;
}
};
/** 跳转到年审页面 */
const goToAnnualReview = () => {
if (queryParams.value.relatedClub?.relatedClubId) {
router.push({
path: '/stgl/club/info/annualReview',
query: { clubId: queryParams.value.relatedClub.relatedClubId }
});
}
};
/** 新增按钮操作 */
const handleAdd = () => {
const handleAdd = async () => {
// 如果有关联社团,先检查权限
if (queryParams.value.relatedClub?.relatedClubId && queryParams.value.relatedClub?.relatedType === 1) {
// 避免使用Number()导致的精度丢失,直接使用字符串形式的ID
const clubId = queryParams.value.relatedClub.relatedClubId;
const permission = await checkClubPermission(clubId);
if (permission && !permission.canCreateActivity) {
// 权限不足,显示提示信息
if (permission.status === 'TEMPORARILY_LIMITED') {
proxy?.$modal.msgError(`您的社团当前无法发起活动:${permission.message}`);
} else if (permission.status === 'PERMANENTLY_LIMITED') {
proxy?.$modal.msgError(`您的社团已被永久限制发起活动:${permission.message}`);
}
return; // 停止新增操作
}
}
// 权限检查通过,继续原有的新增逻辑
// 先重置表单
form.value = { ...initFormData };
activeTab.value = 'basic';
@@ -956,16 +1075,13 @@ const handleAdd = () => {
form.value.instructorList = [];
form.value.instructors = '[]';
// 自动填充报名开始时间为当前时间
const now = new Date();
form.value.registrationStart = now.toISOString().slice(0, 19).replace('T', ' ');
// 更新formKey以强制重新渲染组件
formKey.value++;
console.log('新增活动 - 初始化表单数据:', {
advisors: form.value.advisors,
instructorList: form.value.instructorList,
instructors: form.value.instructors,
formKey: formKey.value
});
// 如果有查询条件中的关联组织(社团或志愿者队伍),自动填充关联信息
if (queryParams.value.relatedClub) {
form.value.relatedClub = queryParams.value.relatedClub;
@@ -974,19 +1090,6 @@ const handleAdd = () => {
// 设置关联社团字段为禁用状态(不允许修改)
isRelatedClubDisabled.value = true;
console.log('新增活动 - 自动填充关联组织信息:', {
relatedType: queryParams.value.relatedClub.relatedType,
relatedClubId: queryParams.value.relatedClub.relatedClubId,
relatedClubName: queryParams.value.relatedClub.relatedClubName
});
// 确保表单数据正确设置
console.log('新增活动 - 表单数据设置后:', {
formRelatedClub: form.value.relatedClub,
formRelatedClubId: form.value.relatedClubId,
formRelatedType: form.value.relatedType
});
} else {
// 没有关联组织时,允许编辑关联社团字段
isRelatedClubDisabled.value = false;
@@ -995,14 +1098,7 @@ const handleAdd = () => {
dialog.visible = true;
dialog.title = '添加活动信息';
// 确保对话框完全打开后再检查表单状态
nextTick(() => {
console.log('新增活动 - 对话框打开后的表单状态:', {
advisors: form.value.advisors,
instructorList: form.value.instructorList,
instructors: form.value.instructors
});
});
};
/** 修改按钮操作 */
@@ -1022,11 +1118,51 @@ const handleUpdate = async (row?: StglActivityVO) => {
return;
}
console.log('handleUpdate - 获取的活动数据:', res.data);
console.log('handleUpdate - 附件字段:', {
summaryAttachments: res.data.summaryAttachments,
achievementsAttachments: res.data.achievementsAttachments
});
// 处理关联部门数据
if (res.data.relatedDeptIds && res.data.relatedDeptNames) {
// 优先使用新的多选部门字段
try {
const deptIds: number[] = JSON.parse(res.data.relatedDeptIds as unknown as string);
const deptNames: string[] = (res.data.relatedDeptNames as unknown as string).split(',').map(name => name.trim());
(res.data as any).relatedDepts = deptIds.map((deptId: number, index: number) => ({
deptId: deptId,
deptName: deptNames[index] || `部门${index + 1}`
}));
(res.data as any).relatedDeptIds = deptIds;
} catch (error) {
console.error('解析多选部门数据失败:', error);
// 降级到单选模式
if (res.data.relatedDeptId && res.data.relatedDeptName) {
(res.data as any).relatedDepts = [{
deptId: res.data.relatedDeptId,
deptName: res.data.relatedDeptName
}];
(res.data as any).relatedDeptIds = [res.data.relatedDeptId];
}
}
} else if (res.data.relatedDeptId && res.data.relatedDeptName) {
// 兼容旧的单选部门数据
(res.data as any).relatedDepts = [{
deptId: res.data.relatedDeptId,
deptName: res.data.relatedDeptName
}];
(res.data as any).relatedDeptIds = [res.data.relatedDeptId];
}
// 保持向后兼容
if (res.data.relatedDeptId && res.data.relatedDeptName) {
(res.data as any).relatedDept = {
deptId: res.data.relatedDeptId,
deptName: res.data.relatedDeptName
};
}
// 处理关联社团数据
if (res.data.relatedClubId && res.data.relatedClubName) {
@@ -1073,7 +1209,6 @@ const handleUpdate = async (row?: StglActivityVO) => {
if (res.data.instructors) {
try {
const instructorsData = JSON.parse(res.data.instructors);
console.log('解析的指导老师数据:', instructorsData);
// 确保数据格式正确
if (Array.isArray(instructorsData)) {
@@ -1104,13 +1239,7 @@ const handleUpdate = async (row?: StglActivityVO) => {
form.value.registrationType = String(form.value.registrationType);
}
// 确保指导老师数据正确设置
console.log('修改活动 - 设置表单数据后的状态:', {
advisors: form.value.advisors,
instructorList: form.value.instructorList,
instructors: form.value.instructors,
registrationType: form.value.registrationType
});
// 更新formKey以强制重新渲染组件
formKey.value++;
@@ -1201,18 +1330,44 @@ const submitForm = () => {
// 准备提交的数据
const submitData = { ...form.value } as any;
console.log('提交前 - 原始表单数据:', {
advisors: submitData.advisors,
instructorList: submitData.instructorList,
instructors: submitData.instructors
});
// 新增时自动设置活动状态为0(草稿)
if (!submitData.id) {
submitData.activityStatus = '0';
}
// 处理关联部门数据
if (submitData.relatedDepts && submitData.relatedDepts.length > 0) {
// 多选部门:将ID列表转换为JSON格式字符串传递给后端
const deptIds = submitData.relatedDepts.map(dept => dept.deptId);
submitData.relatedDeptIds = JSON.stringify(deptIds);
// 将部门名称用逗号连接
submitData.relatedDeptNames = submitData.relatedDepts.map(dept => dept.deptName).join(',');
// 设置第一个部门作为主要关联部门(保持兼容性)
submitData.relatedDeptId = deptIds[0];
submitData.relatedDeptName = submitData.relatedDepts[0].deptName;
submitData.relatedDept = submitData.relatedDepts[0];
} else if (submitData.relatedDept) {
// 单选部门的情况(向后兼容)
submitData.relatedDeptId = submitData.relatedDept.deptId;
submitData.relatedDeptName = submitData.relatedDept.deptName;
// 单选时也设置多选字段
submitData.relatedDeptIds = JSON.stringify([submitData.relatedDept.deptId]);
submitData.relatedDeptNames = submitData.relatedDept.deptName;
} else {
// 清空关联部门数据
submitData.relatedDeptId = null;
submitData.relatedDeptName = null;
submitData.relatedDeptIds = null;
submitData.relatedDeptNames = null;
}
// 处理关联社团数据
if (submitData.relatedClub) {
submitData.relatedClubId = submitData.relatedClub.relatedClubId;
@@ -1223,12 +1378,7 @@ const submitForm = () => {
submitData.relatedType = null;
}
// 调试信息
console.log('提交的关联社团数据:', {
relatedClub: submitData.relatedClub,
relatedClubId: submitData.relatedClubId,
relatedType: submitData.relatedType
});
// 处理承办方数据
if (submitData.coOrganizerList && submitData.coOrganizerList.length > 0) {
@@ -1237,6 +1387,18 @@ const submitForm = () => {
submitData.coOrganizers = '[]'; // 修复:使用空JSON数组而不是undefined
}
// 处理主办方数据(由于前端已屏蔽主办方字段显示,设置默认值)
if (!submitData.organizerName) {
// 如果有关联部门,使用第一个部门名称作为主办方
if (submitData.relatedDepts && submitData.relatedDepts.length > 0) {
submitData.organizerName = submitData.relatedDepts[0].deptName;
} else if (submitData.relatedClub && submitData.relatedClub.relatedClubName) {
submitData.organizerName = submitData.relatedClub.relatedClubName;
} else {
submitData.organizerName = '学校'; // 默认主办方
}
}
// 处理指导老师数据
if (submitData.advisors && submitData.advisors.length > 0) {
submitData.instructors = JSON.stringify(submitData.advisors);
@@ -1313,11 +1475,6 @@ const handleDelete = async (row?: StglActivityVO) => {
/** 查看按钮操作 */
const handleView = async (row: StglActivityVO) => {
const res = await getStglActivity(row.id);
console.log('handleView - 获取的活动数据:', res.data);
console.log('handleView - 附件字段:', {
summaryAttachments: res.data.summaryAttachments,
achievementsAttachments: res.data.achievementsAttachments
});
viewForm.value = res.data;
viewDialog.visible = true;
viewDialog.title = '查看活动信息';
@@ -1372,12 +1529,6 @@ const handleSummary = async (row?: StglActivityVO) => {
const res = await getStglActivity(activity.id);
const fullActivity = res.data;
console.log('handleSummary - 完整活动数据:', fullActivity);
console.log('handleSummary - 附件字段:', {
summaryAttachments: fullActivity.summaryAttachments,
achievementsAttachments: fullActivity.achievementsAttachments
});
summaryForm.id = fullActivity.id;
summaryForm.activityName = fullActivity.activityName || '';
summaryForm.summary = fullActivity.summary || '';
@@ -1385,11 +1536,6 @@ const handleSummary = async (row?: StglActivityVO) => {
summaryForm.summaryAttachments = fullActivity.summaryAttachments || '';
summaryForm.achievementsAttachments = fullActivity.achievementsAttachments || '';
console.log('handleSummary - 设置到表单的数据:', {
summaryAttachments: summaryForm.summaryAttachments,
achievementsAttachments: summaryForm.achievementsAttachments
});
summaryDialog.visible = true;
};
@@ -1407,14 +1553,6 @@ const resetSummaryForm = () => {
const confirmSummary = async () => {
summaryLoading.value = true;
try {
console.log('confirmSummary - 提交的数据:', {
id: summaryForm.id,
summary: summaryForm.summary,
achievements: summaryForm.achievements,
summaryAttachments: summaryForm.summaryAttachments,
achievementsAttachments: summaryForm.achievementsAttachments
});
await summarizeActivity({
id: summaryForm.id!,
summary: summaryForm.summary,
@@ -1453,18 +1591,39 @@ const handleExport = async () => {
}
};
/** 关联部门变化处理 */
const handleRelatedDeptChange = (dept: DeptOption | null) => {
form.value.relatedDept = dept;
form.value.relatedDeptId = dept?.deptId;
};
/** 关联部门多选变化处理 */
const handleRelatedDeptsChange = (depts: DeptOption[] | null) => {
form.value.relatedDepts = depts;
if (depts && depts.length > 0) {
form.value.relatedDeptIds = depts.map(dept => dept.deptId);
// 将第一个部门设置为主要关联部门(兼容现有逻辑)
form.value.relatedDeptId = depts[0].deptId;
form.value.relatedDept = depts[0];
} else {
form.value.relatedDeptIds = [];
form.value.relatedDeptId = undefined;
form.value.relatedDept = null;
}
};
/** 关联社团变化处理 */
const handleRelatedClubChange = (club: ClubOption | null) => {
console.log('活动表单 - handleRelatedClubChange:', club);
form.value.relatedClub = club;
form.value.relatedClubId = club?.relatedClubId;
form.value.relatedType = club?.relatedType; // 修复:同时设置关联类型
console.log('活动表单 - 更新后的表单数据:', {
relatedClub: form.value.relatedClub,
relatedClubId: form.value.relatedClubId,
relatedType: form.value.relatedType
});
};
/** 查询关联部门变化处理 */
const handleQueryRelatedDeptChange = (dept: DeptOption | null) => {
queryParams.value.relatedDept = dept;
queryParams.value.relatedDeptId = dept?.deptId;
};
/** 查询关联社团变化处理 */
@@ -1651,6 +1810,37 @@ const formatDate = (dateStr: string): string => {
});
};
/** 获取表格中关联部门名称列表 */
const getTableRelatedDeptNames = (activity: StglActivityVO): string[] => {
// 优先检查relatedDeptNames字段(多部门支持)
if (activity.relatedDeptNames) {
// 如果是数组类型,直接使用
if (Array.isArray(activity.relatedDeptNames)) {
return activity.relatedDeptNames.filter(name => name);
}
// 如果是字符串类型(逗号分隔),进行分割
const deptNamesStr = activity.relatedDeptNames as unknown as string;
if (typeof deptNamesStr === 'string' && deptNamesStr.includes(',')) {
return deptNamesStr.split(',').map(name => name.trim()).filter(name => name);
}
// 如果是单个字符串
if (typeof deptNamesStr === 'string' && deptNamesStr.trim()) {
return [deptNamesStr.trim()];
}
}
// 兼容旧的单部门字段
if (activity.relatedDeptName) {
if (activity.relatedDeptName.includes(',')) {
return activity.relatedDeptName.split(',').map(name => name.trim()).filter(name => name);
} else {
return [activity.relatedDeptName];
}
}
return [];
};
/** 获取指导老师姓名列表 */
const getAdvisorNames = (activity: StglActivityVO): string[] => {
// 优先从JSON字段读取指导老师数据
@@ -1665,7 +1855,6 @@ const getAdvisorNames = (activity: StglActivityVO): string[] => {
}
}
console.log('getAdvisorNames: 没有找到指导老师数据,activity:', activity);
return [];
};
@@ -1726,10 +1915,54 @@ const getOriginalActivityStatus = (activity: StglActivityVO | null): string => {
return activity.originalActivityStatus || activity.activityStatus || '';
};
// 监听活动开始时间变化,自动更新报名结束时间
watch(
() => form.value.startTime,
(newStartTime) => {
// 只有在新增或修改模式下,且有开始时间时才自动填充
if (newStartTime && dialog.visible) {
// 使用nextTick确保异步更新
nextTick(() => {
// 确保时间格式正确,保留完整的日期和时间信息
if (typeof newStartTime === 'string' && newStartTime.includes(':')) {
// 字符串格式且包含时间部分,直接赋值
form.value.registrationEnd = newStartTime;
} else if (typeof newStartTime === 'string') {
// 字符串格式但可能只有日期部分,添加默认时间
form.value.registrationEnd = newStartTime.includes(' ') ? newStartTime : `${newStartTime} 00:00:00`;
} else {
// 如果是Date对象或其他格式,转换为YYYY-MM-DD HH:mm:ss格式
const date = new Date(newStartTime);
if (!isNaN(date.getTime())) {
form.value.registrationEnd = date.toISOString().slice(0, 19).replace('T', ' ');
}
}
});
}
}
);
onMounted(() => {
initUrlParams();
getList();
// 如果有关联社团,检查权限
if (queryParams.value.relatedClub?.relatedClubId && queryParams.value.relatedClub?.relatedType === 1) {
checkClubPermission(Number(queryParams.value.relatedClub.relatedClubId));
}
});
// 监听路由查询参数变化,重新检查权限
watch(
() => queryParams.value.relatedClub,
(newRelatedClub) => {
if (newRelatedClub?.relatedClubId && newRelatedClub.relatedType === 1) {
checkClubPermission(Number(newRelatedClub.relatedClubId));
} else {
clubPermissionInfo.value = null;
}
},
{ deep: true }
);
</script>
<style scoped>
@@ -1808,6 +2041,29 @@ onMounted(() => {
font-weight: 500;
}
/* 部门列表表格样式 */
.dept-list-table {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
align-items: center;
justify-content: center;
width: 100%;
}
.dept-tag-table {
margin: 0;
font-size: 12px;
line-height: 1.3;
padding: 2px 6px;
border-radius: 2px;
white-space: nowrap;
overflow: visible;
text-overflow: unset;
flex-shrink: 0;
max-width: none;
}
/* 指导老师单元格样式 */
.advisor-cell {
padding: 4px 8px;
@@ -1845,5 +2101,20 @@ onMounted(() => {
}
}
/* 确保关联部门选择器与其他字段高度一致 */
.consistent-height-selector :deep(.el-select) {
height: 32px !important;
}
.consistent-height-selector :deep(.el-select .el-input__wrapper) {
min-height: 32px !important;
height: 32px !important;
}
.consistent-height-selector :deep(.el-select__tags) {
max-height: 28px !important;
overflow: hidden !important;
}
</style>
@@ -0,0 +1,527 @@
<template>
<div class="annual-review-detail">
<div v-loading="loading">
<div v-if="annualReview">
<el-form ref="formRef" :model="annualReview" label-width="120px">
<!-- 年审表头信息 -->
<div class="form-header">
<h3 class="form-title">杭州第一技师学院社团资格年审表</h3>
<div class="year-range">{{ yearRange }} 年</div>
<div class="review-date">年审时间:{{ reviewDate }}</div>
</div>
<!-- 基本信息区域 -->
<div class="form-section">
<h4 class="section-title">基本信息</h4>
<div class="basic-info-table">
<!-- 第一行:社团名称 | 成立时间 -->
<div class="info-row">
<div class="info-item">
<div class="info-label">社团名称</div>
<div class="info-value">{{ annualReview.clubName }}</div>
</div>
<div class="info-item">
<div class="info-label">成立时间</div>
<div class="info-value">{{ parseTime(annualReview.establishDate, '{y}-{m}-{d}') }}</div>
</div>
</div>
<!-- 第二行:指导部门 | 社团类别 -->
<div class="info-row">
<div class="info-item">
<div class="info-label">指导部门</div>
<div class="info-value">{{ annualReview.advisorDept }}</div>
</div>
<div class="info-item">
<div class="info-label">社团类别</div>
<div class="info-value">
<dict-tag :options="stgl_club_type" :value="annualReview.clubType" />
</div>
</div>
</div>
<!-- 第三行:指导老师 | 成员 -->
<div class="info-row">
<div class="info-item">
<div class="info-label">指导老师</div>
<div class="info-value">{{ annualReview.advisorName }}</div>
</div>
<div class="info-item">
<div class="info-label">成员</div>
<div class="info-value">{{ memberCountText }}</div>
</div>
</div>
<!-- 第四行:有无章程 | 社团金额 -->
<div class="info-row">
<div class="info-item">
<div class="info-label">有无章程</div>
<div class="info-value">
<span v-if="annualReview.hasCharter === 'Y'" class="charter-status">
有
<div class="charter-info">
<file-upload
v-model="annualReview.charterUrl"
:disabled="true"
:is-show-tip="false"
:limit="1"
:file-type="['pdf', 'doc', 'docx']"
:file-size="10"
/>
</div>
</span>
<span v-else class="charter-status">无</span>
</div>
</div>
<div class="info-item">
<div class="info-label">社团金额</div>
<div class="info-value">
<span v-if="annualReview.hasFunds === 'Y'">
有
<span v-if="annualReview.fundAmount" style="margin-left: 10px;">
{{ annualReview.fundAmount }}元
</span>
</span>
<span v-else>无</span>
</div>
</div>
</div>
</div>
</div>
<!-- 社团主要成员 -->
<div class="form-section">
<h4 class="section-title">社团主要成员</h4>
<el-table :data="memberList" border style="width: 100%" max-height="300" class="member-table">
<el-table-column label="姓名" prop="name" width="100" align="center" header-align="center" />
<el-table-column label="班级" prop="className" min-width="140" align="center" header-align="center" />
<el-table-column label="社团职务" prop="role" width="100" align="center" header-align="center" />
<el-table-column label="联系方式" prop="phone" min-width="130" align="center" header-align="center" />
</el-table>
</div>
<!-- 年度活动开展情况 -->
<div class="form-section">
<h4 class="section-title">年度活动开展情况</h4>
<el-table :data="activityList" border style="width: 100%" max-height="300" class="activity-table">
<el-table-column label="时间" width="200" align="center" header-align="center">
<template #default="{ row }">
<div class="activity-time-display">
{{ row.activityTimeDisplay }}
</div>
</template>
</el-table-column>
<el-table-column label="地点" prop="location" width="150" align="center" header-align="center" />
<el-table-column label="活动名称" prop="activityName" min-width="200" align="center" header-align="center" />
<el-table-column label="参加人数" prop="participantCount" width="100" align="center" header-align="center" />
</el-table>
</div>
<!-- 审批意见 -->
<div class="form-section">
<h4 class="section-title">审批意见</h4>
<!-- 指导老师意见 -->
<div class="opinion-section">
<div class="opinion-title">指导老师意见</div>
<div class="opinion-content">
{{ annualReview.advisorOpinion || '暂无意见' }}
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
<!-- 二级学院意见 -->
<div class="opinion-section">
<div class="opinion-title">二级学院意见</div>
<div class="opinion-content">
{{ annualReview.collegeOpinion || '暂无意见' }}
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
<!-- 校团委意见 -->
<div class="opinion-section">
<div class="opinion-title">校团委意见</div>
<div class="opinion-content">
{{ annualReview.youthLeagueOpinion || '暂无意见' }}
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
</div>
</el-form>
</div>
<el-empty v-else description="暂无数据" />
</div>
</div>
</template>
<script setup lang="ts">
import { getStglClubAnnualReview, getAnnualReviewActivities } from '@/api/stgl/club/annualReview';
import { StglClubAnnualReviewVO } from '@/api/stgl/club/annualReview/type';
// 扩展的年审VO接口(包含JSON字段)
interface ExtendedAnnualReviewVO extends StglClubAnnualReviewVO {
membersJson?: string;
activitiesJson?: string;
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { stgl_club_type } = toRefs(proxy?.useDict('stgl_club_type') as any);
const props = defineProps<{
annualReviewId: string | number;
}>();
const loading = ref(false);
const annualReview = ref<ExtendedAnnualReviewVO>();
// 成员列表
const memberList = ref<any[]>([]);
// 活动列表
const activityList = ref<any[]>([]);
// 年份范围计算
const yearRange = computed(() => {
if (annualReview.value?.reviewYear) {
const currentYear = annualReview.value.reviewYear;
const lastYear = currentYear - 1;
return `${lastYear}-${currentYear}`;
}
return '';
});
// 年审日期
const reviewDate = computed(() => {
if (annualReview.value?.reviewTime) {
const date = new Date(annualReview.value.reviewTime);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}年${month}月${day}日`;
}
return '';
});
// 成员数量文本
const memberCountText = computed(() => {
return `${annualReview.value?.memberCount || 0}人`;
});
/** 格式化活动时间显示 */
const formatActivityTime = (startTime: string, endTime?: string): string => {
if (!startTime) return '';
try {
// 处理标准格式 (如: "2024-09-05 14:30:00" 或 "2024-09-05")
const parseTimeString = (timeStr: string): Date | null => {
if (!timeStr) return null;
if (timeStr.match(/^\d{4}-\d{2}-\d{2}/)) {
const date = new Date(timeStr);
return isNaN(date.getTime()) ? null : date;
}
const date = new Date(timeStr);
return isNaN(date.getTime()) ? null : date;
};
const startDate = parseTimeString(startTime);
const endDate = endTime ? parseTimeString(endTime) : null;
if (!startDate) {
return startTime;
}
// 格式化为完整的日期时间(保留00:00:00)
const formatDateTime = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const formattedStartTime = formatDateTime(startDate);
if (!endDate || startDate.getTime() === endDate.getTime()) {
return formattedStartTime;
}
const formattedEndTime = formatDateTime(endDate);
// 检查是否是同一天
const isSameDay = startDate.toDateString() === endDate.toDateString();
if (isSameDay) {
// 同一天的情况,分行显示:日期在上,时间范围在下
const dateStr = formattedStartTime.split(' ')[0];
const startTimeStr = formattedStartTime.split(' ')[1];
const endTimeStr = formattedEndTime.split(' ')[1];
return `${dateStr}\n${startTimeStr}-${endTimeStr}`;
}
// 不同天的情况,分行显示完整时间
return `${formattedStartTime}\n${formattedEndTime}`;
} catch (error) {
console.warn('格式化活动时间失败:', error, '原始数据:', startTime);
return startTime;
}
};
/** 获取年审记录详情 */
const getDetail = async () => {
if (!props.annualReviewId) return;
loading.value = true;
try {
const res = await getStglClubAnnualReview(props.annualReviewId);
annualReview.value = res.data;
// 检查数据是否存在
if (!annualReview.value) {
console.warn('年审记录数据为空');
memberList.value = [];
activityList.value = [];
return;
}
// 解析成员JSON数据
if (annualReview.value.membersJson) {
try {
memberList.value = JSON.parse(annualReview.value.membersJson);
} catch (error) {
console.warn('解析成员JSON数据失败:', error);
memberList.value = [];
}
} else {
memberList.value = [];
}
// 从stgl_club_annual_review_activity表中获取年审记录对应的活动数据
try {
const activitiesRes = await getAnnualReviewActivities(props.annualReviewId);
// 检查活动数据是否存在
if (activitiesRes && activitiesRes.data && Array.isArray(activitiesRes.data)) {
// 处理活动数据
activityList.value = activitiesRes.data.map((activity: any) => {
// 使用后端 VO 中的标准字段名
const activityStartTime = activity.activityStartTime || activity.startTime;
const activityEndTime = activity.activityEndTime || activity.endTime;
// 生成显示用的时间格式
let timeDisplay = '';
if (activityStartTime || activityEndTime) {
timeDisplay = formatActivityTime(activityStartTime, activityEndTime);
}
return {
activityStartTime: activityStartTime,
activityEndTime: activityEndTime,
activityTimeDisplay: timeDisplay || '时间未知',
location: activity.location || activity.activityLocation || '',
activityName: activity.activityName || '',
participantCount: activity.participantCount || 0
};
});
} else {
console.warn('活动数据为空或格式不正确:', activitiesRes);
activityList.value = [];
}
} catch (error) {
console.warn('获取年审活动数据失败:', error);
activityList.value = [];
}
} catch (error) {
console.error('获取详情失败:', error);
} finally {
loading.value = false;
}
};
watch(
() => props.annualReviewId,
() => {
getDetail();
},
{ immediate: true }
);
</script>
<style scoped>
.annual-review-detail {
padding: 20px;
max-height: 80vh;
overflow-y: auto;
}
.form-header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
border: 2px solid #409eff;
border-radius: 8px;
background: #fafafa;
}
.form-title {
margin: 0 0 15px 0;
font-size: 24px;
font-weight: bold;
color: #303133;
}
.year-range {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
color: #606266;
}
.review-date {
font-size: 14px;
color: #909399;
text-align: right;
}
.form-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fff;
}
.section-title {
margin: 0 0 20px 0;
padding-bottom: 10px;
font-size: 16px;
font-weight: bold;
color: #303133;
border-bottom: 2px solid #409eff;
}
.basic-info-table {
border: 1px solid #e4e7ed;
border-radius: 4px;
overflow: hidden;
}
.info-row {
display: flex;
border-bottom: 1px solid #e4e7ed;
}
.info-row:last-child {
border-bottom: none;
}
.info-item {
flex: 1;
display: flex;
border-right: 1px solid #e4e7ed;
}
.info-item:last-child {
border-right: none;
}
.info-label {
width: 100px;
min-width: 100px;
padding: 12px 15px;
background-color: #f5f7fa;
border-right: 1px solid #e4e7ed;
font-weight: 500;
color: #303133;
font-size: 14px;
display: flex;
align-items: center;
}
.info-value {
flex: 1;
padding: 12px 15px;
background-color: #fff;
color: #606266;
font-size: 14px;
display: flex;
align-items: center;
min-height: 44px;
}
.charter-status {
font-size: 14px;
color: #606266;
font-weight: 400;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.charter-info {
margin-top: 5px;
margin-left: 0;
}
.opinion-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #fafafa;
}
.opinion-title {
font-weight: bold;
margin-bottom: 10px;
color: #303133;
}
.opinion-content {
min-height: 60px;
padding: 10px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #fff;
margin-bottom: 10px;
line-height: 1.6;
}
.opinion-signature {
text-align: right;
color: #606266;
font-size: 14px;
}
:deep(.el-table) {
margin-bottom: 10px;
}
:deep(.el-table th) {
background-color: #f5f7fa;
color: #303133;
font-weight: bold;
}
:deep(.el-descriptions__label) {
font-weight: bold;
color: #303133;
}
:deep(.el-descriptions__content) {
color: #606266;
}
.activity-time-display {
white-space: pre-line;
line-height: 1.4;
font-size: 13px;
}
</style>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,386 @@
<template>
<div class="p-2 page-container">
<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="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="社团名称" prop="clubName">
<el-input v-model="queryParams.clubName" placeholder="请输入社团名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="年审年度" prop="reviewYear">
<el-select v-model="queryParams.reviewYear" placeholder="请选择年审年度" clearable style="width: 100%">
<el-option v-for="year in yearOptions" :key="year" :label="year + '年'" :value="year" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option v-for="option in statusOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<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" class="button-card table-card">
<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:clubAnnualReview:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['stgl:clubAnnualReview:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['stgl:clubAnnualReview:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" icon="Download" @click="handleExport" v-hasPermi="['stgl:clubAnnualReview:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="annualReviewList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="社团名称" align="center" prop="clubName" min-width="150" />
<el-table-column label="成立日期" align="center" prop="establishDate" width="120">
<template #default="scope">
<span>{{ parseTime(scope.row.establishDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="社长" align="center" prop="presidentName" width="100" />
<el-table-column label="年审年度" align="center" prop="reviewYear" width="100" />
<el-table-column label="社团类型" align="center" prop="clubType" width="100">
<template #default="scope">
<dict-tag :options="stgl_club_type" :value="scope.row.clubType" />
</template>
</el-table-column>
<el-table-column label="指导老师" align="center" prop="advisorName" width="100" />
<el-table-column label="成员数量" align="center" prop="memberCount" width="100" />
<el-table-column label="资金数额" align="center" prop="fundAmount" width="120">
<template #default="scope">
<span v-if="scope.row.fundAmount">{{ scope.row.fundAmount }}元</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status) as any">{{ getStatusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="年审时间" align="center" prop="createTime" width="180">
<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" class-name="small-padding fixed-width" width="200" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['stgl:clubAnnualReview:query']"></el-button>
</el-tooltip>
<el-tooltip content="编辑" placement="top" v-if="scope.row.status === '0'">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['stgl:clubAnnualReview:edit']"></el-button>
</el-tooltip>
<el-tooltip content="提交" placement="top" v-if="scope.row.status === '0'">
<el-button link type="success" icon="Check" @click="handleSubmit(scope.row)" v-hasPermi="['stgl:clubAnnualReview:submit']"></el-button>
</el-tooltip>
<el-tooltip content="撤回" placement="top" v-if="scope.row.status === '1'">
<el-button link type="warning" icon="RefreshLeft" @click="handleWithdraw(scope.row)" v-hasPermi="['stgl:clubAnnualReview:withdraw']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="scope.row.status === '0'">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['stgl:clubAnnualReview: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="1000px" append-to-body>
<annual-review-form ref="annualReviewFormRef" @success="handleFormSuccess" />
</el-dialog>
<!-- 查看社团年审记录对话框 -->
<el-dialog title="年审记录详情" v-model="detailDialog.visible" width="1000px" append-to-body>
<annual-review-detail :annual-review-id="detailDialog.annualReviewId" />
</el-dialog>
</div>
</template>
<script setup name="StglClubAnnualReview" lang="ts">
import { useRoute } from 'vue-router';
import {
listStglClubAnnualReview,
getStglClubAnnualReview,
delStglClubAnnualReview,
submitStglClubAnnualReview,
withdrawStglClubAnnualReview,
exportStglClubAnnualReview
} from '@/api/stgl/club/annualReview';
import { StglClubAnnualReviewVO, StglClubAnnualReviewQuery, ANNUAL_REVIEW_STATUS_OPTIONS } from '@/api/stgl/club/annualReview/type';
import AnnualReviewForm from './form.vue';
import AnnualReviewDetail from './detail.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const { stgl_club_type } = toRefs(proxy?.useDict('stgl_club_type') as any);
const annualReviewList = ref<StglClubAnnualReviewVO[]>([]);
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 annualReviewFormRef = ref<InstanceType<typeof AnnualReviewForm>>();
// 状态选项
const statusOptions = ref(ANNUAL_REVIEW_STATUS_OPTIONS);
// 年份选项(最近10年)
const yearOptions = ref<number[]>([]);
const initYearOptions = () => {
const currentYear = new Date().getFullYear();
for (let i = 0; i < 10; i++) {
yearOptions.value.push(currentYear - i);
}
};
const data = reactive<{
queryParams: StglClubAnnualReviewQuery;
dialog: DialogOption;
detailDialog: {
visible: boolean;
annualReviewId: string | number;
};
}>({
queryParams: {
pageNum: 1,
pageSize: 10,
clubName: undefined,
reviewYear: undefined,
status: undefined
},
dialog: {
visible: false,
title: ''
},
detailDialog: {
visible: false,
annualReviewId: ''
}
});
// 初始化查询参数(处理从社团管理页面传递的参数)
const initQueryParams = () => {
if (route.query.clubName) {
queryParams.value.clubName = route.query.clubName as string;
}
if (route.query.clubId) {
// 如果有具体的社团ID,我们可以根据需要进行筛选
// 这里暂时使用社团名称进行筛选
}
};
const { queryParams, dialog, detailDialog } = toRefs(data);
/** 查询社团年审记录列表 */
const getList = async () => {
loading.value = true;
const res = await listStglClubAnnualReview(queryParams.value);
annualReviewList.value = res.rows;
total.value = res.total;
loading.value = false;
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: StglClubAnnualReviewVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
dialog.value = {
visible: true,
title: '添加社团年审记录'
};
nextTick(async () => {
await annualReviewFormRef.value?.resetForm();
// 如果从社团管理页面跳转过来,自动设置社团信息
if (route.query.clubId) {
annualReviewFormRef.value?.setClubId(route.query.clubId as string);
}
});
};
/** 修改按钮操作 */
const handleUpdate = async (row?: StglClubAnnualReviewVO) => {
const annualReviewId = row?.id || ids.value[0];
const res = await getStglClubAnnualReview(annualReviewId);
dialog.value = {
visible: true,
title: '修改社团年审记录'
};
nextTick(async () => {
await annualReviewFormRef.value?.resetForm(res.data);
});
};
/** 查看按钮操作 */
const handleView = (row: StglClubAnnualReviewVO) => {
detailDialog.value = {
visible: true,
annualReviewId: row.id
};
};
/** 提交按钮操作 */
const handleSubmit = async (row: StglClubAnnualReviewVO) => {
try {
await proxy?.$modal.confirm(`是否确认提交年审记录"${row.clubName}"?`);
await submitStglClubAnnualReview(row.id);
await getList();
proxy?.$modal.msgSuccess('提交成功');
} catch {}
};
/** 撤回按钮操作 */
const handleWithdraw = async (row: StglClubAnnualReviewVO) => {
try {
await proxy?.$modal.confirm(`是否确认撤回年审记录"${row.clubName}"?`);
await withdrawStglClubAnnualReview(row.id);
await getList();
proxy?.$modal.msgSuccess('撤回成功');
} catch {}
};
/** 删除按钮操作 */
const handleDelete = async (row?: StglClubAnnualReviewVO) => {
const annualReviewIds = row?.id || ids.value;
try {
await proxy?.$modal.confirm('是否确认删除所选的社团年审记录?');
await delStglClubAnnualReview(annualReviewIds);
await getList();
proxy?.$modal.msgSuccess('删除成功');
} catch {}
};
/** 导出按钮操作 */
const handleExport = async () => {
try {
const res = await exportStglClubAnnualReview(queryParams.value);
proxy?.$download.excel(res, `annual_review_${new Date().getTime()}.xlsx`);
} catch (error) {
proxy?.$modal.msgError('导出失败');
}
};
/** 表单提交成功回调 */
const handleFormSuccess = () => {
dialog.value.visible = false;
getList();
};
/** 获取状态标签类型 */
const getStatusTagType = (status: string) => {
const tagTypes: { [key: string]: string } = {
'0': 'info',
'1': 'warning',
'2': '',
'3': 'success',
'4': 'danger'
};
return tagTypes[status] || '';
};
/** 获取状态文本 */
const getStatusText = (status: string) => {
const option = statusOptions.value.find(item => item.value === status);
return option?.label || status;
};
onMounted(() => {
initYearOptions();
initQueryParams();
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
background-color: #f5f5f5;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
</style>
@@ -101,7 +101,7 @@
<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>
<dict-tag :options="stgl_role_type" :value="scope.row.roleType" v-if="scope.row.roleType" />
<span v-else>-</span>
</template>
</el-table-column>
@@ -200,7 +200,9 @@
</el-col>
<el-col :span="12">
<el-form-item label="成员角色" prop="roleType">
<el-input v-model="form.roleType" placeholder="请输入成员角色" />
<el-select v-model="form.roleType" placeholder="请选择成员角色">
<el-option v-for="dict in stgl_role_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -237,7 +239,8 @@
<el-descriptions-item label="联系方式">{{ viewForm.phone }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ viewForm.email }}</el-descriptions-item>
<el-descriptions-item label="成员角色">
{{ viewForm.roleType || '-' }}
<dict-tag :options="stgl_role_type" :value="viewForm.roleType" v-if="viewForm.roleType" />
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="状态">
<dict-tag :options="stgl_member_status" :value="viewForm.status" />
@@ -322,7 +325,7 @@ interface Props {
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 { sys_user_sex, stgl_member_status, stgl_role_type } = toRefs<any>(proxy?.useDict('sys_user_sex', 'stgl_member_status', 'stgl_role_type'));
const memberList = ref<StglMemberVO[]>([]);
const buttonLoading = ref(false);
+136 -116
View File
@@ -60,6 +60,26 @@
>转让</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
icon="UserFilled"
:disabled="single"
@click="handleAdvisorHistoryFromToolbar()"
v-hasPermi="['stgl:club:query']"
>指导老师</el-button
>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
icon="Document"
:disabled="single"
@click="handleAnnualReview()"
v-hasPermi="['stgl:clubAnnualReview:list']"
>年审管理</el-button
>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['stgl:club:remove']">删除</el-button>
</el-col>
@@ -127,7 +147,7 @@
<div class="description-cell">{{ scope.row.description }}</div>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280" fixed="right">
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="240" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
@@ -162,17 +182,18 @@
v-hasPermi="['stgl:clubmember:list']"
></el-button>
</el-tooltip>
<el-tooltip content="活动管理" placement="top">
<el-button
link
type="success"
icon="Calendar"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleActivityManagement(scope.row)"
v-hasPermi="['stgl:activity:list']"
></el-button>
</el-tooltip>
<el-tooltip content="活动管理" placement="top">
<el-button
link
type="success"
icon="Calendar"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleActivityManagement(scope.row)"
v-hasPermi="['stgl:activity:list']"
></el-button>
</el-tooltip>
<el-tooltip content="转让" placement="top">
<el-button
link
@@ -312,7 +333,12 @@
<!-- 指导老师信息 -->
<div class="personnel-section">
<h4 class="section-title">指导老师信息</h4>
<AdvisorManager v-model="form.advisors" @change="handleAdvisorsChange" :disabled="!hasInstructorPermission" />
<AdvisorManager
v-model="form.advisors"
:establish-date="form.establishDate"
:is-new-club="!form.id"
:disabled="!hasInstructorPermission"
/>
</div>
</el-tab-pane>
@@ -617,6 +643,14 @@
:current-member-count="currentClub?.memberCount"
@member-count-changed="handleMemberCountChanged"
/>
<!-- 指导老师历史记录弹窗 -->
<AdvisorHistoryDialog
v-model:visible="advisorHistoryDialog.visible"
:title="advisorHistoryDialog.title"
:club-id="currentAdvisorClub?.id"
:club-name="currentAdvisorClub?.clubName"
/>
</div>
</template>
@@ -648,6 +682,7 @@ import { StglClubVO, StglClubQuery, StglClubForm } from '@/api/stgl/club/types';
import StudentSelector from '@/components/StudentSelector/index.vue';
import AdvisorManager from '@/components/AdvisorManager/index.vue';
import MemberManagementDialog from './components/MemberManagementDialog.vue';
import AdvisorHistoryDialog from '@/components/AdvisorHistoryDialog/index.vue';
import { StudentVO } from '@/api/xg/student/types';
import { AdvisorVO } from '@/components/AdvisorManager/index.vue';
import { Document } from '@element-plus/icons-vue';
@@ -711,6 +746,14 @@ const memberDialog = reactive<DialogOption>({
const currentClub = ref<StglClubVO | null>(null);
const memberActiveTab = ref('joined');
// 指导老师历史记录相关
const advisorHistoryDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const currentAdvisorClub = ref<StglClubVO | null>(null);
interface TransferForm {
clubId?: string | number;
oldPresidentName?: string;
@@ -850,12 +893,7 @@ const getList = async () => {
const res = await listStglClub(queryParams.value);
// 🔥 调试信息:检查返回的数据结构
console.log('getList: 获取到的社团列表数据:', res);
if (res.rows && res.rows.length > 0) {
console.log('getList: 第一条社团数据详情:', res.rows[0]);
console.log('getList: 第一条社团的advisorsJson字段:', res.rows[0].advisorsJson);
}
// 处理列表数据:将状态字段从数字类型转换为字符串类型,确保字典组件正常显示
if (res.rows && Array.isArray(res.rows)) {
@@ -927,10 +965,8 @@ const handleAdd = () => {
const handleUpdate = async (row?: StglClubVO) => {
reset();
const _id = row?.id || ids.value[0];
console.log('handleUpdate: 开始获取社团数据,ID:', _id);
const res = await getStglClub(_id);
console.log('handleUpdate: API响应数据:', res.data);
// 处理经费来源数据:如果是字符串则分割为数组
if (res.data.fundingSources) {
@@ -964,37 +1000,30 @@ const handleUpdate = async (row?: StglClubVO) => {
}
// 处理副社长信息:优先从JSON字段读取,如果没有则使用单一字段
console.log('handleUpdate: 副社长原始数据:', {
vicePresidentsJson: res.data.vicePresidentsJson,
vicePresidentName: res.data.vicePresidentName,
vicePresidentStudentNo: res.data.vicePresidentStudentNo,
vicePresidentDept: res.data.vicePresidentDept,
vicePresidentMajor: res.data.vicePresidentMajor,
vicePresidentClass: res.data.vicePresidentClass,
vicePresidentPhone: res.data.vicePresidentPhone
});
// 处理指导老师信息:优先从JSON字段读取,如果没有则使用单一字段
// 🔥 优先从JSON字段读取指导老师数据
let advisorsData = [];
if (res.data.advisorsJson) {
try {
(res.data as any).advisors = JSON.parse(res.data.advisorsJson);
console.log('handleUpdate: 从JSON字段读取的指导老师数据:', (res.data as any).advisors);
advisorsData = JSON.parse(res.data.advisorsJson);
} catch (error) {
console.warn('解析指导老师JSON数据失败:', error);
(res.data as any).advisors = [];
advisorsData = [];
}
} else if (res.data.advisorName) {
// 回退到单一字段
(res.data as any).advisors = [
// 回退到单一字段,构建完整的teacher对象
advisorsData = [
{
name: res.data.advisorName,
dept: res.data.advisorDept,
phone: res.data.advisorPhone || '',
email: res.data.advisorEmail || '',
userId: '',
teacher: {
userId: null,
userName: '',
nickName: res.data.advisorName,
deptName: res.data.advisorDept,
phonenumber: res.data.advisorPhone || '',
@@ -1002,13 +1031,15 @@ const handleUpdate = async (row?: StglClubVO) => {
}
}
];
} else {
(res.data as any).advisors = [];
}
// 先设置其他数据
Object.assign(form.value, res.data);
console.log('handleUpdate: 表单数据设置后:', form.value);
console.log('handleUpdate: 表单中的副社长数据:', form.value.vicePresidents);
// 然后单独设置指导老师数据,避免触发 watch 时的冲突
nextTick(() => {
form.value.advisors = advisorsData;
});
dialog.visible = true;
dialog.title = '修改社团信息';
@@ -1042,15 +1073,9 @@ const submitForm = () => {
// 🔥 新增:同时保存JSON字段和兼容性字段
// 同时保存JSON字段和兼容性字段
submitData.advisorsJson = JSON.stringify(submitData.advisors || []);
// 调试日志:检查指导老师数据
console.log('提交数据 - advisors:', submitData.advisors);
console.log('提交数据 - advisorName:', submitData.advisorName);
console.log('提交数据 - advisorDept:', submitData.advisorDept);
console.log('完整提交数据:', submitData);
if (form.value.id) {
await updateStglClub(submitData).finally(() => (buttonLoading.value = false));
} else {
@@ -1111,16 +1136,10 @@ const handleView = async (row: StglClubVO) => {
// 🔥 处理指导老师信息:优先从JSON字段读取,如果没有则使用单一字段
console.log('handleView: 指导老师原始数据:', {
advisorsJson: viewData.advisorsJson,
advisorName: viewData.advisorName
});
// 处理指导老师信息:优先从JSON字段读取,如果没有则使用单一字段
if (viewData.advisorsJson) {
try {
(viewData as any).advisors = JSON.parse(viewData.advisorsJson);
console.log('handleView: 从JSON字段读取的指导老师数据:', (viewData as any).advisors);
} catch (error) {
console.warn('解析指导老师JSON数据失败:', error);
(viewData as any).advisors = [];
@@ -1145,10 +1164,6 @@ const handleView = async (row: StglClubVO) => {
(viewData as any).advisors = [];
}
console.log('handleView: 处理后的数据:', {
advisors: (viewData as any).advisors
});
Object.assign(viewForm, viewData);
viewDialog.visible = true;
viewDialog.title = '查看社团信息';
@@ -1212,16 +1227,12 @@ const getSourceTagType = (value: string) => {
/** 获取指导老师姓名列表 */
const getAdvisorNames = (row: any): string[] => {
console.log('getAdvisorNames: 处理行数据:', row);
// 优先从JSON字段读取指导老师数据
if (row.advisorsJson) {
try {
const advisors = JSON.parse(row.advisorsJson);
console.log('getAdvisorNames: 从JSON字段解析的指导老师数据:', advisors);
if (Array.isArray(advisors)) {
const names = advisors.map((advisor: any) => advisor.name || advisor.teacher?.nickName || '').filter((name) => name);
console.log('getAdvisorNames: 从JSON提取的姓名列表:', names);
return names;
}
} catch (error) {
@@ -1231,11 +1242,9 @@ const getAdvisorNames = (row: any): string[] => {
// 回退到单一字段
if (row.advisorName) {
console.log('getAdvisorNames: 从单一字段获取指导老师:', row.advisorName);
return [row.advisorName];
}
console.log('getAdvisorNames: 没有找到指导老师数据');
return [];
};
@@ -1265,107 +1274,91 @@ const handlePresidentChange = (student: StudentVO | null) => {
// 防止递归更新的锁
const syncLock = ref(false);
// 防止递归更新的标志
const syncingAdvisors = ref(false);
/** 指导老师列表变化处理 */
const handleAdvisorsChange = (advisors: AdvisorVO[]) => {
// 添加调试日志
console.log('handleAdvisorsChange 被调用,advisors:', advisors);
// 添加防护锁,避免递归更新
if (syncLock.value) return;
syncLock.value = true;
nextTick(() => {
// 同步指导老师数据到表单
form.value.advisors = advisors;
// 为了兼容旧版本,同步第一个指导老师的信息到旧字段
if (advisors.length > 0) {
const firstAdvisor = advisors[0];
form.value.advisorName = firstAdvisor.name || firstAdvisor.teacher?.nickName;
form.value.advisorDept = firstAdvisor.dept || firstAdvisor.teacher?.deptName;
form.value.advisorPhone = firstAdvisor.phone || firstAdvisor.teacher?.phonenumber;
form.value.advisorEmail = firstAdvisor.email || firstAdvisor.teacher?.email;
} else {
form.value.advisorName = undefined;
form.value.advisorDept = undefined;
form.value.advisorPhone = undefined;
form.value.advisorEmail = undefined;
}
syncLock.value = false;
});
};
/** 监听指导老师数据变化,同步兼容性字段 */
watch(
() => form.value.advisors,
(advisors: AdvisorVO[]) => {
// 防止递归更新
if (syncingAdvisors.value) return;
syncingAdvisors.value = true;
nextTick(() => {
try {
// 为了兼容旧版本,同步第一个指导老师的信息到旧字段
if (advisors && advisors.length > 0) {
const firstAdvisor = advisors[0];
form.value.advisorName = firstAdvisor.name || firstAdvisor.teacher?.nickName;
form.value.advisorDept = firstAdvisor.dept || firstAdvisor.teacher?.deptName;
form.value.advisorPhone = firstAdvisor.phone || firstAdvisor.teacher?.phonenumber;
form.value.advisorEmail = firstAdvisor.email || firstAdvisor.teacher?.email;
} else {
form.value.advisorName = undefined;
form.value.advisorDept = undefined;
form.value.advisorPhone = undefined;
form.value.advisorEmail = undefined;
}
} finally {
syncingAdvisors.value = false;
}
});
},
{ deep: true }
);
/** 检查用户是否为指定社团的社长(无论是否为创建者) */
const checkIsPresident = async (clubId: string | number): Promise<boolean> => {
try {
console.log('🔍 checkIsPresident: 开始检查社长权限, clubId:', clubId);
const res = await checkPresidentRole(clubId);
console.log('🔍 checkIsPresident: API返回结果:', res);
const isPresident = res.data === true;
console.log('🔍 checkIsPresident: 最终结果:', isPresident);
return isPresident;
} catch (error) {
console.error('❌ checkIsPresident: 检查社长权限失败:', error);
console.error('checkIsPresident: 检查社长权限失败:', error);
return false;
}
};
/** 批量检查社长权限(检查用户是否为各社团的社长,无论是否为创建者) */
const checkBatchPresidentRoles = async (clubs: StglClubVO[]) => {
console.log('🔍 checkBatchPresidentRoles: 开始批量检查权限, clubs数量:', clubs.length);
const checks = clubs.map(async (club) => {
const isPresident = await checkIsPresident(club.id!);
presidentRoles.value[club.id!] = isPresident;
console.log(`🔍 checkBatchPresidentRoles: 社团${club.id}(${club.clubName}) 权限检查结果:`, isPresident);
return { clubId: club.id!, isPresident };
});
await Promise.all(checks);
console.log('🔍 checkBatchPresidentRoles: 批量权限检查完成, presidentRoles:', presidentRoles.value);
};
/** 判断是否显示转让按钮 */
const shouldShowTransferButton = (club: StglClubVO): boolean => {
console.log('🔍 shouldShowTransferButton: 开始判断转让按钮显示, club:', club);
// 1. 检查基础权限
const hasEditPermission = proxy?.$auth.hasPermi('stgl:club:edit');
console.log('🔍 shouldShowTransferButton: 基础edit权限检查:', hasEditPermission);
if (!hasEditPermission) {
console.log('❌ shouldShowTransferButton: 无基础edit权限,返回false');
return false;
}
// 2. 检查角色权限 - 如果有stgl_president角色,则需要进一步检查是否为该社团社长
const hasPresidentRole = proxy?.$auth.hasRole('stgl_president');
console.log('🔍 shouldShowTransferButton: stgl_president角色检查:', hasPresidentRole);
if (hasPresidentRole) {
// 如果权限检查还未完成,返回false(避免按钮闪现)
const isPresident = presidentRoles.value[club.id!];
console.log('🔍 shouldShowTransferButton: 社团社长权限检查结果:', isPresident, 'presidentRoles:', presidentRoles.value);
if (isPresident === undefined) {
console.log('⏳ shouldShowTransferButton: 权限检查未完成,返回false避免闪现');
return false;
}
// 社长可以看到转让按钮(无论是否为创建者)
const result = isPresident === true;
console.log('🔍 shouldShowTransferButton: stgl_president角色最终结果:', result);
return result;
}
// 3. 如果是管理员角色,显示转让按钮(但转让操作仍需社长权限验证)
const hasAdminRole = proxy?.$auth.hasRoleOr(['admin', 'common']);
console.log('🔍 shouldShowTransferButton: 管理员角色检查:', hasAdminRole);
if (hasAdminRole) {
console.log('✅ shouldShowTransferButton: 管理员角色,返回true');
return true;
}
console.log('❌ shouldShowTransferButton: 无任何有效角色,返回false');
return false;
};
@@ -1473,7 +1466,6 @@ const handleActivityManagement = (row: StglClubVO) => {
/** 成员管理按钮操作 */
const handleMemberManagement = (row: StglClubVO) => {
console.log('handleMemberManagement: 打开成员管理弹窗, row:', row);
currentClub.value = row;
memberDialog.visible = true;
memberDialog.title = `${row.clubName} - 成员管理`;
@@ -1482,7 +1474,6 @@ const handleMemberManagement = (row: StglClubVO) => {
/** 处理成员数量变化 */
const handleMemberCountChanged = (newCount: number) => {
console.log('handleMemberCountChanged: 成员数量变化:', newCount, '当前社团:', currentClub.value);
if (currentClub.value) {
// 更新当前社团的成员数量
currentClub.value.memberCount = newCount;
@@ -1491,11 +1482,40 @@ const handleMemberCountChanged = (newCount: number) => {
const clubIndex = clubList.value.findIndex(club => club.id === currentClub.value?.id);
if (clubIndex !== -1) {
clubList.value[clubIndex].memberCount = newCount;
console.log('handleMemberCountChanged: 已更新列表中的成员数量:', clubList.value[clubIndex]);
}
}
};
/** 指导老师历史记录按钮操作 */
const handleAdvisorHistory = (row: StglClubVO) => {
currentAdvisorClub.value = row;
advisorHistoryDialog.visible = true;
advisorHistoryDialog.title = `${row.clubName} - 指导老师历史记录`;
};
/** 工具栏指导老师按钮操作 */
const handleAdvisorHistoryFromToolbar = () => {
const selectedClub = clubList.value.find((c) => ids.value.includes(c.id!));
if (selectedClub) {
handleAdvisorHistory(selectedClub);
}
};
/** 年审管理按钮操作 */
const handleAnnualReview = () => {
const selectedClub = clubList.value.find((c) => ids.value.includes(c.id!));
if (selectedClub) {
router.push({
path: '/stgl/club/info/annualReview/index',
query: {
clubId: selectedClub.id,
clubName: selectedClub.clubName,
from: 'club'
}
});
}
};
onMounted(() => {
getList();
});
+218 -15
View File
@@ -77,8 +77,26 @@
<dict-tag :options="stgl_activity_type" :value="scope.row.activityType" />
</template>
</el-table-column>
<el-table-column label="主办方" align="center" prop="organizerName" width="120" show-overflow-tooltip />
<el-table-column label="关联组织" align="center" width="120" show-overflow-tooltip>
<!-- 隐藏主办方字段 -->
<!-- <el-table-column label="主办方" align="center" prop="organizerName" width="120" show-overflow-tooltip /> -->
<el-table-column label="关联部门" align="center" width="150" show-overflow-tooltip>
<template #default="scope">
<div v-if="getTableRelatedDeptNames(scope.row).length > 0" class="dept-list-table">
<el-tag
v-for="(deptName, index) in getTableRelatedDeptNames(scope.row)"
:key="index"
type="info"
size="small"
class="dept-tag-table"
:title="deptName"
>
{{ deptName }}
</el-tag>
</div>
<span v-else class="text-gray-400">未关联</span>
</template>
</el-table-column>
<el-table-column label="关联社团" align="center" width="120" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.organizationName">{{ scope.row.organizationName }}</span>
<span v-else class="text-gray-400">未关联</span>
@@ -96,7 +114,7 @@
<el-table-column label="活动地点" align="center" prop="location" width="120" show-overflow-tooltip />
<el-table-column label="活动状态" align="center" prop="activityStatus" width="100">
<template #default="scope">
<dict-tag :options="stgl_activity_status" :value="scope.row.activityStatus" />
<dict-tag :options="stgl_activity_status" :value="calculateActivityStatus(scope.row)" />
</template>
</el-table-column>
<el-table-column label="报名时间" align="center" width="180">
@@ -175,15 +193,28 @@
<div class="detail-content">
<!-- 基本信息 -->
<div class="detail-section">
<el-descriptions :column="2" border>
<el-descriptions :column="2" border class="activity-detail-descriptions">
<el-descriptions-item label="活动名称">{{ detailForm.activityName }}</el-descriptions-item>
<el-descriptions-item label="活动类型">
<dict-tag :options="stgl_activity_type" :value="detailForm.activityType" />
</el-descriptions-item>
<el-descriptions-item label="活动主题">{{ detailForm.activityTheme || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="主办方">{{ detailForm.organizerName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="承办方">{{ detailForm.undertakerName || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="关联组织">
<el-descriptions-item label="活动地点">{{ detailForm.location || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="关联部门">
<div v-if="getTableRelatedDeptNames(detailForm).length > 0" class="dept-list-detail-optimized">
<el-tag
v-for="(deptName, index) in getTableRelatedDeptNames(detailForm)"
:key="index"
type="info"
size="small"
class="dept-tag-detail-optimized"
>
{{ deptName }}
</el-tag>
</div>
<span v-else class="text-gray-400">未关联</span>
</el-descriptions-item>
<el-descriptions-item label="关联社团">
<span v-if="detailForm.organizationName">
{{ detailForm.organizationName }}
<el-tag size="small" type="info" class="ml-1">
@@ -192,17 +223,10 @@
</span>
<span v-else>未关联</span>
</el-descriptions-item>
<el-descriptions-item label="指导老师">
<span v-if="detailForm.instructors && detailForm.instructors.length > 0">
{{ getInstructorNames(detailForm.instructors) }}
</span>
<span v-else>暂无</span>
</el-descriptions-item>
<el-descriptions-item label="活动地点">{{ detailForm.location || '暂无' }}</el-descriptions-item>
<el-descriptions-item label="开始时间">{{ parseTime(detailForm.startTime, '{y}-{m}-{d} {h}:{i}') }}</el-descriptions-item>
<el-descriptions-item label="结束时间">{{ parseTime(detailForm.endTime, '{y}-{m}-{d} {h}:{i}') }}</el-descriptions-item>
<el-descriptions-item label="活动状态">
<dict-tag :options="stgl_activity_status" :value="detailForm.activityStatus" />
<dict-tag :options="stgl_activity_status" :value="calculateActivityStatus(detailForm)" />
</el-descriptions-item>
<el-descriptions-item label="报名状态">
<dict-tag :options="stgl_registration_status" :value="detailForm.registrationStatus" />
@@ -1136,6 +1160,69 @@ const getInstructorNames = (instructors: string | any[]): string => {
.join('、');
};
/** 获取活动关联部门名称列表 */
const getTableRelatedDeptNames = (activity: MyActivityVO): string[] => {
// 优先检查relatedDeptNames字段(多部门支持)
if (activity.relatedDeptNames) {
// 如果是数组类型,直接使用
if (Array.isArray(activity.relatedDeptNames)) {
return activity.relatedDeptNames.filter(name => name);
}
// 如果是字符串类型(逗号分隔),进行分割
const deptNamesStr = activity.relatedDeptNames as unknown as string;
if (typeof deptNamesStr === 'string' && deptNamesStr.includes(',')) {
return deptNamesStr.split(',').map(name => name.trim()).filter(name => name);
}
// 如果是单个字符串
if (typeof deptNamesStr === 'string' && deptNamesStr.trim()) {
return [deptNamesStr.trim()];
}
}
// 兼容旧的单部门字段
if (activity.relatedDeptName) {
if (activity.relatedDeptName.includes(',')) {
return activity.relatedDeptName.split(',').map(name => name.trim()).filter(name => name);
} else {
return [activity.relatedDeptName];
}
}
return [];
};
/** 计算活动的实际状态(基于时间比较) */
const calculateActivityStatus = (activity: MyActivityVO): string => {
// 如果活动状态是草稿(0)、提交审核(1)、待审批(2)、已驳回(7)、已取消(6),直接返回原状态
const originalStatus = activity.activityStatus;
if (['0', '1', '2', '6', '7'].includes(originalStatus)) {
return originalStatus;
}
// 只有已通过(3)、进行中(4)、已结束(5)状态的活动需要根据时间动态计算
if (!['3', '4', '5'].includes(originalStatus)) {
return originalStatus;
}
// 如果没有开始时间和结束时间,返回原状态
if (!activity.startTime || !activity.endTime) {
return originalStatus;
}
const now = new Date();
const startTime = new Date(activity.startTime);
const endTime = new Date(activity.endTime);
// 根据时间比较确定实际状态
if (now < startTime) {
return '3'; // 已通过(活动还未开始)
} else if (now >= startTime && now <= endTime) {
return '4'; // 进行中(活动正在进行)
} else {
return '5'; // 已结束(活动已结束)
}
};
/** 对话框关闭时重置队伍信息 */
const handleDetailClose = () => {
teamInfo.value = null;
@@ -1423,4 +1510,120 @@ onMounted(() => {
display: flex;
justify-content: center;
}
/* 部门标签样式 */
.dept-list-table {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
align-items: center;
justify-content: center;
width: 100%;
}
.dept-tag-table {
margin: 0;
font-size: 12px;
line-height: 1.3;
padding: 2px 6px;
border-radius: 2px;
white-space: nowrap;
overflow: visible;
text-overflow: unset;
flex-shrink: 0;
max-width: none;
}
/* 详情页面部门标签样式 */
.dept-list-detail {
display: flex;
flex-wrap: wrap;
gap: 6px 8px;
align-items: center;
margin-top: 4px;
}
.dept-tag-detail {
margin: 0;
font-size: 13px;
line-height: 1.4;
padding: 4px 8px;
border-radius: 4px;
white-space: nowrap;
flex-shrink: 0;
}
/* 活动详情描述列表优化 */
.activity-detail-descriptions {
width: 100%;
}
.activity-detail-descriptions :deep(.el-descriptions__table) {
table-layout: fixed;
width: 100%;
}
.activity-detail-descriptions :deep(.el-descriptions__label) {
width: 120px !important;
min-width: 120px;
padding: 12px 16px;
background-color: #fafafa;
font-weight: 600;
color: #303133;
}
.activity-detail-descriptions :deep(.el-descriptions__content) {
width: auto !important;
padding: 12px 16px;
word-break: break-word;
overflow-wrap: break-word;
}
/* 针对两列布局的优化 */
.activity-detail-descriptions :deep(.el-descriptions__table tr) {
display: table-row;
}
.activity-detail-descriptions :deep(.el-descriptions__table td) {
vertical-align: top;
}
/* 第一列(左侧)宽度控制 */
.activity-detail-descriptions :deep(.el-descriptions__table td:nth-child(1)) {
width: 120px;
}
.activity-detail-descriptions :deep(.el-descriptions__table td:nth-child(2)) {
width: calc(50% - 60px);
}
/* 第二列(右侧)宽度控制 */
.activity-detail-descriptions :deep(.el-descriptions__table td:nth-child(3)) {
width: 120px;
}
.activity-detail-descriptions :deep(.el-descriptions__table td:nth-child(4)) {
width: calc(50% - 60px);
}
/* 优化后的部门标签样式 */
.dept-list-detail-optimized {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
align-items: center;
margin-top: 4px;
width: 100%;
}
.dept-tag-detail-optimized {
margin: 0;
font-size: 13px;
line-height: 1.4;
padding: 3px 8px;
border-radius: 4px;
white-space: nowrap;
flex-shrink: 0;
/* 移除max-width限制,让标签自适应内容宽度 */
}
</style>
+50 -3
View File
@@ -61,7 +61,7 @@
</template>
</el-table-column>
<el-table-column label="联系电话" align="center" prop="phone" min-width="55" />
<el-table-column label="加入社团" align="center" width="160">
<el-table-column label="加入社团" align="center" width="120">
<template #default="scope">
<el-button
v-if="scope.row.clubCount > 0"
@@ -75,7 +75,7 @@
<span v-else class="no-data">0</span>
</template>
</el-table-column>
<el-table-column label="加入志愿者队伍" align="center" width="170">
<el-table-column label="加入志愿者队伍" align="center" width="130">
<template #default="scope">
<el-button
v-if="scope.row.volunteerTeamCount > 0"
@@ -89,7 +89,7 @@
<span v-else class="no-data">0</span>
</template>
</el-table-column>
<el-table-column label="参与活动" align="center" width="160">
<el-table-column label="参与活动" align="center" width="120">
<template #default="scope">
<el-button
v-if="scope.row.activityCount > 0"
@@ -103,6 +103,24 @@
<span v-else class="no-data">0</span>
</template>
</el-table-column>
<el-table-column label="今日活动" align="center" width="240">
<template #default="scope">
<span v-if="scope.row.todayActivityName" class="activity-name">
{{ scope.row.todayActivityName }}
</span>
<span v-else class="no-data">暂无今日活动</span>
</template>
</el-table-column>
<el-table-column label="活动时间" align="center" width="160">
<template #default="scope">
<div v-if="scope.row.todayActivityStartTime && scope.row.todayActivityEndTime" class="activity-time">
<div class="time-start">{{ scope.row.todayActivityStartTime }}</div>
<div class="time-separator">~</div>
<div class="time-end">{{ scope.row.todayActivityEndTime }}</div>
</div>
<span v-else class="no-data">-</span>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
@@ -452,4 +470,33 @@ onMounted(() => {
.activity-item:last-child {
margin-bottom: 0;
}
/* 今日活动和活动时间样式 */
.activity-name {
color: #409eff;
font-weight: 500;
line-height: 1.4;
word-break: break-word;
}
.activity-time {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
line-height: 1.3;
}
.time-start,
.time-end {
color: #606266;
font-size: 13px;
font-weight: 500;
}
.time-separator {
color: #909399;
font-size: 12px;
margin: 0 4px;
}
</style>