Files
hmc_gbhd/frontend/src/pages/activity/team/index.vue
T
2025-05-06 18:06:33 +08:00

669 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-container">
<view v-if="team">
<t-sticky>
<t-dropdown-menu class="top-dropdown-menu">
<t-dropdown-item :options="actOptions" :value="pageForm.actId" @change="actChange"/>
</t-dropdown-menu>
</t-sticky>
<!-- 团队卡片 -->
<div class="content-area">
<div class="team-card">
<div class="team-header">
<div class="team-logo">
<i class="bi bi-people-fill" v-if="!team.logoUrl"></i>
<img :src="team.logoUrl" alt="团队logo" v-if="team.logoUrl">
</div>
<div class="team-basic-info">
<div class="team-name">{{ team.groupName }}</div>
<div class="team-meta">创建于 {{ moment(team.createdAt).format("YYYY-MM-DD HH:mm") }}</div>
</div>
</div>
<div class="team-stats">
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-people"></i></div>
<div class="stat-content">
<div class="stat-value">成员:{{ team.groupUsers?.length }}</div>
<!-- <div class="stat-label">成员</div>-->
</div>
</div>
<div class="stat-divider"></div>
<div class="stat-item" @click="queryRank">
<div class="stat-icon"><i class="bi bi-trophy"></i></div>
<div class="stat-content">
<div class="stat-value">排名:{{ team.totalRank ? team.totalRank : '--' }}</div>
<div class="stat-label">点击查看排行榜</div>
</div>
</div>
</div>
</div>
<!-- 项目报名按钮 -->
<div class="action-section">
<t-button block theme="danger" size="large" class="register-btn" @click="goToEventRegistration"
:disabled="!team.teamIsLeader">
<div v-if="team.teamIsLeader"> {{ team.isSelfLeader ? '参与项目报名' : '查看项目报名' }}</div>
<div v-else>暂无队长无法报名</div>
</t-button>
</div>
<!-- 团队成员列表 -->
<div class="members-section">
<div class="section-header">
<div class="section-title">
<i class="bi bi-people-fill"></i>
团队成员
</div>
<div class="section-badge">{{ team.groupUsers.length }}</div>
</div>
<div class="member-grid">
<div class="member-grid-item" v-for="member in team.groupUsers" :key="member.id"
@click="openUserDetail(member)">
<div class="member-name">
{{ member.userName }}
<t-tag theme="danger" size="small" v-if="member.isLeader">队长</t-tag>
</div>
<div class="member-details">
<span><i class="bi bi-gender-ambiguous"></i>{{ member.sex }}</span>
<span><i class="bi bi-building"></i>{{ member.unitName }}</span>
</div>
</div>
</div>
<!-- 当前角色与申请队长 -->
<div class="role-section">
<div class="role-card" v-if="team.isSelfLeader">
<div class="role-icon">
<i class="bi bi-trophy"></i>
</div>
<div class="role-info">
<div class="role-title">您是队长</div>
<div class="role-desc">拥有团队所有管理权限</div>
</div>
</div>
<div v-if="team.teamIsLeader&&!team.isSelfLeader">
<div class="role-card">
<div class="role-icon">
<i class="bi bi-person"></i>
</div>
<div class="role-info">
<div class="role-title">您是普通成员</div>
<div class="role-desc">项目报名请联系队长</div>
</div>
</div>
</div>
<div v-if="!team.teamIsLeader&&!team.isSelfLeader">
<div class="role-card">
<div class="role-icon">
<i class="bi bi-person"></i>
</div>
<div class="role-info">
<div class="role-title">您是普通成员</div>
<div class="role-desc">申请成为队长可获得更多权限</div>
</div>
</div>
<t-button block
class="leader-apply-btn"
theme="danger"
variant="outline"
@click="leaderConfirmVisible = true">
申请成为队长
</t-button>
</div>
</div>
</div>
</div>
</view>
<view v-else style="flex: 1;display: flex;justify-content: center;align-items: center">
<t-empty v-if="actOptions && actOptions.length === 0"
image="/static/img/empty.png"
description="您没有任何团队信息"
style="flex: 1">
</t-empty>
</view>
</view>
<t-dialog
v-model:visible="leaderConfirmVisible"
title="申请成为队长"
cancel-btn="取消"
confirm-btn="确认"
@confirm="onLeaderConfirm"
@cancel="leaderConfirmVisible = false"
>
<template #content>
<div class="dialog-content">
<div class="dialog-icon">
<i class="bi bi-info-circle"></i>
</div>
<div class="dialog-text">
<p>申请成为队长前,请确认您已与团队其他成员商议并达成一致意见。</p>
<p>一个团队仅能有一名队长,作为队长,您可以帮助队员一键报名项目。</p>
</div>
</div>
</template>
</t-dialog>
<t-popup v-model:visible="showUserDetailPicker" placement="bottom" destroy-on-close style="height: 50%">
<view class="member-picker">
<view class="picker-header">
<text class="picker-title">成员信息</text>
<t-icon name="close" @click="showUserDetailPicker = false"/>
</view>
<view class="member-list">
<t-cell-group bordered>
<t-cell title="姓名" hover>{{ userInfo.userName }}</t-cell>
<t-cell title="性别" hover>{{userInfo.sex}}</t-cell>
<t-cell title="联系方式" hover>{{userInfo.phone}}</t-cell>
<t-cell title="职务" hover>{{userInfo.position}}</t-cell>
<t-cell title="所属单位" hover>{{userInfo.unitName}}</t-cell>
</t-cell-group>
</view>
</view>
</t-popup>
</template>
<script setup>
import {ref, reactive, onMounted, computed} from 'vue';
import {Dialog, Toast} from 'tdesign-mobile-vue';
import {useRequest} from "@/utils/request";
import moment from "moment";
import {onLoad} from "@dcloudio/uni-app";
const actList = ref([])
const showDesc = ref(false)
const actOptions = computed(() => {
return actList.value.map(item => {
return {
value: item.id,
label: item.title
}
})
})
const pageForm = reactive({
actId: null,
})
const teamId = ''
let team = ref(null);
// 申请成为队长弹框提示
const leaderConfirmVisible = ref(false)
// 用户详情弹框
const showUserDetailPicker = ref(false)
const userInfo = ref({})
// 打开用户详情
const openUserDetail = (val) => {
showUserDetailPicker.value = true
userInfo.value = val
console.log(val)
}
const actChange = (val) => {
getListAct()
}
// 申请成为队长
const onLeaderConfirm = async () => {
try {
pageForm.groupId = team.value.id
const {code, msg} = await useRequest().post('/client/group/apply-leader/' + team.value.id);
if (code === 0) {
await getTeamInfo();
Toast({message: '申请成功', theme: 'success'});
leaderConfirmVisible.value = false
} else {
Toast({message: msg, theme: 'error'});
}
} catch (e) {
Toast({message: e, theme: 'error'});
}
}
// 点击参与项目报名
const goToEventRegistration = () => {
uni.navigateTo({url: '/pages/activity/team-form/index?id=' + pageForm.actId})
};
// 根据活动查询队伍信息
const getTeamInfo = async () => {
try {
const {code, msg, data} = await useRequest().post('/client/group/info/' + pageForm.actId);
if (code === 0) {
team.value = data
team.value.teamIsLeader = !!team.value.groupUsers.find(t => t.isLeader)
} else {
Toast({message: msg, theme: 'error'});
}
} catch (e) {
Toast({message: e, theme: 'error'});
}
};
// 查询自己已经签到的活动
const getListAct = async () => {
try {
const {code, msg, data} = await useRequest().post('/client/group/list-act');
if (code === 0) {
actList.value = data;
if (data.length > 0) {
pageForm.actId = data[0].id;
await getTeamInfo();
}
}
} catch (error) {
Toast({message: error, theme: 'error'});
}
};
// 点击查看成绩排名
const queryRank = () => {
uni.navigateTo({url: '/pages/activity/rank/index?id=' + pageForm.actId})
}
// 初始化
onLoad(() => {
getListAct();
});
</script>
<style scoped lang="less">
:deep(.top-dropdown-menu) {
background-color: #fff;
padding: 10px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
z-index: 999;
}
.content-area {
padding: 0 16px 24px;
}
/*用户弹框*/
.member-picker {
background-color: #fff;
border-radius: 16px 16px 0 0;
padding: 20px;
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.picker-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
}
.picker-subtitle {
font-size: 14px;
color: #ff5f5c;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.member-list {
max-height: 50vh;
overflow-y: auto;
margin-bottom: 20px;
padding: 0 4px;
}
.member-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&--disabled {
opacity: 0.7;
}
.member-status {
font-size: 12px;
color: #52c41a;
}
:deep(.t-checkbox) {
margin: 0;
}
}
.picker-footer {
padding-top: 12px;
}
}
/* 团队卡片 */
.team-card {
background-color: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(255, 95, 92, 0.08);
margin: 12px 0 20px;
}
.team-header {
display: flex;
align-items: center;
padding: 16px;
background: linear-gradient(to right, rgba(255, 95, 92, 0.05), rgba(255, 95, 92, 0.02));
border-bottom: 1px solid rgba(255, 95, 92, 0.1);
}
.team-logo {
width: 48px;
height: 48px;
border-radius: 12px;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(255, 95, 92, 0.15);
overflow: hidden;
flex-shrink: 0;
}
.team-logo i {
font-size: 24px;
color: #ff5f5c;
}
.team-logo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.team-basic-info {
margin-left: 12px;
flex: 1;
}
.team-name {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.team-meta {
font-size: 12px;
color: #999;
}
.team-stats {
display: flex;
padding: 12px 16px;
}
.stat-item {
flex: 1;
display: flex;
align-items: center;
}
.stat-icon {
width: 36px;
height: 36px;
border-radius: 18px;
background-color: rgba(255, 95, 92, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
color: #ff5f5c;
font-size: 18px;
}
.stat-content {
display: flex;
flex-direction: column;
}
.stat-divider {
width: 1px;
height: 36px;
background-color: #f0f0f0;
margin: 0 16px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #333;
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: #999;
}
.team-desc-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
font-size: 14px;
color: #666;
background-color: #f9f9f9;
border-top: 1px solid #f0f0f0;
cursor: pointer;
}
.team-desc-toggle i {
font-size: 12px;
color: #999;
}
.team-desc {
padding: 12px 16px;
font-size: 14px;
color: #666;
line-height: 1.5;
position: relative;
background-color: #fff8f8;
border-top: 1px solid #f0f0f0;
}
.team-desc i {
color: #ffdddd;
font-size: 18px;
position: absolute;
top: 8px;
left: 10px;
}
/* 操作按钮 */
.action-section {
margin-bottom: 20px;
}
.register-btn {
height: 48px;
font-size: 16px;
background-color: #ff5f5c;
border-color: #ff5f5c;
box-shadow: 0 6px 16px rgba(255, 95, 92, 0.2);
}
/* 成员列表 */
.members-section {
background-color: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 8px;
color: #ff5f5c;
font-size: 18px;
}
.section-badge {
display: flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
background-color: #fff0f0;
color: #ff5f5c;
border-radius: 12px;
font-size: 13px;
font-weight: 500;
padding: 0 8px;
}
.member-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 12px;
}
.member-grid-item {
background-color: #f9f9f9;
border-radius: 8px;
padding: 10px;
}
.member-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 2px;
display: flex;
align-items: center;
justify-content: space-between;
}
.member-details {
display: flex;
flex-direction: column;
font-size: 12px;
color: #999;
}
.member-details span {
display: flex;
align-items: center;
margin-top: 2px;
}
.member-details i {
margin-right: 4px;
font-size: 12px;
color: #ccc;
}
/* 角色相关 */
.role-section {
padding: 16px;
}
.role-card {
display: flex;
align-items: center;
background-color: #fff8f8;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(255, 95, 92, 0.05);
}
.role-icon {
width: 40px;
height: 40px;
border-radius: 20px;
background: linear-gradient(120deg, #ff5f5c, #ff8c8a);
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
box-shadow: 0 4px 12px rgba(255, 95, 92, 0.2);
}
.role-icon i {
font-size: 20px;
color: white;
}
.role-info {
flex: 1;
}
.role-title {
font-size: 15px;
font-weight: 500;
color: #333;
margin-bottom: 2px;
}
.role-desc {
font-size: 13px;
color: #666;
}
.leader-apply-btn {
margin-top: 8px;
border-radius: 12px;
height: 48px;
border-color: #ff5f5c;
color: #ff5f5c;
}
/* 弹窗内容样式 */
.dialog-content {
display: flex;
padding: 16px 0;
}
.dialog-icon {
margin-right: 16px;
font-size: 24px;
color: #ff9800;
}
.dialog-text p {
margin: 0 0 8px;
font-size: 14px;
color: #666;
line-height: 1.6;
}
</style>