Compare commits

...
6 Commits
Author SHA1 Message Date
DevOps 56a1897b7d fix: 修复选手列表重复问题 - 只查询registrationStatus=0的选手信息记录
根本原因:报名时会创建新的registrationStatus=1参赛记录,而查询选手列表时没有过滤,导致显示重复
解决方案:在查询选手列表时添加registrationStatus=0参数,只查询选手信息库
2026-01-15 17:20:02 +08:00
DevOps 5f68218079 fix: 修复选手/集体列表重复问题,添加集体项目已报名标记
- add-team.vue: 添加选手列表去重逻辑
- event-register.vue: loadTeamList添加去重和已报名标记
- event-register.vue: toggleTeam添加已报名警告
- 集体项目UI显示已报名标签
2026-01-15 16:52:08 +08:00
DevOps 82deb8b4f9 fix: 选手列表去重并显示已报名标记
- loadPlayerList 添加按身份证去重逻辑
- 查询已报名选手并标记 hasRegistered
- UI 显示"已报名"标签
- 选择已报名选手时弹出警告

Closes #3, Closes #4
2026-01-15 15:57:08 +08:00
DevOpsandfactory-droid[bot] 076aa287bd fix: 报名时强制验证联系人是否已选择
- 在goToStep3方法中添加selectedContact验证
- 未选择联系人时显示提示并阻止提交
- 修复Issue #2

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-15 15:57:08 +08:00
DevOpsandfactory-droid[bot] d8c255d69d 出场顺序:显示预计出场时间和状态样式
- 显示预计出场时间(单位 | 时间)
- 状态样式:已完成(绿色)、进行中(橙色)、待出场(灰色)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-15 15:57:08 +08:00
Z-WICK 7c4507c3ed sync changes: 出场顺序优化、联系人功能 2026-01-15 15:57:08 +08:00
9 changed files with 243 additions and 37 deletions
+7
View File
@@ -81,5 +81,12 @@ export default {
removeContact(id) { removeContact(id) {
return request.post('/martial/contact/remove?ids=' + id, {}) return request.post('/martial/contact/remove?ids=' + id, {})
},
/**
* 获取已报名选手列表(用于检查重复报名)
*/
getRegisteredAthletes(params = {}) {
return request.get('/martial/athlete/registered', params)
} }
} }
+4 -4
View File
@@ -10,7 +10,7 @@ import request from '@/utils/request.js'
*/ */
export function getScheduleResult(competitionId) { export function getScheduleResult(competitionId) {
return request.get('/martial/schedule/result', { return request.get('/martial/schedule/result', {
params: { competitionId } competitionId
}) })
} }
@@ -97,7 +97,7 @@ export function saveDispatch(data) {
*/ */
export function getScheduleStatus(competitionId) { export function getScheduleStatus(competitionId) {
return request.get('/mini/schedule/status', { return request.get('/mini/schedule/status', {
params: { competitionId } competitionId
}) })
} }
@@ -106,9 +106,9 @@ export function getScheduleStatus(competitionId) {
* @param {Number} competitionId - 赛事ID * @param {Number} competitionId - 赛事ID
* @param {Number} projectId - 项目ID (可选) * @param {Number} projectId - 项目ID (可选)
*/ */
export function getLineup(competitionId, projectId) { export function getLineup(competitionId, projectId, orderId) {
return request.get('/mini/schedule/lineup', { return request.get('/mini/schedule/lineup', {
params: { competitionId, projectId } competitionId, projectId, orderId
}) })
} }
+30
View File
@@ -124,6 +124,36 @@ export default {
try { try {
await athleteAPI.saveContact(this.formData); await athleteAPI.saveContact(this.formData);
uni.showToast({ title: '保存成功', icon: 'success' }); uni.showToast({ title: '保存成功', icon: 'success' });
// Set flag to notify previous page
uni.setStorageSync('contactListNeedRefresh', true);
// Notify previous page to refresh contact list
try {
const pages = getCurrentPages();
console.log('页面栈长度:', pages.length);
console.log('页面栈:', pages);
if (pages.length >= 2) {
const prevPage = pages[pages.length - 2];
console.log('上一页:', prevPage);
// Try different ways to access the component
const vm = prevPage.$vm || prevPage;
console.log('vm:', vm);
if (vm && typeof vm.loadContactList === 'function') {
console.log('调用 loadContactList');
vm.loadContactList();
} else if (vm && vm.$refs && vm.$refs.page && typeof vm.$refs.page.loadContactList === 'function') {
console.log('通过 $refs 调用');
vm.$refs.page.loadContactList();
}
}
} catch (e) {
console.error('通知上一页失败:', e);
}
// Also emit event as backup
uni.$emit('contactAdded');
setTimeout(() => { uni.navigateBack(); }, 1500); setTimeout(() => { uni.navigateBack(); }, 1500);
} catch (err) { } catch (err) {
console.error('保存联系人失败:', err); console.error('保存联系人失败:', err);
+11 -1
View File
@@ -109,7 +109,8 @@ export default {
const res = await athleteAPI.getAthleteList({ const res = await athleteAPI.getAthleteList({
current: 1, current: 1,
size: 100, size: 100,
createUser: userInfo.userId createUser: userInfo.userId,
registrationStatus: 0 // Only query player info records
}) })
let list = [] let list = []
@@ -119,6 +120,15 @@ export default {
list = res list = res
} }
// Deduplicate by id
const seen = new Set()
list = list.filter(item => {
const key = item.id
if (seen.has(key)) return false
seen.add(key)
return true
})
this.playerList = list.map(item => ({ this.playerList = list.map(item => ({
id: item.id, id: item.id,
name: item.name || item.playerName name: item.name || item.playerName
+9 -1
View File
@@ -168,7 +168,15 @@ export default {
}) })
let list = res.records || (Array.isArray(res) ? res : []) let list = res.records || (Array.isArray(res) ? res : [])
this.playerList = list.map(item => ({ // Deduplicate by idCard
const seen = new Set()
const uniqueList = list.filter(item => {
const idCard = item.idCard || item.idCardNumber
if (idCard && seen.has(idCard)) return false
if (idCard) seen.add(idCard)
return true
})
this.playerList = uniqueList.map(item => ({
id: item.id, id: item.id,
name: item.name || item.playerName, name: item.name || item.playerName,
idCard: item.idCard || item.idCardNumber, idCard: item.idCard || item.idCardNumber,
+3 -1
View File
@@ -82,6 +82,7 @@ export default {
data() { data() {
return { return {
eventId: '', eventId: '',
orderId: '',
eventInfo: { eventInfo: {
id: '', id: '',
title: '', title: '',
@@ -96,6 +97,7 @@ export default {
onLoad(options) { onLoad(options) {
if (options.id) { if (options.id) {
this.eventId = options.id this.eventId = options.id
this.orderId = options.orderId || null
this.loadEventDetail(options.id) this.loadEventDetail(options.id)
} }
}, },
@@ -192,7 +194,7 @@ export default {
if (url) { if (url) {
// 跳转时传递赛事ID // 跳转时传递赛事ID
uni.navigateTo({ uni.navigateTo({
url: `${url}?eventId=${this.eventId}` url: `${url}?eventId=${this.eventId}&orderId=${this.orderId || ""}`
}) })
} else { } else {
uni.showToast({ uni.showToast({
+19 -8
View File
@@ -41,6 +41,8 @@
<view class="lineup-name">{{ item.playerName }}</view> <view class="lineup-name">{{ item.playerName }}</view>
<view class="lineup-detail"> <view class="lineup-detail">
<text class="detail-item">{{ item.organization }}</text> <text class="detail-item">{{ item.organization }}</text>
<text class="detail-divider" v-if="item.estimatedTime"> | </text>
<text class="detail-item" v-if="item.estimatedTime">{{ item.estimatedTime }}</text>
</view> </view>
</view> </view>
<view class="lineup-status" :class="item.status"> <view class="lineup-status" :class="item.status">
@@ -63,6 +65,7 @@ export default {
data() { data() {
return { return {
eventId: '', eventId: '',
orderId: '',
loading: true, loading: true,
isScheduleCompleted: false, isScheduleCompleted: false,
currentGroupIndex: 0, currentGroupIndex: 0,
@@ -79,6 +82,7 @@ export default {
onLoad(options) { onLoad(options) {
if (options.eventId) { if (options.eventId) {
this.eventId = options.eventId this.eventId = options.eventId
this.orderId = options.orderId || null
this.checkScheduleStatus() this.checkScheduleStatus()
} }
}, },
@@ -87,8 +91,8 @@ export default {
async checkScheduleStatus() { async checkScheduleStatus() {
try { try {
const res = await getScheduleStatus(this.eventId) const res = await getScheduleStatus(this.eventId)
if (res.data) { if (res) {
this.isScheduleCompleted = res.data.isCompleted this.isScheduleCompleted = res.isCompleted
if (this.isScheduleCompleted) { if (this.isScheduleCompleted) {
await this.loadLineup() await this.loadLineup()
} }
@@ -103,9 +107,9 @@ export default {
async loadLineup() { async loadLineup() {
try { try {
const res = await getLineup(this.eventId) const res = await getLineup(this.eventId, null, this.orderId)
if (res.data && res.data.groups) { if (res && res.groups) {
this.groups = res.data.groups this.groups = res.groups
} }
} catch (error) { } catch (error) {
console.error('加载出场顺序失败:', error) console.error('加载出场顺序失败:', error)
@@ -115,6 +119,10 @@ export default {
getStatusText(status) { getStatusText(status) {
const map = { const map = {
'waiting': '待出场', 'waiting': '待出场',
'未签到': '待出场',
'已完成': '已完成',
'进行中': '进行中',
'异常': '异常',
'ongoing': '进行中', 'ongoing': '进行中',
'finished': '已完成' 'finished': '已完成'
} }
@@ -262,17 +270,20 @@ export default {
flex-shrink: 0; flex-shrink: 0;
} }
.lineup-status.finished { .lineup-status.finished,
.lineup-status.已完成 {
background-color: #E8F5E9; background-color: #E8F5E9;
color: #4CAF50; color: #4CAF50;
} }
.lineup-status.ongoing { .lineup-status.ongoing,
.lineup-status.进行中 {
background-color: #FFF3E0; background-color: #FFF3E0;
color: #FF9800; color: #FF9800;
} }
.lineup-status.waiting { .lineup-status.waiting,
.lineup-status.未签到 {
background-color: #F5F5F5; background-color: #F5F5F5;
color: #999999; color: #999999;
} }
+154 -16
View File
@@ -30,13 +30,16 @@
</view> </view>
<view class="player-list"> <view class="player-list">
<view class="player-item" v-for="(item, index) in playerList" :key="index"> <view class="player-item" v-for="(item, index) in playerList" :key="index" :class="{ 'registered': item.hasRegistered }">
<view class="player-checkbox" @click="togglePlayer(item)"> <view class="player-checkbox" @click="togglePlayer(item)">
<image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image> <image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image>
<image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image> <image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image>
</view> </view>
<view class="player-info"> <view class="player-info">
<view class="player-name">{{ item.name }}</view> <view class="player-name">
{{ item.name }}
<text v-if="item.hasRegistered" class="registered-tag">已报名</text>
</view>
<view class="player-id">身份证{{ item.idCard }}</view> <view class="player-id">身份证{{ item.idCard }}</view>
</view> </view>
<view class="player-actions"> <view class="player-actions">
@@ -63,13 +66,16 @@
</view> </view>
<view class="player-list"> <view class="player-list">
<view class="player-item" v-for="(item, index) in teamList" :key="index"> <view class="player-item" v-for="(item, index) in teamList" :key="index" :class="{ 'registered': item.hasRegistered }">
<view class="player-checkbox" @click="toggleTeam(item)"> <view class="player-checkbox" @click="toggleTeam(item)">
<image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image> <image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image>
<image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image> <image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image>
</view> </view>
<view class="player-info"> <view class="player-info">
<view class="player-name">{{ item.name }}</view> <view class="player-name">
{{ item.name }}
<text v-if="item.hasRegistered" class="registered-tag">已报名</text>
</view>
<view class="player-id">成员数{{ item.memberCount || 0 }}</view> <view class="player-id">成员数{{ item.memberCount || 0 }}</view>
</view> </view>
<view class="player-actions"> <view class="player-actions">
@@ -282,6 +288,11 @@ export default {
} }
}, },
onLoad(options) { onLoad(options) {
// Listen for contact added event
uni.$on('contactAdded', () => {
console.log('=== 收到 contactAdded 事件 ===')
this.loadContactList()
})
if (options.eventId) { if (options.eventId) {
this.eventId = options.eventId this.eventId = options.eventId
this.loadEventDetail(options.eventId) this.loadEventDetail(options.eventId)
@@ -309,9 +320,21 @@ export default {
this.loadPlayerList() this.loadPlayerList()
} }
}, },
onUnload() {
uni.$off('contactAdded')
},
onShow() { onShow() {
// Refresh contact list when returning from add-contact page console.log('=== onShow 被触发 ===')
// Check if contact list needs refresh
const needRefresh = uni.getStorageSync('contactListNeedRefresh')
if (needRefresh) {
console.log('检测到需要刷新联系人列表')
uni.removeStorageSync('contactListNeedRefresh')
this.loadContactList() this.loadContactList()
} else {
// Always refresh contact list when page shows
this.loadContactList()
}
if (this.currentStep === 1) { if (this.currentStep === 1) {
if (this.isTeamProject) { if (this.isTeamProject) {
this.loadTeamList() this.loadTeamList()
@@ -324,13 +347,19 @@ export default {
// Contact methods // Contact methods
async loadContactList() { async loadContactList() {
try { try {
console.log('=== 开始加载联系人列表 ===')
const res = await athleteAPI.getContactList({ current: 1, size: 100 }) const res = await athleteAPI.getContactList({ current: 1, size: 100 })
this.contactList = (res.records || []).map(item => ({ console.log('联系人API返回:', res)
const records = res.records || res.data?.records || []
console.log('联系人记录:', records)
this.contactList = records.map(item => ({
id: item.id, id: item.id,
name: item.name, name: item.name,
phone: item.phone, phone: item.phone,
idCard: item.idCard idCard: item.idCard,
isDefault: item.isDefault
})) }))
console.log('处理后的联系人列表:', this.contactList)
// Auto select default contact if exists // Auto select default contact if exists
if (!this.selectedContact && this.contactList.length > 0) { if (!this.selectedContact && this.contactList.length > 0) {
const defaultContact = this.contactList.find(c => c.isDefault) || this.contactList[0] const defaultContact = this.contactList.find(c => c.isDefault) || this.contactList[0]
@@ -399,16 +428,48 @@ export default {
const res = await athleteAPI.getAthleteList({ const res = await athleteAPI.getAthleteList({
current: 1, current: 1,
size: 100, size: 100,
createUser: userInfo.userId createUser: userInfo.userId,
registrationStatus: 0 // Only query player info records, not registration records
}) })
let list = res.records || (Array.isArray(res) ? res : []) let list = res.records || (Array.isArray(res) ? res : [])
this.playerList = list.map(item => ({
// Deduplicate by idCard
const seen = new Set()
list = list.filter(item => {
const key = item.idCard || item.idCardNumber || item.id
if (seen.has(key)) return false
seen.add(key)
return true
})
// Get current project IDs
const projectIds = this.selectedProjects.map(p => p.id)
// Check which players have already registered for these projects
let registeredIdCards = []
if (projectIds.length > 0 && this.eventId) {
try {
const regRes = await athleteAPI.getRegisteredAthletes({
competitionId: this.eventId,
projectIds: projectIds.join(',')
})
registeredIdCards = (regRes || []).map(r => r.idCard)
} catch (e) {
console.log('查询已报名选手失败:', e)
}
}
this.playerList = list.map(item => {
const idCard = item.idCard || item.idCardNumber || ''
return {
id: item.id, id: item.id,
name: item.name || item.athleteName || item.playerName || '未命名', name: item.name || item.athleteName || item.playerName || '未命名',
idCard: item.idCard || item.idCardNumber || '', idCard: idCard,
selected: false selected: false,
})) hasRegistered: registeredIdCards.includes(idCard)
}
})
} catch (err) { } catch (err) {
console.error('加载选手列表失败:', err) console.error('加载选手列表失败:', err)
} }
@@ -425,12 +486,43 @@ export default {
}) })
let list = res.records || (Array.isArray(res) ? res : []) let list = res.records || (Array.isArray(res) ? res : [])
this.teamList = list.map(item => ({
// Deduplicate by id
const seen = new Set()
list = list.filter(item => {
const key = item.id
if (seen.has(key)) return false
seen.add(key)
return true
})
// Get current project IDs
const projectIds = this.selectedProjects.map(p => p.id)
// Check which teams have already registered for these projects
let registeredTeamNames = []
if (projectIds.length > 0 && this.eventId) {
try {
const regRes = await athleteAPI.getRegisteredAthletes({
competitionId: this.eventId,
projectIds: projectIds.join(',')
})
registeredTeamNames = (regRes || []).filter(r => r.teamName).map(r => r.teamName)
} catch (e) {
console.log('查询已报名集体失败:', e)
}
}
this.teamList = list.map(item => {
const teamName = item.teamName || item.name
return {
id: item.id, id: item.id,
name: item.teamName || item.name, name: teamName,
memberCount: item.memberCount || 0, memberCount: item.memberCount || 0,
selected: false selected: false,
})) hasRegistered: registeredTeamNames.includes(teamName)
}
})
} catch (err) { } catch (err) {
console.error('加载集体列表失败:', err) console.error('加载集体列表失败:', err)
this.teamList = [] this.teamList = []
@@ -455,6 +547,17 @@ export default {
}, },
togglePlayer(item) { togglePlayer(item) {
// Warn if player has already registered for this project
if (item.hasRegistered && !item.selected) {
uni.showModal({
title: '提示',
content: '该选手已报名此项目,重复报名将被系统拒绝',
confirmText: '知道了',
showCancel: false
})
return
}
const index = this.playerList.findIndex(p => p.id === item.id) const index = this.playerList.findIndex(p => p.id === item.id)
if (index !== -1) { if (index !== -1) {
this.$set(this.playerList[index], 'selected', !this.playerList[index].selected) this.$set(this.playerList[index], 'selected', !this.playerList[index].selected)
@@ -462,6 +565,17 @@ export default {
}, },
toggleTeam(item) { toggleTeam(item) {
// Warn if team has already registered for this project
if (item.hasRegistered && !item.selected) {
uni.showModal({
title: '提示',
content: '该集体已报名此项目,重复报名将被系统拒绝',
confirmText: '知道了',
showCancel: false
})
return
}
const index = this.teamList.findIndex(t => t.id === item.id) const index = this.teamList.findIndex(t => t.id === item.id)
if (index !== -1) { if (index !== -1) {
this.$set(this.teamList[index], 'selected', !this.teamList[index].selected) this.$set(this.teamList[index], 'selected', !this.teamList[index].selected)
@@ -566,6 +680,12 @@ export default {
return return
} }
// 强制验证联系人是否已选择
if (!this.selectedContact) {
uni.showToast({ title: '请选择联系人', icon: 'none' })
return
}
const now = new Date() const now = new Date()
const orderNo = `BM${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}` const orderNo = `BM${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}${String(Math.floor(Math.random() * 10000)).padStart(4, '0')}`
@@ -801,6 +921,24 @@ export default {
font-weight: bold; font-weight: bold;
color: #333333; color: #333333;
margin-bottom: 10rpx; margin-bottom: 10rpx;
display: flex;
align-items: center;
gap: 10rpx;
}
.registered-tag {
font-size: 22rpx;
font-weight: normal;
color: #ff6600;
background: #fff3e6;
padding: 4rpx 12rpx;
border-radius: 6rpx;
border: 1px solid #ff6600;
}
.player-item.registered {
opacity: 0.7;
background: #f9f9f9;
} }
.player-id { .player-id {
@@ -341,7 +341,7 @@ export default {
}, },
goToEventDetail(item) { goToEventDetail(item) {
uni.navigateTo({ uni.navigateTo({
url: '/pages/event-detail/event-detail?id=' + item.competitionId url: '/pages/event-detail/event-detail?id=' + item.competitionId + '&orderId=' + item.id
}); });
}, },
handleViewCert(item) { handleViewCert(item) {