feat(JoinOrganization): 新增助力码输入页面及功能
添加助力码输入页面,包含4位码输入框和幼儿园信息展示功能 实现输入验证、自动聚焦和幼儿园匹配逻辑 添加确认加入功能和状态提示
Showing
4 changed files
with
406 additions
and
1 deletions
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-06-28 10:33:00 | 2 | * @Date: 2025-06-28 10:33:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-09-17 19:58:26 | 4 | + * @LastEditTime: 2025-09-18 15:23:48 |
| 5 | * @FilePath: /lls_program/src/app.config.js | 5 | * @FilePath: /lls_program/src/app.config.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| ... | @@ -35,6 +35,7 @@ export default { | ... | @@ -35,6 +35,7 @@ export default { |
| 35 | 'pages/FamilyRank/index', | 35 | 'pages/FamilyRank/index', |
| 36 | 'pages/PosterCheckin/index', | 36 | 'pages/PosterCheckin/index', |
| 37 | 'pages/CheckinList/index', | 37 | 'pages/CheckinList/index', |
| 38 | + 'pages/JoinOrganization/index', | ||
| 38 | ], | 39 | ], |
| 39 | window: { | 40 | window: { |
| 40 | backgroundTextStyle: 'light', | 41 | backgroundTextStyle: 'light', | ... | ... |
src/pages/JoinOrganization/index.config.js
0 → 100644
src/pages/JoinOrganization/index.less
0 → 100644
| 1 | +/* JoinFamily/index.less */ | ||
| 2 | + | ||
| 3 | +.motto-input-container { | ||
| 4 | + display: flex; | ||
| 5 | + justify-content: space-between; | ||
| 6 | + margin-bottom: 1rem; | ||
| 7 | +} | ||
| 8 | + | ||
| 9 | +.motto-input-box { | ||
| 10 | + width: 5rem; | ||
| 11 | + height: 5rem; | ||
| 12 | + text-align: center; | ||
| 13 | + border: 1px solid #d1d5db; | ||
| 14 | + border-radius: 0.5rem; | ||
| 15 | + display: flex; | ||
| 16 | + align-items: center; | ||
| 17 | + justify-content: center; | ||
| 18 | + transition: border-color 0.2s; | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +.motto-input { | ||
| 22 | + width: 100%; | ||
| 23 | + height: 100%; | ||
| 24 | + text-align: center; | ||
| 25 | + font-size: 1.5rem; | ||
| 26 | + background-color: transparent; | ||
| 27 | + border: none; | ||
| 28 | + outline: none; | ||
| 29 | + padding: 0; | ||
| 30 | + margin: 0; | ||
| 31 | + box-sizing: border-box; | ||
| 32 | + color: inherit; | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +.identity-title { | ||
| 36 | + font-size: 32rpx; | ||
| 37 | + font-weight: 600; | ||
| 38 | + color: #374151; | ||
| 39 | + margin-bottom: 24rpx; | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +// 家庭选择弹窗样式 | ||
| 43 | +.family-selector-container { | ||
| 44 | + display: flex; | ||
| 45 | + flex-direction: column; | ||
| 46 | + height: 100%; | ||
| 47 | + background: white; | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +.family-item { | ||
| 51 | + cursor: pointer; | ||
| 52 | + transition: all 0.2s ease; | ||
| 53 | + | ||
| 54 | + &:hover { | ||
| 55 | + transform: translateY(-2rpx); | ||
| 56 | + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + &:active { | ||
| 60 | + transform: translateY(0); | ||
| 61 | + } | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +// 文本截断样式 | ||
| 65 | +.line-clamp-2 { | ||
| 66 | + display: -webkit-box; | ||
| 67 | + -webkit-line-clamp: 2; | ||
| 68 | + -webkit-box-orient: vertical; | ||
| 69 | + overflow: hidden; | ||
| 70 | + text-overflow: ellipsis; | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// 间距样式 | ||
| 74 | +.space-y-3 > * + * { | ||
| 75 | + margin-top: 24rpx; | ||
| 76 | +} | ||
| 77 | + | ||
| 78 | +.space-x-3 > * + * { | ||
| 79 | + margin-left: 24rpx; | ||
| 80 | +} |
src/pages/JoinOrganization/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="min-h-screen flex flex-col bg-white"> | ||
| 3 | + <view class="flex-1 px-4 pt-3 pb-6 flex flex-col"> | ||
| 4 | + <!-- Title --> | ||
| 5 | + <h2 class="text-xl font-bold text-center mb-2"> | ||
| 6 | + 输入助力码 | ||
| 7 | + </h2> | ||
| 8 | + <!-- Description --> | ||
| 9 | + <view class="text-gray-600 text-center text-sm mb-6"> | ||
| 10 | + 请输入家人提供的助力码,参与助力榜排行 | ||
| 11 | + </view> | ||
| 12 | + <!-- Input boxes --> | ||
| 13 | + <view class="motto-input-container"> | ||
| 14 | + <view | ||
| 15 | + v-for="(char, index) in mottoChars" | ||
| 16 | + :key="index" | ||
| 17 | + class="motto-input-box" | ||
| 18 | + :style="{ | ||
| 19 | + borderColor: focusedIndex === index ? THEME_COLORS.PRIMARY : '#d1d5db' | ||
| 20 | + }" | ||
| 21 | + > | ||
| 22 | + <input | ||
| 23 | + :ref="(el) => (inputRefs[index] = el)" | ||
| 24 | + type="text" | ||
| 25 | + v-model="mottoChars[index]" | ||
| 26 | + @input="(e) => handleInputChange(index, e.target.value)" | ||
| 27 | + @keydown="(e) => handleKeyDown(index, e)" | ||
| 28 | + @focus="focusedIndex = index" | ||
| 29 | + @blur="handleBlur(index)" | ||
| 30 | + class="motto-input" | ||
| 31 | + :cursorSpacing="100" | ||
| 32 | + /> | ||
| 33 | + </view> | ||
| 34 | + </view> | ||
| 35 | + <!-- Help text --> | ||
| 36 | + <view class="text-gray-500 text-center text-sm mb-4"> | ||
| 37 | + 没有口令?请联系家里小辈,咨询单位学校是否参与了助力榜排行哦 | ||
| 38 | + </view> | ||
| 39 | + | ||
| 40 | + <!-- 幼儿园信息显示 --> | ||
| 41 | + <view v-if="matchedKindergarten" class="mb-6"> | ||
| 42 | + <view class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4"> | ||
| 43 | + <view class="flex items-center space-x-3"> | ||
| 44 | + <!-- 幼儿园Logo --> | ||
| 45 | + <view class="w-12 h-12 rounded-full bg-white flex items-center justify-center overflow-hidden border-gray-200 border"> | ||
| 46 | + <image | ||
| 47 | + :src="matchedKindergarten.logo || defaultKindergartenLogo" | ||
| 48 | + class="w-full h-full object-cover" | ||
| 49 | + /> | ||
| 50 | + </view> | ||
| 51 | + <!-- 幼儿园信息 --> | ||
| 52 | + <view class="flex-1"> | ||
| 53 | + <view class="font-medium text-gray-900 mb-1">{{ matchedKindergarten.name }}</view> | ||
| 54 | + <view class="text-sm text-gray-600">{{ matchedKindergarten.address }}</view> | ||
| 55 | + </view> | ||
| 56 | + </view> | ||
| 57 | + </view> | ||
| 58 | + | ||
| 59 | + <!-- 确认提示 --> | ||
| 60 | + <view v-if="!isAlreadyJoined" class="text-center mb-4"> | ||
| 61 | + <view class="text-gray-700 text-sm mb-2">这是您家小辈所在的单位/学校吗?</view> | ||
| 62 | + <view class="text-gray-600 text-xs"> | ||
| 63 | + <view>确认后您的家庭步数将计入其中,参与助力榜的排行哦</view> | ||
| 64 | + <view>(不会影响家庭参与市、区榜)</view> | ||
| 65 | + </view> | ||
| 66 | + </view> | ||
| 67 | + | ||
| 68 | + <!-- 已参与提示 --> | ||
| 69 | + <view v-if="isAlreadyJoined" class="text-center mb-4"> | ||
| 70 | + <view class="text-green-600 text-sm font-medium">您的家庭已经参与助力榜</view> | ||
| 71 | + </view> | ||
| 72 | + </view> | ||
| 73 | + <!-- Submit Button --> | ||
| 74 | + <view | ||
| 75 | + @tap="handleConfirmJoin" | ||
| 76 | + :disabled="!isComplete" | ||
| 77 | + :class="[ | ||
| 78 | + 'w-full py-3 text-white text-lg font-medium rounded-lg mt-auto text-center', | ||
| 79 | + isComplete ? 'bg-blue-500' : 'bg-gray-300' | ||
| 80 | + ]" | ||
| 81 | + > | ||
| 82 | + 确认加入 | ||
| 83 | + </view> | ||
| 84 | + </view> | ||
| 85 | + | ||
| 86 | + </view> | ||
| 87 | +</template> | ||
| 88 | + | ||
| 89 | +<script setup> | ||
| 90 | +import { ref, computed, nextTick, onMounted, watch } from 'vue'; | ||
| 91 | +import Taro from '@tarojs/taro'; | ||
| 92 | +import { My, Check, IconFont } from '@nutui/icons-vue-taro'; | ||
| 93 | +// 获取接口信息 | ||
| 94 | +import { searchFamilyByPassphraseAPI, joinFamilyAPI } from '@/api/family'; | ||
| 95 | +// 导入主题颜色 | ||
| 96 | +import { THEME_COLORS } from '@/utils/config'; | ||
| 97 | +// 默认幼儿园Logo | ||
| 98 | +const defaultKindergartenLogo = 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 99 | + | ||
| 100 | +const mottoChars = ref(['', '', '', '']); | ||
| 101 | +const inputRefs = ref([]); | ||
| 102 | +const focusedIndex = ref(-1); | ||
| 103 | + | ||
| 104 | +// 幼儿园相关数据 | ||
| 105 | +const matchedKindergarten = ref(null); | ||
| 106 | +const isAlreadyJoined = ref(false); | ||
| 107 | + | ||
| 108 | +// Mock 幼儿园数据 | ||
| 109 | +const mockKindergartenData = { | ||
| 110 | + '1234': { | ||
| 111 | + id: 1, | ||
| 112 | + name: '阳光幼儿园', | ||
| 113 | + address: '北京市朝阳区阳光街123号', | ||
| 114 | + logo: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 115 | + }, | ||
| 116 | + '5678': { | ||
| 117 | + id: 2, | ||
| 118 | + name: '彩虹幼儿园', | ||
| 119 | + address: '北京市海淀区彩虹路456号', | ||
| 120 | + logo: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 121 | + } | ||
| 122 | +}; | ||
| 123 | + | ||
| 124 | +// Mock 已参与助力榜的数据(模拟用户已经参与的幼儿园) | ||
| 125 | +const mockJoinedKindergarten = { | ||
| 126 | + id: 1, | ||
| 127 | + name: '阳光幼儿园', | ||
| 128 | + address: '北京市朝阳区阳光街123号', | ||
| 129 | + logo: 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg' | ||
| 130 | +}; | ||
| 131 | + | ||
| 132 | +// 页面加载时检查是否已参与助力榜 | ||
| 133 | +onMounted(() => { | ||
| 134 | + checkExistingJoinStatus(); | ||
| 135 | +}); | ||
| 136 | + | ||
| 137 | +// 检查现有参与状态 | ||
| 138 | +const checkExistingJoinStatus = () => { | ||
| 139 | + // 模拟检查用户是否已经参与助力榜 | ||
| 140 | + // 实际应该调用API检查 | ||
| 141 | + const hasJoined = true; // 模拟已参与状态 | ||
| 142 | + | ||
| 143 | + if (hasJoined && mockJoinedKindergarten) { | ||
| 144 | + matchedKindergarten.value = mockJoinedKindergarten; | ||
| 145 | + isAlreadyJoined.value = true; | ||
| 146 | + // 清空输入框,因为已经参与了 | ||
| 147 | + mottoChars.value = ['', '', '', '']; | ||
| 148 | + } | ||
| 149 | +}; | ||
| 150 | + | ||
| 151 | +// 检查并匹配幼儿园 | ||
| 152 | +const checkAndMatchKindergarten = () => { | ||
| 153 | + const motto = mottoChars.value.join(''); | ||
| 154 | + | ||
| 155 | + if (motto.length === 4) { | ||
| 156 | + // 输入完成,查找匹配的幼儿园 | ||
| 157 | + const kindergarten = mockKindergartenData[motto]; | ||
| 158 | + if (kindergarten) { | ||
| 159 | + // 检查是否要更换幼儿园 | ||
| 160 | + if (isAlreadyJoined.value && matchedKindergarten.value && kindergarten.id !== matchedKindergarten.value.id) { | ||
| 161 | + // 用户要更换幼儿园,显示确认提示 | ||
| 162 | + showChangeConfirmation(kindergarten); | ||
| 163 | + return; | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + matchedKindergarten.value = kindergarten; | ||
| 167 | + // 如果之前已经参与,保持已参与状态;否则设为未参与 | ||
| 168 | + if (!isAlreadyJoined.value) { | ||
| 169 | + isAlreadyJoined.value = false; | ||
| 170 | + } | ||
| 171 | + } else { | ||
| 172 | + matchedKindergarten.value = null; | ||
| 173 | + isAlreadyJoined.value = false; | ||
| 174 | + } | ||
| 175 | + } else { | ||
| 176 | + // 输入未完成,如果之前已参与,保持显示已参与的幼儿园 | ||
| 177 | + if (!isAlreadyJoined.value) { | ||
| 178 | + matchedKindergarten.value = null; | ||
| 179 | + } | ||
| 180 | + } | ||
| 181 | +}; | ||
| 182 | + | ||
| 183 | +// 显示更换幼儿园确认提示 | ||
| 184 | +const showChangeConfirmation = (newKindergarten) => { | ||
| 185 | + Taro.showModal({ | ||
| 186 | + title: '更换单位/学校', | ||
| 187 | + content: `您要更换新的单位/学校,参与助力榜吗?\n\n新单位:${newKindergarten.name}`, | ||
| 188 | + confirmText: '确认更换', | ||
| 189 | + cancelText: '取消', | ||
| 190 | + success: (res) => { | ||
| 191 | + if (res.confirm) { | ||
| 192 | + // 用户确认更换,使用 nextTick 确保状态更新的顺序 | ||
| 193 | + nextTick(() => { | ||
| 194 | + matchedKindergarten.value = newKindergarten; | ||
| 195 | + isAlreadyJoined.value = false; // 重置为未参与状态 | ||
| 196 | + }); | ||
| 197 | + } else { | ||
| 198 | + // 用户取消,清空输入 | ||
| 199 | + nextTick(() => { | ||
| 200 | + mottoChars.value = ['', '', '', '']; | ||
| 201 | + }); | ||
| 202 | + } | ||
| 203 | + } | ||
| 204 | + }); | ||
| 205 | +}; | ||
| 206 | + | ||
| 207 | +const handleInputChange = (index, value) => { | ||
| 208 | + // 允许输入多个字符,但只保留第一个有效字符(汉字、数字、大小写字母),兼容输入法 | ||
| 209 | + if (value) { | ||
| 210 | + // 提取第一个有效字符(汉字、数字、大小写字母) | ||
| 211 | + const firstChar = value.match(/[\u4e00-\u9fa5a-zA-Z0-9]/)?.[0] || ''; | ||
| 212 | + mottoChars.value[index] = firstChar; | ||
| 213 | + | ||
| 214 | + // 如果输入了有效字符且不是最后一个输入框,自动聚焦下一个 | ||
| 215 | + if (firstChar && index < 3) { | ||
| 216 | + focusedIndex.value = index + 1; | ||
| 217 | + // 使用 nextTick 确保 DOM 更新后再聚焦 | ||
| 218 | + nextTick(() => { | ||
| 219 | + if (inputRefs.value[index + 1]) { | ||
| 220 | + inputRefs.value[index + 1].focus(); | ||
| 221 | + } | ||
| 222 | + }); | ||
| 223 | + } | ||
| 224 | + } else { | ||
| 225 | + mottoChars.value[index] = ''; | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + // 检查是否输入完成,如果完成则匹配幼儿园 | ||
| 229 | + checkAndMatchKindergarten(); | ||
| 230 | +}; | ||
| 231 | + | ||
| 232 | +const handleKeyDown = (index, e) => { | ||
| 233 | + if (e.key === 'Backspace' && !mottoChars.value[index] && index > 0) { | ||
| 234 | + // 同样,在Taro中处理光标移动需要不同的方式 | ||
| 235 | + } | ||
| 236 | +}; | ||
| 237 | + | ||
| 238 | +/** | ||
| 239 | + * 处理输入框失焦事件 | ||
| 240 | + * @param {number} index - 输入框索引 | ||
| 241 | + */ | ||
| 242 | +const handleBlur = (index) => { | ||
| 243 | + // 重置焦点状态 | ||
| 244 | + focusedIndex.value = -1; | ||
| 245 | + | ||
| 246 | + // 失焦时再次验证输入值,确保只保留有效字符(汉字、数字、大小写字母) | ||
| 247 | + const currentValue = mottoChars.value[index]; | ||
| 248 | + if (currentValue) { | ||
| 249 | + const firstChar = currentValue.match(/[\u4e00-\u9fa5a-zA-Z0-9]/)?.[0] || ''; | ||
| 250 | + mottoChars.value[index] = firstChar; | ||
| 251 | + } | ||
| 252 | +}; | ||
| 253 | + | ||
| 254 | +const isComplete = computed(() => { | ||
| 255 | + return mottoChars.value.every((char) => char) && matchedKindergarten.value; | ||
| 256 | +}); | ||
| 257 | +const handleConfirmJoin = async () => { | ||
| 258 | + if (!isComplete.value) return; | ||
| 259 | + | ||
| 260 | + // 如果已经参与助力榜,检查是否是更换操作 | ||
| 261 | + if (isAlreadyJoined.value) { | ||
| 262 | + // 检查当前匹配的幼儿园是否与之前参与的不同 | ||
| 263 | + const currentMotto = mottoChars.value.join(''); | ||
| 264 | + const currentKindergarten = mockKindergartenData[currentMotto]; | ||
| 265 | + | ||
| 266 | + if (currentKindergarten && mockJoinedKindergarten && currentKindergarten.id === mockJoinedKindergarten.id) { | ||
| 267 | + // 相同的幼儿园,提示已参与 | ||
| 268 | + Taro.showToast({ | ||
| 269 | + title: '您已经参与该助力榜', | ||
| 270 | + icon: 'none' | ||
| 271 | + }); | ||
| 272 | + return; | ||
| 273 | + } | ||
| 274 | + } | ||
| 275 | + | ||
| 276 | + try { | ||
| 277 | + // 这里应该调用加入助力榜的API | ||
| 278 | + // 暂时使用模拟逻辑 | ||
| 279 | + console.log('加入助力榜:', { | ||
| 280 | + kindergarten: matchedKindergarten.value, | ||
| 281 | + motto: mottoChars.value.join('') | ||
| 282 | + }); | ||
| 283 | + | ||
| 284 | + // 模拟API调用成功 | ||
| 285 | + isAlreadyJoined.value = true; | ||
| 286 | + // 更新已参与的幼儿园信息 | ||
| 287 | + mockJoinedKindergarten.id = matchedKindergarten.value.id; | ||
| 288 | + mockJoinedKindergarten.name = matchedKindergarten.value.name; | ||
| 289 | + mockJoinedKindergarten.address = matchedKindergarten.value.address; | ||
| 290 | + mockJoinedKindergarten.logo = matchedKindergarten.value.logo; | ||
| 291 | + | ||
| 292 | + Taro.showToast({ | ||
| 293 | + title: '加入成功', | ||
| 294 | + icon: 'success' | ||
| 295 | + }); | ||
| 296 | + | ||
| 297 | + setTimeout(() => { | ||
| 298 | + // 返回上一页 | ||
| 299 | + Taro.navigateBack({ | ||
| 300 | + delta: 1 | ||
| 301 | + }); | ||
| 302 | + }, 1500); | ||
| 303 | + } catch (error) { | ||
| 304 | + console.error('加入助力榜失败:', error); | ||
| 305 | + Taro.showToast({ | ||
| 306 | + title: '加入失败,请重试', | ||
| 307 | + icon: 'none' | ||
| 308 | + }); | ||
| 309 | + } | ||
| 310 | +}; | ||
| 311 | +</script> | ||
| 312 | +<style lang="less"> | ||
| 313 | +@import './index.less'; | ||
| 314 | +</style> |
-
Please register or login to post a comment