hookehuyr

feat(JoinOrganization): 新增助力码输入页面及功能

添加助力码输入页面,包含4位码输入框和幼儿园信息展示功能
实现输入验证、自动聚焦和幼儿园匹配逻辑
添加确认加入功能和状态提示
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-17 19:58:26
* @LastEditTime: 2025-09-18 15:23:48
* @FilePath: /lls_program/src/app.config.js
* @Description: 文件描述
*/
......@@ -35,6 +35,7 @@ export default {
'pages/FamilyRank/index',
'pages/PosterCheckin/index',
'pages/CheckinList/index',
'pages/JoinOrganization/index',
],
window: {
backgroundTextStyle: 'light',
......
/*
* @Date: 2025-08-27 18:25:24
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-18 15:23:26
* @FilePath: /lls_program/src/pages/JoinOrganization/index.config.js
* @Description: 文件描述
*/
export default {
navigationBarTitleText: '助力码'
}
/* JoinFamily/index.less */
.motto-input-container {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
.motto-input-box {
width: 5rem;
height: 5rem;
text-align: center;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s;
}
.motto-input {
width: 100%;
height: 100%;
text-align: center;
font-size: 1.5rem;
background-color: transparent;
border: none;
outline: none;
padding: 0;
margin: 0;
box-sizing: border-box;
color: inherit;
}
.identity-title {
font-size: 32rpx;
font-weight: 600;
color: #374151;
margin-bottom: 24rpx;
}
// 家庭选择弹窗样式
.family-selector-container {
display: flex;
flex-direction: column;
height: 100%;
background: white;
}
.family-item {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
// 文本截断样式
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
// 间距样式
.space-y-3 > * + * {
margin-top: 24rpx;
}
.space-x-3 > * + * {
margin-left: 24rpx;
}
<template>
<view class="min-h-screen flex flex-col bg-white">
<view class="flex-1 px-4 pt-3 pb-6 flex flex-col">
<!-- Title -->
<h2 class="text-xl font-bold text-center mb-2">
输入助力码
</h2>
<!-- Description -->
<view class="text-gray-600 text-center text-sm mb-6">
请输入家人提供的助力码,参与助力榜排行
</view>
<!-- Input boxes -->
<view class="motto-input-container">
<view
v-for="(char, index) in mottoChars"
:key="index"
class="motto-input-box"
:style="{
borderColor: focusedIndex === index ? THEME_COLORS.PRIMARY : '#d1d5db'
}"
>
<input
:ref="(el) => (inputRefs[index] = el)"
type="text"
v-model="mottoChars[index]"
@input="(e) => handleInputChange(index, e.target.value)"
@keydown="(e) => handleKeyDown(index, e)"
@focus="focusedIndex = index"
@blur="handleBlur(index)"
class="motto-input"
:cursorSpacing="100"
/>
</view>
</view>
<!-- Help text -->
<view class="text-gray-500 text-center text-sm mb-4">
没有口令?请联系家里小辈,咨询单位学校是否参与了助力榜排行哦
</view>
<!-- 幼儿园信息显示 -->
<view v-if="matchedKindergarten" class="mb-6">
<view class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
<view class="flex items-center space-x-3">
<!-- 幼儿园Logo -->
<view class="w-12 h-12 rounded-full bg-white flex items-center justify-center overflow-hidden border-gray-200 border">
<image
:src="matchedKindergarten.logo || defaultKindergartenLogo"
class="w-full h-full object-cover"
/>
</view>
<!-- 幼儿园信息 -->
<view class="flex-1">
<view class="font-medium text-gray-900 mb-1">{{ matchedKindergarten.name }}</view>
<view class="text-sm text-gray-600">{{ matchedKindergarten.address }}</view>
</view>
</view>
</view>
<!-- 确认提示 -->
<view v-if="!isAlreadyJoined" class="text-center mb-4">
<view class="text-gray-700 text-sm mb-2">这是您家小辈所在的单位/学校吗?</view>
<view class="text-gray-600 text-xs">
<view>确认后您的家庭步数将计入其中,参与助力榜的排行哦</view>
<view>(不会影响家庭参与市、区榜)</view>
</view>
</view>
<!-- 已参与提示 -->
<view v-if="isAlreadyJoined" class="text-center mb-4">
<view class="text-green-600 text-sm font-medium">您的家庭已经参与助力榜</view>
</view>
</view>
<!-- Submit Button -->
<view
@tap="handleConfirmJoin"
:disabled="!isComplete"
:class="[
'w-full py-3 text-white text-lg font-medium rounded-lg mt-auto text-center',
isComplete ? 'bg-blue-500' : 'bg-gray-300'
]"
>
确认加入
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, nextTick, onMounted, watch } from 'vue';
import Taro from '@tarojs/taro';
import { My, Check, IconFont } from '@nutui/icons-vue-taro';
// 获取接口信息
import { searchFamilyByPassphraseAPI, joinFamilyAPI } from '@/api/family';
// 导入主题颜色
import { THEME_COLORS } from '@/utils/config';
// 默认幼儿园Logo
const defaultKindergartenLogo = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
const mottoChars = ref(['', '', '', '']);
const inputRefs = ref([]);
const focusedIndex = ref(-1);
// 幼儿园相关数据
const matchedKindergarten = ref(null);
const isAlreadyJoined = ref(false);
// Mock 幼儿园数据
const mockKindergartenData = {
'1234': {
id: 1,
name: '阳光幼儿园',
address: '北京市朝阳区阳光街123号',
logo: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
},
'5678': {
id: 2,
name: '彩虹幼儿园',
address: '北京市海淀区彩虹路456号',
logo: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
}
};
// Mock 已参与助力榜的数据(模拟用户已经参与的幼儿园)
const mockJoinedKindergarten = {
id: 1,
name: '阳光幼儿园',
address: '北京市朝阳区阳光街123号',
logo: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'
};
// 页面加载时检查是否已参与助力榜
onMounted(() => {
checkExistingJoinStatus();
});
// 检查现有参与状态
const checkExistingJoinStatus = () => {
// 模拟检查用户是否已经参与助力榜
// 实际应该调用API检查
const hasJoined = true; // 模拟已参与状态
if (hasJoined && mockJoinedKindergarten) {
matchedKindergarten.value = mockJoinedKindergarten;
isAlreadyJoined.value = true;
// 清空输入框,因为已经参与了
mottoChars.value = ['', '', '', ''];
}
};
// 检查并匹配幼儿园
const checkAndMatchKindergarten = () => {
const motto = mottoChars.value.join('');
if (motto.length === 4) {
// 输入完成,查找匹配的幼儿园
const kindergarten = mockKindergartenData[motto];
if (kindergarten) {
// 检查是否要更换幼儿园
if (isAlreadyJoined.value && matchedKindergarten.value && kindergarten.id !== matchedKindergarten.value.id) {
// 用户要更换幼儿园,显示确认提示
showChangeConfirmation(kindergarten);
return;
}
matchedKindergarten.value = kindergarten;
// 如果之前已经参与,保持已参与状态;否则设为未参与
if (!isAlreadyJoined.value) {
isAlreadyJoined.value = false;
}
} else {
matchedKindergarten.value = null;
isAlreadyJoined.value = false;
}
} else {
// 输入未完成,如果之前已参与,保持显示已参与的幼儿园
if (!isAlreadyJoined.value) {
matchedKindergarten.value = null;
}
}
};
// 显示更换幼儿园确认提示
const showChangeConfirmation = (newKindergarten) => {
Taro.showModal({
title: '更换单位/学校',
content: `您要更换新的单位/学校,参与助力榜吗?\n\n新单位:${newKindergarten.name}`,
confirmText: '确认更换',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户确认更换,使用 nextTick 确保状态更新的顺序
nextTick(() => {
matchedKindergarten.value = newKindergarten;
isAlreadyJoined.value = false; // 重置为未参与状态
});
} else {
// 用户取消,清空输入
nextTick(() => {
mottoChars.value = ['', '', '', ''];
});
}
}
});
};
const handleInputChange = (index, value) => {
// 允许输入多个字符,但只保留第一个有效字符(汉字、数字、大小写字母),兼容输入法
if (value) {
// 提取第一个有效字符(汉字、数字、大小写字母)
const firstChar = value.match(/[\u4e00-\u9fa5a-zA-Z0-9]/)?.[0] || '';
mottoChars.value[index] = firstChar;
// 如果输入了有效字符且不是最后一个输入框,自动聚焦下一个
if (firstChar && index < 3) {
focusedIndex.value = index + 1;
// 使用 nextTick 确保 DOM 更新后再聚焦
nextTick(() => {
if (inputRefs.value[index + 1]) {
inputRefs.value[index + 1].focus();
}
});
}
} else {
mottoChars.value[index] = '';
}
// 检查是否输入完成,如果完成则匹配幼儿园
checkAndMatchKindergarten();
};
const handleKeyDown = (index, e) => {
if (e.key === 'Backspace' && !mottoChars.value[index] && index > 0) {
// 同样,在Taro中处理光标移动需要不同的方式
}
};
/**
* 处理输入框失焦事件
* @param {number} index - 输入框索引
*/
const handleBlur = (index) => {
// 重置焦点状态
focusedIndex.value = -1;
// 失焦时再次验证输入值,确保只保留有效字符(汉字、数字、大小写字母)
const currentValue = mottoChars.value[index];
if (currentValue) {
const firstChar = currentValue.match(/[\u4e00-\u9fa5a-zA-Z0-9]/)?.[0] || '';
mottoChars.value[index] = firstChar;
}
};
const isComplete = computed(() => {
return mottoChars.value.every((char) => char) && matchedKindergarten.value;
});
const handleConfirmJoin = async () => {
if (!isComplete.value) return;
// 如果已经参与助力榜,检查是否是更换操作
if (isAlreadyJoined.value) {
// 检查当前匹配的幼儿园是否与之前参与的不同
const currentMotto = mottoChars.value.join('');
const currentKindergarten = mockKindergartenData[currentMotto];
if (currentKindergarten && mockJoinedKindergarten && currentKindergarten.id === mockJoinedKindergarten.id) {
// 相同的幼儿园,提示已参与
Taro.showToast({
title: '您已经参与该助力榜',
icon: 'none'
});
return;
}
}
try {
// 这里应该调用加入助力榜的API
// 暂时使用模拟逻辑
console.log('加入助力榜:', {
kindergarten: matchedKindergarten.value,
motto: mottoChars.value.join('')
});
// 模拟API调用成功
isAlreadyJoined.value = true;
// 更新已参与的幼儿园信息
mockJoinedKindergarten.id = matchedKindergarten.value.id;
mockJoinedKindergarten.name = matchedKindergarten.value.name;
mockJoinedKindergarten.address = matchedKindergarten.value.address;
mockJoinedKindergarten.logo = matchedKindergarten.value.logo;
Taro.showToast({
title: '加入成功',
icon: 'success'
});
setTimeout(() => {
// 返回上一页
Taro.navigateBack({
delta: 1
});
}, 1500);
} catch (error) {
console.error('加入助力榜失败:', error);
Taro.showToast({
title: '加入失败,请重试',
icon: 'none'
});
}
};
</script>
<style lang="less">
@import './index.less';
</style>