hookehuyr

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

添加助力码输入页面,包含4位码输入框和幼儿园信息展示功能
实现输入验证、自动聚焦和幼儿园匹配逻辑
添加确认加入功能和状态提示
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',
......
1 +/*
2 + * @Date: 2025-08-27 18:25:24
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-18 15:23:26
5 + * @FilePath: /lls_program/src/pages/JoinOrganization/index.config.js
6 + * @Description: 文件描述
7 + */
8 +export default {
9 + navigationBarTitleText: '助力码'
10 +}
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 +}
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>