This commit is contained in:
那些花儿
2025-04-28 15:32:41 +08:00
parent cb3bcb57bc
commit 12c3afb4a6
42 changed files with 3272 additions and 1627 deletions
@@ -0,0 +1,80 @@
<template>
<view class="page-container">
<view style="position:relative;width: 100%;height: 400rpx;overflow: hidden;margin-bottom: 0rpx">
<image :src="actData.cover" mode="aspectFill" style="width: 100%"></image>
</view>
</view>
</template>
<script setup>
import {onLoad, onPageScroll} from "@dcloudio/uni-app";
import {Toast} from "tdesign-mobile-vue";
import {ref} from 'vue'
import {useRequest} from "@/utils/request";
import moment from "moment/moment";
const request = useRequest()
const actId = ref('');
const actData = ref({});
const signUpInfo = ref({})
onLoad((option) => {
if (option.id) {
actId.value = option.id;
getDetailData(option.id);
getSignUpInfo(option.id);
} else {
Toast({message: 'ID不存在', theme: 'error'});
}
});
// 获取详情
const getDetailData = async (id) => {
const loading = Toast({
theme: 'loading',
duration: 0
});
try {
const {code, data} = await request.get('/client/activity/get/' + id)
if (code === 0) {
actData.value = data;
// 如果活动未开始,开始倒计时
// if (activityStatus.value === 'notStarted') {
// startCountdown();
// }
} else {
Toast({message: '获取详情失败', theme: 'error'});
}
} catch (error) {
console.error('获取详情失败', error);
Toast({message: '获取详情失败', theme: 'error'});
} finally {
loading.clear()
}
};
// 查询报名信息
const getSignUpInfo = async () => {
try {
const {code, data} = await request.get('/client/activity/sign-info/' + actId.value);
if (code === 0 && data) {
signUpInfo.value = data;
} else {
Toast({message: '查询报名信息失败', theme: 'error'});
}
} catch (e) {
console.error(e)
}
}
</script>
<style scoped lang="less">
.page-container {
position: relative;
width: 100%;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
<script setup>
</script>
<template>
</template>
<style scoped>
</style>
+58 -197
View File
@@ -1,6 +1,5 @@
<template>
<view class="page-container">
<!-- 顶部搜索和筛选 -->
<view class="header">
<view class="search-container">
<t-search
@@ -8,65 +7,66 @@
placeholder="搜索活动名称"
shape="round"
class="search-input"
@search="handleSearch"
@onSearch="handleSearch"
/>
</view>
<!-- 状态筛选标签组 -->
<view class="filter-tabs">
<scroll-view scroll-x class="tabs-scroll-view" :show-scrollbar="false">
<view class="tabs-container">
<view
v-for="option in statusOptions"
:key="option.value"
class="filter-tab"
:class="{ active: pageForm.status === option.value }"
@click="selectStatusFilter(option.value)"
>
{{ option.label }}
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 活动列表 - 使用scroll-view实现 -->
<scroll-view
scroll-y
class="list-scroll-view"
@scrolltolower="handleScrollToBottom"
refresher-enabled
:refresher-triggered="refresherTriggered"
@refresherrefresh="handleRefresh"
refresher-background="#f2f3f5"
<scroll-view
scroll-y
class="list-scroll-view"
@scrolltolower="handleScrollToBottom"
refresher-enabled
:refresher-triggered="refresherTriggered"
@refresherrefresh="handleRefresh"
refresher-background="#f2f3f5"
>
<view class="list-container">
<view
class="activity-card"
class="card-item"
v-for="item in dataList"
:key="item.id"
@click="navigateToDetail(item.id)"
>
<view class="image-container">
<image :src="item.cover" mode="aspectFill" class="activity-image"/>
<view class="activity-time">
{{ formatTimeRange(item.startTime, item.endTime) }}
</view>
<view class="status-tag-container">
<t-tag
:theme="getTagTheme(getActivityStatus(item))"
size="medium"
shape="mark"
>{{ getStatusText(getActivityStatus(item)) }}
</t-tag>
</view>
</view>
<view class="activity-info">
<text class="activity-title">{{ item.title }}</text>
<view class="activity-address">
<t-icon name="location" size="small"/>
<text>{{ item.address || '暂无地址信息' }}</text>
<view>
<view style="display: flex;align-items: center;margin-bottom: 10px">
<image
src="https://assets.cumen.fun/upload/017cdaf1-c5fa-788e-eea2-d0ee249f902b/0187376a-3cfa-07b3-3cd6-7479bf463865?x-oss-process=style/720-jpg"
style="width: 30px;height: 30px;flex-shrink: 0"
></image>
<view
style="margin-left: 10px;font-weight: 600;white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
{{ item.title }}
</view>
</view>
<view style="display: flex;">
<image :src="item.cover" mode="aspectFill" style="flex-shrink:0;width: 120px;height: 90px;border-radius: 5px;"></image>
<view
style="margin-left:10px;flex:1;overflow:hidden;display: flex;flex-direction: column;justify-content:space-around;color: #534d4d;">
<view
style="display: flex; align-items: center; column-gap: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t-icon name="time"></t-icon>
{{ moment(item.startTime).format('MM-DD') }}
</view>
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t-icon name="location"></t-icon>
{{ item.address }}
</view>
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t-icon name="castle-7"></t-icon>
{{ item.address }}
</view>
</view>
</view>
<view style="border-top: 1px solid #f0f0f0;margin-top: 10px;padding-top: 12px;display: flex;align-items: center;justify-content: space-between">
<view>
100人已报名
</view>
<!-- <image src="https://assets.cumen.fun/upload/017cdaf1-c5fa-788e-eea2-d0ee249f902b/01892580-5db0-be0b-5643-35d6c5a83591"-->
<!-- style="width: 105px;height: 40px"-->
<!-- ></image>-->
<t-button theme="primary" shape="round" size="small">立即报名</t-button>
</view>
</view>
</view>
@@ -89,8 +89,6 @@
<script setup>
import {ref, reactive, onMounted} from 'vue';
// 移除不再需要的onReachBottom和onPullDownRefresh
// import {onReachBottom, onPullDownRefresh} from "@dcloudio/uni-app";
import {Toast} from 'tdesign-mobile-vue';
import {useRequest} from "@/utils/request";
import moment from "moment";
@@ -118,57 +116,8 @@ const pageForm = reactive({
const dataList = ref([]);
const loading = ref(false);
const hasMore = ref(true);
// 添加下拉刷新状态
const refresherTriggered = ref(false);
// 获取活动状态
const getActivityStatus = (item) => {
const now = moment();
const signStartTime = moment(item.signStartTime);
const signEndTime = moment(item.signEndTime);
const activityStartTime = moment(item.startTime);
const activityEndTime = moment(item.endTime);
if (now.isBefore(signStartTime)) {
return 'notStartSign'; // 未开始报名
} else if (now.isBetween(signStartTime, signEndTime)) {
return 'signing'; // 报名中
} else if (now.isBetween(signEndTime, activityStartTime)) {
return 'notStartActivity'; // 活动未开始
} else if (now.isBetween(activityStartTime, activityEndTime)) {
return 'activitying'; // 活动进行中
} else {
return 'ended'; // 已结束
}
};
// 获取状态文本
const getStatusText = (status) => {
return {
'notStartSign': '未开始报名',
'signing': '报名中',
'notStartActivity': '活动未开始',
'activitying': '活动进行中',
'ended': '已结束'
}[status] || '未知状态';
};
// 获取标签主题
const getTagTheme = (status) => {
return {
'notStartSign': 'primary',
'signing': 'success',
'notStartActivity': 'warning',
'activitying': 'info',
'ended': 'default'
}[status] || 'default';
};
// 格式化时间范围
const formatTimeRange = (startTime, endTime) => {
return `${moment(startTime).format('MM-DD')} ~ ${moment(endTime).format('MM-DD')}`;
};
// 选择状态筛选
const selectStatusFilter = (status) => {
pageForm.status = status;
@@ -221,13 +170,13 @@ const getListData = async (isRefresh = false) => {
}
};
// scroll-view 下拉刷新
//下拉刷新
const handleRefresh = () => {
if (loading.value) {
refresherTriggered.value = false;
return;
}
refresherTriggered.value = true;
pageForm.pageNumber = 1;
getListData(true);
@@ -241,7 +190,7 @@ const handleSearch = () => {
getListData(true);
};
// scroll-view 触底加载更多
// 触底加载更多
const handleScrollToBottom = () => {
if (loading.value || !hasMore.value) return;
getListData();
@@ -276,38 +225,6 @@ onMounted(() => {
width: 100%;
}
.filter-tabs {
margin-top: 20rpx;
padding: 0 12rpx;
}
.tabs-scroll-view {
white-space: nowrap;
width: 100%;
}
.tabs-container {
display: inline-flex;
padding: 0 12rpx;
}
.filter-tab {
display: inline-block;
padding: 10rpx 24rpx;
margin: 0 12rpx;
font-size: 26rpx;
color: #666;
background-color: #f5f5f5;
border-radius: 48rpx;
transition: all 0.3s;
&.active {
color: #fff;
background-color: var(--td-brand-color);
font-weight: 500;
}
}
.list-scroll-view {
flex: 1;
height: 0;
@@ -319,82 +236,26 @@ onMounted(() => {
padding: 20rpx 24rpx;
}
.activity-card {
.card-item {
width: 100%;
margin-bottom: 24rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
transition: transform 0.3s;
//background: url("https://assets.cumen.fun/upload/017cdaf1-c5fa-788e-eea2-d0ee249f902b/0189257e-fc75-5b89-b900-104c706f2d63") top / 100% auto no-repeat,
//linear-gradient(to right, white, #ecb5b5);
//background: no-repeat, linear-gradient(to right, white, #f6d4d4);
background: #ffffff;
&:active {
transform: scale(0.98);
}
}
.image-container {
position: relative;
height: 360rpx;
overflow: hidden;
}
.activity-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s;
.activity-card:hover & {
transform: scale(1.05);
}
}
.activity-time {
position: absolute;
left: 16rpx;
bottom: 16rpx;
padding: 6rpx 16rpx;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 22rpx;
border-radius: 6rpx;
}
.activity-info {
padding: 20rpx 24rpx 10rpx;
}
.activity-title {
width: 100%;
font-size: 32rpx;
font-weight: 500;
color: #333;
line-height: 1.4;
display: block;
}
.activity-address {
display: flex;
align-items: center;
font-size: 26rpx;
color: #666;
margin-top: 12rpx;
}
.activity-address :deep(.t-icon) {
margin-right: 8rpx;
color: #999;
}
.status-tag-container {
position: absolute;
right: 0;
top: 16rpx;
}
.loading-container {
padding: 40rpx 0;
text-align: center;
}
</style>
@@ -1,13 +1,13 @@
<template>
<view class="page-container">
<!-- <view class="act-info">-->
<!-- <view class="act-title">奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑</view>-->
<!-- <view class="act-meta">-->
<!-- <view class="act-meta-item">22222</view>-->
<!-- <view class="act-meta-item">22222222222222222222</view>-->
<!-- </view>-->
<!-- </view>-->
<!-- <view class="act-info">-->
<!-- <view class="act-title">奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑</view>-->
<!-- <view class="act-meta">-->
<!-- <view class="act-meta-item">22222</view>-->
<!-- <view class="act-meta-item">22222222222222222222</view>-->
<!-- </view>-->
<!-- </view>-->
<view class="form-area">
<t-form
@@ -44,7 +44,7 @@
<view class="user-card-header-title">
<span>{{ index + 1 }}</span>
</view>
<view class="remove" @click="formData.userList.splice(index, 1)">
<view class="remove" @click="removeUser(index)">
<t-icon name="delete" size="18px"></t-icon>
</view>
</view>
@@ -59,7 +59,8 @@
<t-radio :block="false" label="女" value="女"/>
</t-radio-group>
</t-form-item>
<t-form-item label="职务" :name="`userList[${index}].position`" :rules="[{required:true,message: '请输入职务'}]">
<t-form-item label="职务" :name="`userList[${index}].position`"
:rules="[{required:true,message: '请输入职务'}]">
<t-input v-model="user.position" borderless placeholder="请输入职务"></t-input>
</t-form-item>
<t-form-item label="身份证号" :name="`userList[${index}].idCard`"
@@ -74,7 +75,7 @@
:rules="[{required:true,message: '请选择是否住宿'}]">
<t-radio-group v-model="user.isStay" class="radio-box">
<t-radio :block="false" name="radio" value="1" label="是"/>
<t-radio :block="false" name="radio" value="2" label="否"/>
<t-radio :block="false" name="radio" value="0" label="否"/>
</t-radio-group>
</t-form-item>
</view>
@@ -109,36 +110,141 @@
<script setup>
import {ref} from 'vue'
import {useRequest} from '@/utils/request';
import {Toast} from "tdesign-mobile-vue";
import {Dialog, Toast} from "tdesign-mobile-vue";
import {onLoad} from "@dcloudio/uni-app";
const request = useRequest()
const actId = ref(null)
const agreementChecked = ref(false)
const formRef = ref(null)
const formData = ref({
unitName: null,
userList: []
unitName: "测试",
contactName: "测试",
contactPhone: "18899998888",
userList: [{
userName: "测试",
sex: "男",
position: "测试",
idCard: "421087200004040010",
phone: "18899998888",
isStay: "1"
}]
})
onLoad((option) => {
if (option.id) {
actId.value = option.id
formData.value.activityId = option.id
listSignUpUser()
} else {
Toast({message: 'ID不存在', theme: 'error'});
}
})
// 查询报名人员信息
async function listSignUpUser() {
try {
const {code, data} = await request.get('/client/activity/list-signup-user/' + actId.value)
if (code === 0 && data) {
if (data.length > 0) {
formData.value.userList = data
}
} else {
Toast({message: '查询报名人员信息失败', theme: 'error'});
}
} catch (e) {
console.error(e)
}
}
// 表单验证
async function validaForm() {
return new Promise((resolve, reject) => {
formRef.value.validate().then(async (valid) => {
resolve(valid === true)
}).catch((error) => {
reject(error)
})
})
}
// 承诺书
async function validateAgreement() {
return new Promise((resolve, reject) => {
if (agreementChecked.value) {
resolve(true)
return
}
Dialog.confirm({
title: '提示',
content: '参赛条款与免责声明:阅读并同意本承诺书内容,点击"确定并报名"即表示您已充分理解并接受所有条款,承担相应法律责任。',
confirmBtn: '确定并报名',
cancelBtn: '取消',
onConfirm: () => {
agreementChecked.value = true
resolve(true)
},
onCancel: () => {
agreementChecked.value = false
resolve(false)
}
})
})
}
// 提交表单
async function onSubmit() {
console.log(formRef)
console.log(formRef.value)
formRef.value.validate()
try{
const {code,data} = await request.post('/client/activity/sign', formData.value)
const formRes = await validaForm()
if (!formRes) return
const agreementRes = await validateAgreement()
if (!agreementRes) return
// 提交表单
await submitForm()
}
// 报名
async function submitForm() {
uni.showLoading({
title: '提交中...',
mask: true,
})
try {
const {code, data, msg} = await request.post('/client/activity/sign-up', formData.value)
if (code === 0) {
Toast.success('报名成功')
// setTimeout(() => {
// uni.navigateBack()
// }, 1500)
}else{
Toast.error(data.msg)
setTimeout(() => {
uni.navigateTo({
url: '/pages/activity/registration-form/success?id=' + actId.value
})
}, 500)
} else {
Dialog.alert({
title: '报名失败',
content: msg,
confirmBtn: '确定'
})
}
}catch (e) {
} catch (e) {
console.log(e)
Toast.error(e.message)
} finally {
uni.hideLoading()
}
}
// 移除人员
async function removeUser(index) {
if (formData.value.userList.length === 1) {
Toast('至少需要一个报名人员')
return
}
formData.value.userList.splice(index, 1)
}
</script>
<style scoped lang="less">
@@ -185,7 +291,7 @@ async function onSubmit() {
}
}
.form-area{
.form-area {
flex: 1;
overflow-y: auto;
//margin-top: 100px;
@@ -0,0 +1,188 @@
<template>
<view class="page-container">
<!-- 成功提示区域 -->
<view class="success-content">
<!-- 成功图片 -->
<view class="success-image">
<image src="https://cdn-icons-png.flaticon.com/512/10613/10613684.png" mode="aspectFit"></image>
</view>
<!-- 成功信息 -->
<view class="success-info">
<view class="success-title">报名成功</view>
<view class="success-subtitle">您已成功报名参加"{{ activityTitle }}"</view>
</view>
<!-- 小提示 -->
<view class="success-tips">
<t-icon name="info-circle-filled" size="16px" />
<text>活动开始前我们将通过短信通知您</text>
</view>
<!-- 底部按钮 -->
<view class="action-buttons">
<t-button theme="default" shape="round" @click="navigateToHome">返回首页</t-button>
<t-button theme="primary" shape="round" @click="viewRegistrationDetails">查看报名详情</t-button>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { Toast } from 'tdesign-mobile-vue';
import { useRequest } from '@/utils/request';
const request = useRequest();
const actId = ref(null);
const activityTitle = ref('活动名称');
// 获取路由参数
onLoad((option) => {
if (option.id) {
actId.value = option.id;
// 获取活动信息和标题
getActivityInfo(option.id);
} else {
Toast({message: 'ID不存在', theme: 'error'});
}
});
// 获取活动信息(只获取标题)
const getActivityInfo = async (id) => {
try {
const {code, data} = await request.get('/client/activity/get/' + id);
if (code === 0 && data) {
activityTitle.value = data.title;
}
} catch (error) {
console.error('获取活动信息失败', error);
}
};
// 返回首页
const navigateToHome = () => {
uni.switchTab({
url: '/pages/index/index'
});
};
// 查看报名详情
const viewRegistrationDetails = () => {
uni.navigateTo({
url: '/pages/activity/registration-details/index?id=' + actId.value
});
};
</script>
<style scoped lang="less">
.page-container {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-bottom: 50px;
background-color: #fff;
min-height: 100vh;
}
/* 成功内容区域 */
.success-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
}
/* 成功图片 */
.success-image {
margin-bottom: 30px;
width: 200px;
height: 200px;
image {
width: 100%;
height: 100%;
animation: float 3s ease-in-out infinite;
}
}
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
/* 成功信息 */
.success-info {
position: relative;
margin-bottom: 30px;
}
.success-title {
color: #333;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
}
.success-subtitle {
color: #666;
font-size: 16px;
margin-bottom: 20px;
}
/* 成功提示 */
.success-tips {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 95, 92, 0.1);
border-radius: 100px;
padding: 10px 20px;
margin-top: 20px;
margin-bottom: 40px;
.t-icon {
color: #ff5f5c;
margin-right: 6px;
}
text {
color: #666;
font-size: 14px;
}
}
/* 按钮区域 */
.action-buttons {
display: flex;
gap: 12px;
width: 100%;
padding: 0 20px;
margin-top: 20px;
:deep(.t-button) {
flex: 1;
}
:deep(.t-button--primary) {
background-color: #ff5f5c !important;
border-color: #ff5f5c !important;
}
:deep(.t-button--default) {
border-color: #ff5f5c !important;
color: #ff5f5c !important;
}
}
</style>
+397 -30
View File
@@ -1,53 +1,60 @@
<template>
<view class="page-container">
<t-dropdown-menu>
<t-dropdown-menu class="top-dropdown">
<t-dropdown-item :options="actOptions" :value="selectActId" @change="getSignInfo"/>
</t-dropdown-menu>
<view class="content-container">
<view class="scan-area" @click="handleScan">
<view class="scan-icon-wrapper">
<view class="scan-area animate__animated animate__fadeIn" @click="handleScan">
<view class="scan-icon-wrapper pulse-animation">
<t-icon name="scan" size="80" color="#f45a5a"></t-icon>
<view class="scan-circles"></view>
</view>
<t-button shape="round"
theme="primary"
size="medium">扫描二维码签到
size="medium"
class="scan-button">扫描二维码签到
</t-button>
</view>
<view class="guide-area">
<view class="guide-area animate__animated animate__fadeInUp">
<view class="guide-title">签到说明</view>
<view class="guide-list">
<view class="guide-item">
<uni-icons class="guide-icon" type="info" size="16" color="#999999"/>
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.1s">
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
<text class="guide-text">请对准活动二维码进行扫描</text>
</view>
<view class="guide-item">
<uni-icons class="guide-icon" type="info" size="16" color="#999999"/>
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.2s">
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
<text class="guide-text">确保手机相机权限已开启</text>
</view>
<view class="guide-item">
<uni-icons class="guide-icon" type="info" size="16" color="#999999"/>
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.3s">
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
<text class="guide-text">保持二维码清晰可见</text>
</view>
</view>
</view>
<view class="current-status-area">
<view class="current-status-area animate__animated animate__fadeInUp" style="animation-delay: 0.2s">
<view class="status-header">
<text class="status-title">当前签到状态</text>
</view>
<view v-if="signInfo" class="status-content">
<template v-if="signInfo.isSignIn">
<view class="sign-success">
<t-icon name="task-checked-1" size="48" color="#07c160"></t-icon>
<div class="success-icon-wrapper">
<t-icon name="task-checked-1" size="48" color="#07c160"></t-icon>
<svg class="checkmark-circle" viewBox="0 0 52 52">
<circle class="checkmark-circle__circle" cx="26" cy="26" r="25" fill="none" />
</svg>
</div>
<text class="success-text">已完成签到</text>
<view class="sign-detail">
<view class="detail-item">
<uni-icons type="calendar" size="14" color="#666666" />
<uni-icons type="calendar" size="14" color="#666666"/>
<text>签到时间:{{ signInfo.signInTime }}</text>
</view>
<view class="detail-item">
<uni-icons type="location" size="14" color="#666666" />
<text>签到地点:{{ signInfo.location }}</text>
</view>
<!-- <view class="detail-item">-->
<!-- <uni-icons type="location" size="14" color="#666666"/>-->
<!-- <text>签到地点:{{ signInfo.location }}</text>-->
<!-- </view>-->
</view>
</view>
</template>
@@ -61,6 +68,29 @@
</view>
</view>
</view>
<div class="bg-decoration">
<svg class="wave" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
<path fill="#f45a5a" fill-opacity="0.1" d="M0,160L48,154.7C96,149,192,139,288,144C384,149,480,171,576,176C672,181,768,171,864,154.7C960,139,1056,117,1152,128C1248,139,1344,181,1392,202.7L1440,224L1440,0L1392,0C1344,0,1248,0,1152,0C1056,0,960,0,864,0C768,0,672,0,576,0C480,0,384,0,288,0C192,0,96,0,48,0L0,0Z"></path>
</svg>
<svg class="dots" width="100" height="100" viewBox="0 0 100 100">
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="10" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="10" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="10" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="10" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="10" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="30" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="30" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="30" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="30" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="30" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="50" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="50" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="50" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="50" r="3" />
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="50" r="3" />
</svg>
</div>
</view>
<ScanQRCode
@@ -73,7 +103,7 @@
<script setup>
import {ref, computed, onMounted} from 'vue';
import {useRequest} from '@/utils/request';
import {Toast} from 'tdesign-mobile-vue';
import {Dialog, Toast} from 'tdesign-mobile-vue';
import moment from 'moment';
import ScanQRCode from '@/components/scanQrCode.vue'
@@ -99,15 +129,16 @@ onMounted(() => {
// 获取活动列表
async function listMineAct() {
try {
const {data, code} = await request.get('/client/activity/list-mine-act')
const {data, code, msg} = await request.get('/client/activity/list-mine-act')
if (code === 0) {
list.value = data
if (list.value.length) {
selectActId.value = list.value[0].id
console.log(selectActId.value)
await getSignInfo();
}
} else {
Toast.error(data.msg)
Toast.error(msg)
}
} catch (e) {
console.error(e)
@@ -116,9 +147,9 @@ async function listMineAct() {
}
// 获取签到信息
async function getSignInfo(val){
async function getSignInfo(val) {
try {
const {code, data, msg} = await request.get('/client/activity/sign-info/' + (val ? val :selectActId.value))
const {code, data, msg} = await request.get('/client/activity/sign-in-info/' + (val ? val : selectActId.value))
if (code === 0) {
signInfo.value = data
} else {
@@ -135,26 +166,125 @@ function handleScan() {
qrScannerRef.value.startScan()
}
function handleScanSuccess(val){
console.log(val)
Toast.success(val)
// 扫码成功回调
function handleScanSuccess(val) {
console.log('扫描成功,活动ID为' + val, "当前选择活动ID为" + selectActId.value)
if (!val) {
Toast.error("请扫描合法的二维码")
return
}
if (String(val) !== String(selectActId.value)) {
Toast.error('请扫码签到当前活动,扫描成功,活动ID为' + val + ",当前选择活动ID为" + selectActId.value)
return
}
signIn()
}
// 签到
async function signIn() {
try {
await uni.showLoading({
title: '签到中...',
mask: true,
})
const {code, msg} = await request.post('/client/activity/sign-in/' + selectActId.value)
uni.hideLoading()
Dialog.alert({
title: code === 0 ? '签到成功' : '签到失败',
content: code === 0 ? '祝您活动顺利' : msg,
confirmBtn: '确定'
})
if (code === 0) {
await getSignInfo()
}
} catch (e) {
Toast.error(e.message)
console.error(e)
} finally {
uni.hideLoading()
}
}
</script>
<style scoped lang="less">
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes ripple {
0% {
transform: scale(0.8);
opacity: 1;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
@keyframes checkmark-circle {
0% {
stroke-dashoffset: 166;
}
100% {
stroke-dashoffset: 0;
}
}
@keyframes float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0);
}
}
.top-dropdown {
flex-shrink: 0;
z-index: 10;
position: relative;
}
.content-container {
padding: 15px;
z-index: 9;
position: relative;
flex: 1;
overflow-y: auto;
}
.scan-area {
background-color: #ffffff;
border-radius: 16rpx;
padding: 64rpx 0;
border-radius: 16px;
padding: 64px 0;
margin-bottom: 32rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
}
.scan-icon-wrapper {
@@ -164,6 +294,62 @@ function handleScanSuccess(val){
align-items: center;
justify-content: center;
margin-bottom: 32rpx;
position: relative;
}
.pulse-animation {
animation: pulse 2s infinite ease-in-out;
}
.scan-circles {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
transform: translate(-50%, -50%);
&:before, &:after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 50%;
border: 2px solid rgba(244, 90, 90, 0.3);
animation: ripple 2s infinite;
}
&:after {
animation-delay: 0.5s;
}
}
.scan-button {
position: relative;
overflow: hidden;
transition: all 0.3s ease;
&:active {
transform: translateY(2px);
}
&:after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: rgba(255, 255, 255, 0.2);
transform: rotate(30deg);
transition: all 0.3s ease;
}
&:hover:after {
transform: rotate(30deg) translate(-30%, -30%);
}
}
.guide-area {
@@ -171,6 +357,9 @@ function handleScanSuccess(val){
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
}
.guide-title {
@@ -178,18 +367,37 @@ function handleScanSuccess(val){
font-weight: 500;
color: #333333;
margin-bottom: 24rpx;
position: relative;
&:after {
content: '';
position: absolute;
bottom: -8rpx;
left: 0;
width: 40rpx;
height: 4rpx;
background-color: #f45a5a;
border-radius: 2rpx;
}
}
.guide-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
padding: 12rpx 16rpx;
border-radius: 8rpx;
transition: all 0.3s ease;
&:hover {
background-color: rgba(244, 90, 90, 0.05);
}
}
.guide-icon {
width: 32rpx;
height: 32rpx;
margin-right: 16rpx;
margin-right: 16px;
flex-shrink: 0;
}
@@ -202,7 +410,9 @@ function handleScanSuccess(val){
background-color: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
}
.status-header {
@@ -214,6 +424,18 @@ function handleScanSuccess(val){
font-weight: 500;
color: #333333;
margin-bottom: 0.75rem;
position: relative;
&:after {
content: '';
position: absolute;
bottom: -8rpx;
left: 0;
width: 40rpx;
height: 4rpx;
background-color: #f45a5a;
border-radius: 2rpx;
}
}
.status-content {
@@ -230,6 +452,34 @@ function handleScanSuccess(val){
text-align: center;
}
.success-icon-wrapper {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 96rpx;
}
.checkmark-circle {
width: 96rpx;
height: 96rpx;
position: absolute;
top: 0;
left: 0;
&__circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke: #07c160;
stroke-miterlimit: 10;
fill: none;
animation: checkmark-circle 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
animation-delay: 0.2s;
}
}
.success-text {
font-size: 20px;
color: #07c160;
@@ -239,6 +489,9 @@ function handleScanSuccess(val){
.sign-detail {
margin-top: 32rpx;
background-color: rgba(7, 193, 96, 0.05);
padding: 16rpx 24rpx;
border-radius: 8rpx;
}
.detail-item {
@@ -247,6 +500,7 @@ function handleScanSuccess(val){
margin-bottom: 16rpx;
color: #666666;
font-size: 14px;
font-weight: 400;
}
.detail-item uni-icons {
@@ -265,4 +519,117 @@ function handleScanSuccess(val){
color: #999999;
}
.bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
overflow: hidden;
}
.wave {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200px;
}
.dots {
position: absolute;
bottom: 5%;
right: 5%;
animation: float 4s ease-in-out infinite;
}
/* 导入Animate.css的部分动画 */
.animate__animated {
animation-duration: 1s;
animation-fill-mode: both;
}
.animate__fadeIn {
animation-name: fadeIn;
}
.animate__fadeInUp {
animation-name: fadeInUp;
}
.animate__fadeInLeft {
animation-name: fadeInLeft;
}
.animate__bounceIn {
animation-name: bounceIn;
}
.animate__pulse {
animation-name: pulse;
}
.animate__infinite {
animation-iteration-count: infinite;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translate3d(0, 40px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translate3d(-40px, 0, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@keyframes bounceIn {
from, 20%, 40%, 60%, 80%, to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
20% {
transform: scale3d(1.1, 1.1, 1.1);
}
40% {
transform: scale3d(0.9, 0.9, 0.9);
}
60% {
opacity: 1;
transform: scale3d(1.03, 1.03, 1.03);
}
80% {
transform: scale3d(0.97, 0.97, 0.97);
}
to {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
</style>
@@ -0,0 +1,442 @@
<template>
<view class="page-container">
<t-dropdown-menu class="top-dropdown">
<t-dropdown-item :options="actOptions" :value="selectActId"/>
</t-dropdown-menu>
<view class="content-container">
<view class="page-header">
<text class="page-title">团队报名</text>
<text class="page-subtitle">选择您感兴趣的项目进行报名</text>
</view>
<!-- 项目列表 -->
<view class="project-cards">
<view
v-for="project in itemOptions"
:key="project.id"
class="project-card"
:class="{'project-card--registered': registrationStatus[project.id]?.length}"
@click="selectProject(project.id)"
>
<view class="project-card__content">
<view class="project-card__title">{{ project.name }}</view>
<view class="project-card__desc">{{ project.description }}</view>
</view>
<view class="project-card__status">
<t-tag v-if="registrationStatus[project.id]?.length" theme="success" variant="light-outline">
已报名 {{ registrationStatus[project.id]?.length }}人
</t-tag>
<view v-else class="project-card__action">
<t-icon name="chevron-right" color="#999"/>
</view>
</view>
<view v-if="registrationStatus[project.id]?.length" class="project-card__checkmark">
<t-icon name="check" color="#52c41a"/>
</view>
</view>
</view>
</view>
<t-popup v-model:visible="showMemberPicker" placement="bottom" destroy-on-close>
<view class="member-picker">
<view class="picker-header">
<text class="picker-title">选择项目参与成员</text>
<t-icon name="close" @click="showMemberPicker = false"/>
</view>
<view class="picker-subtitle" v-if="selectedItemId">
{{ itemOptions.find(p => p.id === selectedItemId)?.name }}
</view>
<view class="member-list">
<t-checkbox-group v-model="selectedMemberIds">
<view
v-for="member in teamMembers"
:key="member.id"
>
<t-checkbox
:value="member.id"
:label="member.userName"
:content="member.unitName"
:disabled="registrationStatus[selectedItemId]?.includes(member.id)"
icon-size="20px"
/>
<view class="member-status" v-if="registrationStatus[selectedItemId]?.includes(member.id)">
已报名
</view>
</view>
</t-checkbox-group>
</view>
<view class="picker-footer">
<t-button block theme="primary" @click="submitRegistration" :disabled="!selectedMemberIds.length">
确认报名 ({{ selectedMemberIds.length }}人)
</t-button>
</view>
</view>
</t-popup>
</view>
</template>
<script setup>
import {ref, onMounted, computed} from 'vue';
import {useRequest} from '@/utils/request';
import {Dialog, Toast} from 'tdesign-mobile-vue';
import {onLoad} from '@dcloudio/uni-app';
const request = useRequest();
// 活动数据
const actOptions = ref([])
// 选中的活动ID
const selectActId = ref(null);
// 项目列表数据
const itemOptions = ref([
{
id: 1,
name: '篮球比赛',
description: '3v3篮球友谊赛'
},
{
id: 2,
name: '足球比赛',
description: '5人制足球赛'
},
{
id: 3,
name: '羽毛球比赛',
description: '双打友谊赛'
}
]);
// 选中的项目ID
const selectedItemId = ref(null);
// 团队成员列表
const teamMembers = ref([]);
onLoad((option) => {
if (option.id) {
selectActId.value = option.id;
}
});
onMounted(async () => {
await listActs()
await listItems();
listTeamUser()
// fetchRegistrationStatus();
});
// 选中的团队成员
const selectedMemberIds = ref([]);
// 成员选择弹窗显示状态
const showMemberPicker = ref(false);
// 当前项目的报名状态
const registrationStatus = ref({});
// 获取活动列表
async function listActs() {
try {
const {data, code, msg} = await request.get('/client/activity/list-mine-act')
if (code === 0) {
actOptions.value = data && data.map(item => {
return {
value: item.id,
label: item.title
}
})
if(selectActId.value === null && data.length){
selectActId.value = data[0].id
}
} else {
Toast.error(msg)
}
} catch (e) {
console.error(e)
Toast.error(e.message)
}
}
// 获取项目列表
async function listItems() {
try {
const {code, data, msg} = await request.get('/client/item/list/' + selectActId.value)
if (code === 0) {
itemOptions.value = data;
}
} catch (e) {
console.error(e)
}
}
// 获取团队成员
async function listTeamUser(){
try {
const {code, data, msg} = await request.get('/client/item/list-team-user/'+selectActId.value)
if (code === 0) {
teamMembers.value = data;
}else{
Toast.error(msg)
}
}catch (e){
Toast.error(msg)
console.error(e)
}
}
// 获取报名状态 - 使用静态数据
const fetchRegistrationStatus = async () => {
};
// 选择项目
const selectProject = (projectId) => {
selectedItemId.value = projectId;
selectedMemberIds.value = [];
showMemberPicker.value = true;
};
// 提交报名 - 使用静态数据模拟
const submitRegistration = async () => {
if (!selectedMemberIds.value.length) {
Toast.warning('请选择团队成员');
return;
}
try {
console.log(selectedMemberIds.value)
console.log(selectedItemId.value)
console.log(selectActId.value)
const teamUser = teamMembers.value.filter(v=>selectedMemberIds.value.includes(v.id))
// const {code, msg} = await request.post('/client/item/sign-up', {
// activityId: selectActId.value,
// itemId: selectedItemId.value,
// userList: selectedMemberIds.value
// })
// if (code === 0) {
// Toast.success('报名成功')
// showMemberPicker.value = false;
// selectedMemberIds.value = []
// }else{
// Toast.error(msg)
// }
}catch (e){
Toast.error(e.message)
console.error(e)
}
};
</script>
<style scoped lang="less">
.top-dropdown {
flex-shrink: 0;
z-index: 10;
position: relative;
}
.content-container {
padding: 15px;
z-index: 9;
position: relative;
}
.page-header {
margin-bottom: 24px;
padding: 0 4px;
.page-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
display: block;
}
.page-subtitle {
font-size: 14px;
color: #999;
display: block;
}
}
.project-cards {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 24px;
}
.project-card {
background-color: #fff;
border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
&--registered {
background: #fff;
.project-card__title,
.project-card__desc {
color: #333;
}
}
&__content {
flex: 1;
min-width: 0;
}
&__title {
font-size: 16px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__desc {
font-size: 14px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__status {
margin-left: 12px;
}
&__action {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
&__checkmark {
position: absolute;
top: 16px;
right: 16px;
color: #52c41a;
}
}
.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;
}
}
// 添加动画效果
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.project-card {
animation: fadeIn 0.3s ease-out;
animation-fill-mode: both;
}
.project-card:nth-child(1) {
animation-delay: 0.05s;
}
.project-card:nth-child(2) {
animation-delay: 0.1s;
}
.project-card:nth-child(3) {
animation-delay: 0.15s;
}
.project-card:nth-child(4) {
animation-delay: 0.2s;
}
.project-card:nth-child(5) {
animation-delay: 0.25s;
}
</style>
+317 -221
View File
@@ -1,112 +1,116 @@
<template>
<view class="page-container">
<t-dropdown-menu @menu-opened="handleMenuOpened" @menu-closed="handleMenuClosed">
<t-dropdown-item :options="actOptions" :value="pageForm.actId" @change="actChange" />
</t-dropdown-menu>
<view class="page-container" :style="{ backgroundColor: '#f7f7f9' }">
<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-banner">
<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-info">
<div class="team-basic-info">
<div class="team-name">{{ team.name }}</div>
<div class="team-meta">创建于 {{ team.createdDate }}</div>
<div class="team-id">团队ID: {{ team.id }}</div>
</div>
<div class="event-name">{{ team.eventName }}</div>
</div>
<div class="team-detail">
<div class="team-stat-row">
<div class="team-stat-item">
<div class="team-stat-value">{{ team.memberCount }}</div>
<div class="team-stat-label">团队成员</div>
</div>
<div class="team-stat-item">
<div class="team-stat-value">{{ team.rank }}</div>
<div class="team-stat-label">当前排名</div>
</div>
<div class="team-stat-item">
<div class="team-stat-value">{{ team.score }}</div>
<div class="team-stat-label">团队积分</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.memberCount }}</div>
<div class="stat-label">成员</div>
</div>
</div>
<div style="font-size: 14px; color: #666; line-height: 1.6;">
{{ team.description }}
<div class="stat-divider"></div>
<div class="stat-item">
<div class="stat-icon"><i class="bi bi-trophy"></i></div>
<div class="stat-content">
<div class="stat-value">{{ team.rank }}</div>
<div class="stat-label">排名</div>
</div>
</div>
</div>
<div v-if="team.description" class="team-desc-toggle" @click="showDesc = !showDesc">
<span>团队介绍</span>
<i class="bi" :class="showDesc ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
</div>
<div v-if="showDesc && team.description" class="team-desc">
<i class="bi bi-quote"></i>
{{ team.description }}
</div>
</div>
<!-- 团队成员 -->
<div class="team-section">
<!-- 项目报名按钮 -->
<div class="action-section">
<t-button block theme="danger" size="large" class="register-btn" @click="goToEventRegistration">
参与项目报名
</t-button>
</div>
<!-- 团队成员列表 -->
<div class="members-section">
<div class="section-header">
<div class="section-title">
<i class="bi bi-people"></i>团队成员
<i class="bi bi-people-fill"></i>
团队成员
</div>
<div class="section-badge">{{ team.memberCount }}</div>
</div>
<div class="member-list">
<div class="member-item" v-for="member in team.members" :key="member.id">
<div class="member-avatar">
<img :src="member.avatar" :alt="member.name" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2YwZjBmMCIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LXNpemU9IjIwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBhbGlnbm1lbnQtYmFzZWxpbmU9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmaWxsPSIjNTU1Ij57eyBtZW1iZXIubmFtZS5jaGFyQXQoMCkgfX08L3RleHQ+PC9zdmc+'">
<div class="member-grid">
<div class="member-grid-item" v-for="member in team.members" :key="member.id">
<div class="member-name">
{{ member.name }}
<t-tag theme="danger" size="small" v-if="member.role === 'leader'">队长</t-tag>
</div>
<div class="member-info">
<div class="member-name">
{{ member.name }}
<span class="member-role" v-if="member.role === 'leader'">队长</span>
</div>
<div class="member-detail">
<div class="member-detail-item">
<i class="bi bi-gender-ambiguous"></i>
<span>{{ member.gender }}</span>
</div>
<div class="member-detail-item">
<i class="bi bi-building"></i>
<span>{{ member.organization }}</span>
</div>
</div>
</div>
<div class="member-actions">
<i class="bi bi-three-dots-vertical"></i>
<div class="member-details">
<span><i class="bi bi-gender-ambiguous"></i> {{ member.gender }}</span>
<span><i class="bi bi-building"></i> {{ member.organization }}</span>
</div>
</div>
</div>
<!-- 当前角色与申请队长 -->
<div v-if="currentUserRole !== 'leader'">
<div class="role-notice">
<i class="bi bi-info-circle"></i>
您当前是普通成员身份
<div class="role-section">
<div class="role-card" v-if="currentUserRole === 'leader'">
<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-else>
<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>
<t-button block
theme="primary"
variant="outline"
size="large"
shape="round"
@click="leaderConfirmVisible = true">
<i class="bi bi-award"></i>申请成为队长
</t-button>
</div>
<div class="role-notice" v-else>
<i class="bi bi-check-circle"></i>
您当前是队长身份,拥有团队所有权限
</div>
</div>
<!-- 项目报名按钮 -->
<div class="team-section">
<t-button block
theme="primary"
size="large"
shape="round" @click="goToEventRegistration">
<i class="bi bi-calendar-check"></i>
项目报名
</t-button>
</div>
</div>
</view>
@@ -120,18 +124,24 @@
@cancel="leaderConfirmVisible = false"
>
<template #content>
<p>申请成为队长前,请确认您已与团队其他成员商议并达成一致意见。</p>
<p>一个团队仅能有一名队长,作为队长,您可以帮助队员一键报名项目。</p>
<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>
</template>
<script setup>
import {ref,reactive} from "vue";
import {ref, reactive} from "vue";
const actOptions = ref([])
const showDesc = ref(false)
const pageForm = reactive({
actId: null,
@@ -216,45 +226,54 @@ function onLeaderConfirm(){
}
function goToEventRegistration(){}
</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;
}
/* 团队信息卡片 */
/* 团队卡片 */
.team-card {
background-color: white;
margin: 15px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
background-color: #fff;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(255, 95, 92, 0.08);
margin: 12px 0 20px;
}
.team-banner {
position: relative;
height: 120px;
background: linear-gradient(135deg, #e74c3c, #ff7675);
.team-header {
display: flex;
align-items: center;
padding: 20px;
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: 80px;
height: 80px;
border-radius: 50%;
width: 48px;
height: 48px;
border-radius: 12px;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
border: 2px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
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 {
@@ -263,93 +282,137 @@ function goToEventRegistration(){}
object-fit: cover;
}
.team-logo i {
font-size: 40px;
color: #e74c3c;
}
.team-info {
.team-basic-info {
margin-left: 12px;
flex: 1;
color: white;
}
.team-name {
font-size: 20px;
font-size: 18px;
font-weight: 600;
margin-bottom: 6px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
color: #333;
margin-bottom: 4px;
}
.team-meta {
font-size: 13px;
opacity: 0.95;
}
.team-id {
display: inline-block;
background-color: rgba(255, 255, 255, 0.2);
padding: 2px 8px;
border-radius: 10px;
margin-top: 6px;
font-size: 12px;
color: #999;
}
.event-name {
position: absolute;
bottom: 10px;
right: 15px;
background-color: rgba(0, 0, 0, 0.2);
padding: 3px 10px;
border-radius: 15px;
font-size: 12px;
color: white;
}
/* 团队详情内容 */
.team-detail {
padding: 15px;
}
.team-stat-row {
.team-stats {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 15px;
padding: 12px 16px;
}
.team-stat-item {
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 5px 0;
}
.team-stat-value {
font-size: 22px;
.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: #e74c3c;
margin-bottom: 5px;
color: #333;
line-height: 1.2;
}
.team-stat-label {
font-size: 13px;
.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-section {
.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;
margin-bottom: 15px;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
}
.section-title {
font-size: 17px;
font-size: 16px;
font-weight: 600;
color: #333;
display: flex;
@@ -358,105 +421,138 @@ function goToEventRegistration(){}
.section-title i {
margin-right: 8px;
color: #e74c3c;
font-size: 20px;
color: #ff5f5c;
font-size: 18px;
}
.member-list {
background: white;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.member-item {
.section-badge {
display: flex;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
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-item:last-child {
border-bottom: none;
.member-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 12px;
}
.member-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #f0f0f0;
margin-right: 15px;
overflow: hidden;
flex-shrink: 0;
}
.member-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.member-info {
flex: 1;
.member-grid-item {
background-color: #f9f9f9;
border-radius: 8px;
padding: 10px;
}
.member-name {
font-size: 16px;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 5px;
margin-bottom: 2px;
display: flex;
align-items: center;
justify-content: space-between;
}
.member-role {
display: inline-block;
.member-details {
display: flex;
flex-direction: column;
font-size: 12px;
background-color: #e74c3c;
color: white;
padding: 1px 6px;
border-radius: 4px;
margin-left: 8px;
color: #999;
}
.member-detail {
display: flex;
font-size: 13px;
color: #666;
}
.member-detail-item {
.member-details span {
display: flex;
align-items: center;
margin-right: 15px;
margin-top: 2px;
}
.member-detail-item i {
.member-details i {
margin-right: 4px;
font-size: 14px;
color: #999;
font-size: 12px;
color: #ccc;
}
.member-actions {
color: #999;
padding: 5px;
border-radius: 5px;
/* 角色相关 */
.role-section {
padding: 16px;
}
.role-notice {
background-color: #f8f9fa;
padding: 12px 15px;
border-radius: 8px;
margin-top: 15px;
font-size: 13px;
color: #666;
.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-notice i {
color: #ffa63f;
margin-right: 8px;
font-size: 16px;
.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>
+1 -3
View File
@@ -1,11 +1,9 @@
<template>
<view class="page-container">
<!-- 顶部背景和内容区 -->
<view class="header">
<image class="header-bg" src="https://erp-bucket.pek3b.qingstor.com/assistant/89c4b573-c54f-467a-b703-0bc44679ab08.png" mode="aspectFill"></image>
</view>
<!-- 功能菜单区 -->
<view class="menu-container">
<view class="menu-item" @click="navigateTo('/pages/activity/list/index')">
<view class="menu-icon-wrapper red-bg">
@@ -26,7 +24,7 @@
</view>
<view class="menu-text">我的团队</view>
</view>
<view class="menu-item" @click="navigateTo('/pages/activity/team/index')">
<view class="menu-item" @click="navigateTo('/pages/activity/team-form/index')">
<view class="menu-icon-wrapper blue-bg">
<view class="menu-icon star-icon"></view>
</view>
+330 -151
View File
@@ -1,51 +1,118 @@
<template>
<view class="login-container">
<image
class="banner-image"
src="https://res.firstui.cn/template/loginB/login/img_banner_3x.png"
mode="aspectFill"
/>
<view class="title-area">
<div class="tech-animation">
<svg preserveAspectRatio="xMidYMid slice" viewBox="0 0 300 100">
<pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="scale(0.8)">
<rect x="0" y="0" width="10" height="10" fill="none"/>
<path d="M0,5 L2.5,0 L7.5,0 L10,5 L7.5,10 L2.5,10 Z" stroke-width="0.5" stroke="rgba(244, 90, 90, 0.2)" fill="rgba(244, 90, 90, 0.1)">
<animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite" />
</path>
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)" />
<view class="login-form">
<text class="title">登录</text>
<g fill="none" stroke-width="0.5">
<path d="M20,50 Q60,20 100,50 T180,50 T260,50" stroke="rgba(244, 90, 90, 0.6)">
<animate attributeName="d"
values="M20,50 Q60,20 100,50 T180,50 T260,50;
M20,50 Q60,80 100,50 T180,50 T260,50;
M20,50 Q60,20 100,50 T180,50 T260,50"
dur="10s" repeatCount="indefinite" />
</path>
<t-input
class="input-item"
v-model="loginForm.phone"
placeholder="请输入手机号码"
>
</t-input>
<path d="M20,70 Q60,40 100,70 T180,70 T260,70" stroke="rgba(244, 90, 90, 0.4)">
<animate attributeName="d"
values="M20,70 Q60,40 100,70 T180,70 T260,70;
M20,70 Q60,100 100,70 T180,70 T260,70;
M20,70 Q60,40 100,70 T180,70 T260,70"
dur="12s" repeatCount="indefinite" />
</path>
</g>
<t-input
class="input-item"
v-model="loginForm.smsCode"
placeholder="请输入验证码"
>
<template #suffix>
<t-button variant="text" class="verify-btn" @click="getSmsCode" :disabled="isCountingDown">
<circle cx="50" cy="50" r="1" fill="rgba(244, 90, 90, 0.8)">
<animate attributeName="cx" values="50;250;50" dur="20s" repeatCount="indefinite" />
<animate attributeName="cy" values="50;30;80;50" dur="16s" repeatCount="indefinite" />
<animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite" />
</circle>
<circle cx="150" cy="30" r="1" fill="rgba(244, 90, 90, 0.8)">
<animate attributeName="cx" values="150;100;200;150" dur="25s" repeatCount="indefinite" />
<animate attributeName="cy" values="30;60;20;30" dur="18s" repeatCount="indefinite" />
<animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite" />
</circle>
<circle cx="250" cy="70" r="1" fill="rgba(244, 90, 90, 0.8)">
<animate attributeName="cx" values="250;150;250" dur="22s" repeatCount="indefinite" />
<animate attributeName="cy" values="70;40;70" dur="15s" repeatCount="indefinite" />
<animate attributeName="r" values="1;1.2;1" dur="4s" repeatCount="indefinite" />
</circle>
</svg>
</div>
<view class="title-content">
<text class="main-title">HELLO,</text>
<view class="sub-title-container">
<text class="sub-title">欢迎使用</text>
</view>
</view>
</view>
<view class="login-card">
<view class="login-tabs">
<view
:class="['tab', activeMethod === 'phone' ? 'active' : '']"
@click="setActiveMethod('phone')"
>
手机快速登录
</view>
<view
:class="['tab', activeMethod === 'password' ? 'active' : '']"
@click="setActiveMethod('password')"
>
账号密码登录
</view>
</view>
<!-- 登录表单区域 -->
<view class="form-container">
<!-- 手机号码输入框 -->
<view class="input-box">
<t-icon name="mobile" size="20px" class="input-icon" />
<input
class="input"
type="tel"
v-model="loginForm.phone"
placeholder="请输入手机号"
maxlength="11"
/>
</view>
<!-- 验证码输入框 -->
<view class="input-box">
<t-icon name="shield-error" size="20px" class="input-icon" />
<input
class="input"
type="number"
v-model="loginForm.smsCode"
placeholder="请输入验证码"
maxlength="6"
/>
<view class="verify-code-btn" @click="getSmsCode" :class="{ disabled: isCountingDown }">
{{ countDownText }}
</t-button>
</template>
</t-input>
</view>
</view>
<!-- <view class="login-options">-->
<!-- <text>账号登录</text>-->
<!-- <text>注册</text>-->
<!-- </view>-->
<t-button block theme="primary" size="large" class="login-btn" @click="handleLogin">
登录
</t-button>
<!-- <view class="other-login">-->
<!-- <text class="other-text">其他登录方式</text>-->
<!-- <view class="login-methods">-->
<!-- <t-button shape="circle" variant="outline" icon="qq" class="method-btn" />-->
<!-- <t-button shape="circle" variant="outline" icon="wechat" class="method-btn" />-->
<!-- <t-button shape="circle" variant="outline" icon="image" class="method-btn" />-->
<!-- </view>-->
<!-- </view>-->
<!-- 登录按钮 -->
<view class="submit-btn" @click="handleLogin">
立即登录
</view>
<!-- &lt;!&ndash; 注册链接 &ndash;&gt;-->
<!-- <view class="register-link">-->
<!-- <text>没有账号?</text>-->
<!-- <text class="link">去注册</text>-->
<!-- </view>-->
</view>
</view>
</view>
</template>
@@ -56,12 +123,17 @@ import {Toast} from 'tdesign-mobile-vue';
import {useRequest} from "@/utils/request";
const request = useRequest()
const activeMethod = ref('phone')
const loginForm = reactive({
phone: '18571562330',
phone: '',
smsCode: ''
})
const setActiveMethod = (method) => {
activeMethod.value = method
}
// 倒计时相关
const countdown = ref(0)
const timer = ref(null)
@@ -84,7 +156,7 @@ const saveCountdownState = (seconds, phone) => {
uni.removeStorageSync(SMS_PHONE_KEY)
return
}
// 保存结束时间戳和手机号
const endTime = Date.now() + seconds * 1000
uni.setStorageSync(SMS_COUNTDOWN_KEY, endTime)
@@ -96,13 +168,18 @@ const restoreCountdownState = () => {
const endTime = uni.getStorageSync(SMS_COUNTDOWN_KEY)
const savedPhone = uni.getStorageSync(SMS_PHONE_KEY)
if (endTime && savedPhone && savedPhone === loginForm.phone) {
if (endTime && savedPhone) {
const now = Date.now()
const remainingSeconds = Math.floor((endTime - now) / 1000)
if (remainingSeconds > 0) {
countdown.value = remainingSeconds
startCountDownTimer() // 重新启动定时器
// 如果用户还没输入手机号,填入保存的手机号
if (!loginForm.phone) {
loginForm.phone = savedPhone
}
} else {
// 倒计时已结束,清除存储
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
@@ -116,13 +193,13 @@ const startCountDownTimer = () => {
if (timer.value) {
clearInterval(timer.value)
}
timer.value = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer.value)
timer.value = null
// 清除存储
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
uni.removeStorageSync(SMS_PHONE_KEY)
@@ -137,50 +214,69 @@ const startCountDown = () => {
startCountDownTimer()
}
// 在页面挂载时恢复倒计时状态
onMounted(() => {
restoreCountdownState()
})
// 在页面卸载前清除定时器
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
// 获取验证码
async function getSmsCode() {
if (!loginForm.phone) {
Toast.error('请输入手机号码');
Toast('请输入手机号码');
return
}
// 验证手机号码
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
Toast('请输入正确的手机号码');
return
}
if (isCountingDown.value) return
try {
const {code, data, msg} = await request.post('/client/auth/send-sms-code/' + loginForm.phone)
if (code === 0) {
Toast.success('验证码发送成功')
Toast({message: '验证码发送成功', theme: 'success'})
// 开始倒计时
startCountDown()
} else {
Toast.error(msg)
Toast({message: msg, theme: 'error'})
}
} catch (e) {
Toast.error(e.message)
Toast({message: e.message, theme: 'error'})
}
}
async function handleLogin() {
if (!loginForm.phone) {
Toast.error('请输入手机号码');
Toast('请输入手机号码');
return
}
if (!loginForm.smsCode) {
Toast.error('请输入验证码');
Toast('请输入验证码');
return
}
try {
const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
if (code === 0) {
Toast.success('登录成功')
Toast({message: '登录成功', theme: 'success'})
// 保存token
uni.setStorageSync('token', data)
// 获取之前保存的重定向URL
const redirectUrl = uni.getStorageSync('REDIRECT_URL') || '/pages/index/index'
uni.removeStorageSync('REDIRECT_URL') // 使用后删除
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
if (redirectUrl.startsWith('/pages/index/') || redirectUrl.startsWith('/pages/profile/')) {
@@ -196,119 +292,202 @@ async function handleLogin() {
}
}, 1000)
} else {
Toast.error(msg)
Toast({message: msg, theme: 'error'})
}
} catch (e) {
console.error(e)
Toast.error(e.message)
Toast({message: e.message, theme: 'error'})
}
}
// 页面加载时恢复倒计时状态
onMounted(() => {
restoreCountdownState()
})
// 页面卸载前清理定时器
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
</script>
<style lang="less" scoped>
.login-container {
min-height: 100vh;
background-color: #fff;
position: relative;
}
.banner-image {
body {
height: 100%;
overflow: hidden;
position: fixed;
width: 100%;
height: 260px;
background-color: #4080ff;
top: 0;
left: 0;
}
.login-form {
padding: 20px;
.login-container {
height: 100vh;
background: #f4f2f2;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.title {
font-size: 24px;
font-weight: bold;
margin: 20px 0;
display: block;
color: var(--td-brand-color-5);
.title-area {
padding: 0;
height: 30vh;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
background: linear-gradient(135deg, #7a1818 0%, #d03e3e 50%, #f45a5a 100%);
}
.tech-animation {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.tech-animation svg {
width: 100%;
height: 100%;
}
.title-content {
padding: 0 30px;
position: relative;
z-index: 1;
}
.main-title {
font-size: 36px;
font-weight: bold;
margin-bottom: 8px;
display: block;
position: relative;
z-index: 1;
color: #ffffff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.sub-title-container {
display: flex;
align-items: center;
position: relative;
z-index: 1;
}
.sub-title {
font-size: 18px;
color: #ffffff;
position: relative;
z-index: 1;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.login-card {
flex: 1;
background-color: #ffffff;
border-radius: 30px 30px 0 0;
margin-top: -30px;
padding: 30px 0 0;
position: relative;
z-index: 2;
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.05);
}
.login-tabs {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
position: relative;
gap: 10px;
padding: 0 30px;
}
.tab {
width: calc(50% - 5px);
height: 55px;
display: flex;
justify-content: center;
align-items: center;
font-size: 15px;
color: #f45a5a;
font-weight: 500;
background-color: #fff0f0;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.tab:first-child {
border-radius: 25px 15px 30px 25px;
clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%);
}
.tab:last-child {
border-radius: 30px 25px 25px 15px;
clip-path: polygon(10% 0, 100% 0, 100% 100%, 0 100%);
}
.tab.active {
color: #fff;
background-color: #f45a5a;
}
.form-container {
padding: 0 30px;
}
.input-box {
height: 50px;
background-color: #eceaea;
border-radius: 25px;
display: flex;
align-items: center;
padding: 0 15px;
margin-bottom: 16px;
}
.input-icon {
color: #999;
margin-right: 10px;
}
.input {
flex: 1;
height: 100%;
border: none;
font-size: 15px;
background-color: transparent;
}
.input::placeholder {
color: #bcbcbc;
}
.verify-code-btn {
color: #f45a5a;
font-size: 14px;
white-space: nowrap;
&.disabled {
color: #999;
}
}
.input-item {
margin-bottom: 20px;
}
.submit-btn {
margin-top: 32px;
height: 50px;
background-color: #f45a5a;
border-radius: 25px;
color: white;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.country-code {
color: #333;
font-weight: bold;
}
.register-link {
margin-top: 16px;
text-align: center;
font-size: 14px;
color: #666;
.verify-btn {
color: var(--td-brand-color-5);
}
.login-options {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
.login-btn {
margin: 20px 0;
}
.other-login {
margin-top: 40px;
text-align: center;
.other-text {
color: #999;
font-size: 14px;
position: relative;
display: inline-block;
padding: 0 10px;
background: #fff;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
width: 60px;
height: 1px;
background: #eee;
}
&::before {
left: -70px;
}
&::after {
right: -70px;
}
}
.login-methods {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 20px;
.method-btn {
width: 40px;
height: 40px;
}
}
.link {
color: #f45a5a;
margin-left: 4px;
}
}
</style>