This commit is contained in:
eric
2026-01-29 11:52:04 -06:00
parent b6dd66574d
commit 00165cc262

View File

@@ -1,68 +1,56 @@
// @ts-nocheck import { View, Text, Image, Picker } from "@tarojs/components";
import { Component } from "react";
import { View, Text, Image, Button, Picker } from "@tarojs/components";
import "./index.scss"; import "./index.scss";
import { import {
AtButton, AtButton,
AtInput, AtInput,
AtTabBar,
AtForm, AtForm,
AtMessage, AtMessage,
AtList, AtList,
AtListItem, AtListItem,
AtTag,
AtTextarea, AtTextarea,
} from "taro-ui"; } from "taro-ui";
import React, { useCallback, useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { AtGrid } from "taro-ui";
import { Swiper, SwiperItem } from "@tarojs/components";
import { AtCard } from "taro-ui";
import Taro from "@tarojs/taro"; import Taro from "@tarojs/taro";
import loft from "../../images/loft.png"; import loft from "../../images/loft.png";
import nomadvloglife from "../../images/nomadvloglife.png";
import {
AtModal,
AtModalHeader,
AtModalContent,
AtModalAction,
AtAvatar,
AtAccordion,
} from "taro-ui";
import qrcode from "../../images/qrcode.png"; import qrcode from "../../images/qrcode.png";
import joinGroup from "../../images/joinGroup.png"; import joinGroup from "../../images/joinGroup.png";
const PB_URL = 'https://pocketbase.hackrobot.cn'; const PB_URL = "https://pocketbase.hackrobot.cn";
const COLLECTION = 'solan'; // 你建立的表名 const COLLECTION = "solan";
const PAY_API_URL = 'https://api.hackrobot.cn/payh5'; // 你的支付接口地址(根据实际调整) const PAY_API_URL = "https://api.hackrobot.cn/payh5";
const SUBMIT_APPLICATION_URL =
"https://api.hackrobot.cn/submit_meetup_application";
const Salon = () => { const Salon = () => {
const [meetupQR, setmeetupQR] = useState(false); const [meetupQR, setmeetupQR] = useState(false);
const [isWeChat, setisWeChat] = useState(false); const [isWeChat, setisWeChat] = useState(false);
useEffect(() => { useEffect(() => {
isWeChatFun(); const init = async () => {
checkIfJoined(); isWeChatFun();
checkPendingAndPaid(); checkIfJoined();
const userId = getOrCreateUserId();
const fullyPaid = await checkIfFullyPaid(userId);
if (fullyPaid) {
Taro.setStorageSync("salonJoined", true);
setmeetupQR(true);
}
};
init();
}, []); }, []);
// 检查是否已加入(本地标记)
const checkIfJoined = () => { const checkIfJoined = () => {
if (Taro.getStorageSync('salonJoined')) { if (Taro.getStorageSync("salonJoined")) {
setmeetupQR(true); setmeetupQR(true);
} }
}; };
const handleClick = (value) => {
Taro.navigateTo({
url: "/pages/index/index",
});
};
const isWeChatFun = () => { const isWeChatFun = () => {
var ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
if (ua.indexOf("micromessenger") != -1) { if (ua.indexOf("micromessenger") !== -1) {
setisWeChat(true); setisWeChat(true);
return true; return true;
} else { } else {
@@ -71,25 +59,22 @@ const Salon = () => {
} }
}; };
// 生成或获取 userId(无下划线,干净格式)
const getOrCreateUserId = () => { const getOrCreateUserId = () => {
let userId = Taro.getStorageSync('userId'); let userId = Taro.getStorageSync("userId");
if (!userId) { if (!userId) {
userId = `user${Date.now()}`; userId = `user${Date.now()}`;
Taro.setStorageSync('userId', userId); Taro.setStorageSync("userId", userId);
} }
return userId; return userId;
}; };
// 检查是否已支付(查询 payments 表) const checkIfFullyPaid = (userId) => {
const checkIfPaid = async (userId) => {
return new Promise((resolve) => { return new Promise((resolve) => {
Taro.request({ Taro.request({
url: `${PB_URL}/api/collections/payments/records`, url: `${PB_URL}/api/collections/${COLLECTION}/records`,
method: 'GET', method: "GET",
data: { data: {
filter: `(user_id="${userId}" && type="meetup")`, filter: `(user_id="${userId}" && amount > 0 && type="meetup")`,
sort: '-created',
perPage: 1, perPage: 1,
}, },
success: (res) => { success: (res) => {
@@ -100,210 +85,158 @@ const Salon = () => {
}); });
}; };
// 支付函数(XorPay H5) const payh5 = (type, total_fee = 8800) => {
const payh5 = (type, total_fee = 9900) => { // 99元
const userId = getOrCreateUserId(); const userId = getOrCreateUserId();
Taro.request({ Taro.request({
url: PAY_API_URL, url: PAY_API_URL,
method: 'POST', method: "POST",
data: { data: {
total_fee, total_fee,
type, type,
user_id: userId, user_id: userId,
// return_url: window.location.href, // 如果后端支持,取消注释并在后端加上, 支付成功自动返回
}, },
header: { header: {
'content-type': 'application/json', "content-type": "application/json",
}, },
success: (res) => { success: (res) => {
if (res.data?.status === 'ok' && res.data?.xorpay_params) { if (res.data?.status === "ok" && res.data?.xorpay_params) {
const params = res.data.xorpay_params; const params = res.data.xorpay_params;
const form = document.createElement('form'); const form = document.createElement("form");
form.method = 'POST'; form.method = "POST";
form.action = res.data.xorpay_url; form.action = res.data.xorpay_url;
form.style.display = 'none'; form.style.display = "none";
for (const key in params) { for (const key in params) {
const input = document.createElement('input'); const input = document.createElement("input");
input.type = 'hidden'; input.type = "hidden";
input.name = key; input.name = key;
input.value = params[key]; input.value = params[key];
form.appendChild(input); form.appendChild(input);
} }
// 如果你希望支付成功自动返回,取消注释以下(需后端支持 return_url)
// const returnInput = document.createElement('input');
// returnInput.type = 'hidden';
// returnInput.name = 'return_url';
// returnInput.value = window.location.href;
// form.appendChild(returnInput);
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
} else { } else {
Taro.showToast({ title: '支付发起失败', icon: 'none' }); Taro.showToast({ title: "支付发起失败", icon: "none" });
} }
}, },
fail: () => { fail: () => {
Taro.showToast({ title: '网络错误', icon: 'none' }); Taro.showToast({ title: "网络错误", icon: "none" });
}, },
}); });
}; };
// 表单状态统一管理
const [form, setForm] = useState({ const [form, setForm] = useState({
nickname: '', nickname: "",
occupation: '', occupation: "",
reason: '', reason: "",
wechatId: '', wechatId: "",
gender: '', gender: "",
education: '', education: "",
gradYear: '', gradYear: "",
phone: '', phone: "",
}); });
// 性别下拉选项(保持原样,实现隐形限制) const genderOptions = ["男", "女"];
const genderOptions = ['男', '女'];
const [genderIndex, setGenderIndex] = useState(null); const [genderIndex, setGenderIndex] = useState(null);
// 学历下拉选项(保持原样) const educationOptions = ["高中及以下", "大专", "本科", "硕士"];
const educationOptions = ['高中及以下', '大专', '本科', '硕士'];
const [educationIndex, setEducationIndex] = useState(null); const [educationIndex, setEducationIndex] = useState(null);
// 毕业年份下拉选项
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const yearOptions = []; const yearOptions = [];
for (let i = 0; i <= 60; i++) { for (let i = 0; i <= 60; i++) {
yearOptions.push((currentYear - i).toString()); yearOptions.push((currentYear - i).toString());
} }
const [gradYearIndex, setGradYearIndex] = useState(0); const [gradYearIndex, setGradYearIndex] = useState(null);
// 通用字段更新函数
const updateField = (field) => (value) => { const updateField = (field) => (value) => {
setForm((prev) => ({ ...prev, [field]: value })); setForm((prev) => ({ ...prev, [field]: value }));
}; };
// 性别选择变更(保持原逻辑,实现隐形性别限制)
const handleGenderChange = (e) => { const handleGenderChange = (e) => {
const idx = e.detail.value; const idx = e.detail.value;
setGenderIndex(idx); setGenderIndex(idx);
updateField('gender')(idx === 0 ? '' : genderOptions[idx]); updateField("gender")(genderOptions[idx]);
}; };
// 学历选择变更
const handleEducationChange = (e) => { const handleEducationChange = (e) => {
const idx = e.detail.value; const idx = e.detail.value;
setEducationIndex(idx); setEducationIndex(idx);
updateField('education')(idx === 0 ? '' : educationOptions[idx]); updateField("education")(educationOptions[idx]);
}; };
// 毕业年份选择变更
const handleGradYearChange = (e) => { const handleGradYearChange = (e) => {
const idx = e.detail.value; const idx = e.detail.value;
setGradYearIndex(idx); setGradYearIndex(idx);
updateField('gradYear')(idx === 0 ? '' : yearOptions[idx]); updateField("gradYear")(yearOptions[idx]);
}; };
// 提交申请到 PocketBase const submitApplicationFirst = (submitData) => {
const submitApplication = (submitData) => { return new Promise((resolve, reject) => {
const uploadUrl = `${PB_URL}/api/collections/${COLLECTION}/records`; Taro.request({
url: SUBMIT_APPLICATION_URL,
Taro.request({ method: "POST",
url: uploadUrl, header: { "Content-Type": "application/json" },
method: 'POST', data: submitData,
header: { 'Content-Type': 'application/json' }, success: (res) => {
data: { if (res.statusCode === 200 && res.data?.status === "ok") {
nickname: submitData.nickname, resolve(true);
occupation: submitData.occupation, } else {
reason: submitData.reason, Taro.showToast({ title: "申请提交失败", icon: "none" });
wechatId: submitData.wechatId, reject();
gender: submitData.gender, }
education: submitData.education, },
gradYear: submitData.gradYear, fail: () => {
phone: submitData.phone, Taro.showToast({ title: "网络错误,请重试", icon: "none" });
calculated_age: submitData.calculatedAge, reject();
user_id: submitData.userId || getOrCreateUserId(), },
}, });
success: (res) => {
if (res.statusCode === 200 || res.statusCode === 201) {
Taro.showToast({ title: '申请提交成功,欢迎加入!', icon: 'success' });
Taro.setStorageSync('salonJoined', true);
setmeetupQR(true);
localStorage.removeItem('pendingSalonApplication');
} else {
Taro.showToast({ title: '提交失败', icon: 'none' });
}
},
fail: () => {
Taro.showToast({ title: '提交失败,请检查网络', icon: 'none' });
},
}); });
}; };
// 检查本地是否有 pending 申请且已支付 → 自动提交
const checkPendingAndPaid = async () => {
const pending = localStorage.getItem('pendingSalonApplication');
if (!pending) return;
const pendingData = JSON.parse(pending);
const userId = getOrCreateUserId();
if (pendingData.userId !== userId) return;
const paid = await checkIfPaid(userId);
if (paid) {
submitApplication({
...pendingData.form,
calculatedAge: pendingData.calculatedAge,
userId,
});
}
};
const handleSubmit = async () => { const handleSubmit = async () => {
// 原有必填校验(包含性别隐形限制) let missing = [];
if (
!form.nickname || if (!form.nickname) missing.push("昵称");
!form.occupation || if (!form.occupation) missing.push("职业/专业");
!form.reason || if (!form.reason) missing.push("自我介绍");
!form.wechatId || if (!form.wechatId) missing.push("微信号");
!form.gender if (!form.gender) missing.push("性别");
) { if (!form.education) missing.push("学历");
if (!form.gradYear) missing.push("毕业时间");
if (!form.phone) missing.push("手机号"); // 手机号必填
if (missing.length > 0) {
Taro.atMessage({ Taro.atMessage({
message: '请填写所有必填项', message: `请填写:${missing.join("、")}`,
type: 'warning', type: "warning",
}); });
return; return;
} }
// 隐形校验学历和毕业年份(必须有效) // 手机号正则校验(必填,必须11位有效手机号)
if ( if (!/^1[3-9]\d{9}$/.test(form.phone)) {
!form.education ||
!form.gradYear ||
form.gradYear === '请选择毕业年份'
) {
Taro.atMessage({ Taro.atMessage({
message: '请填写所有必填项', message: "手机号格式错误,请填写11位有效手机号",
type: 'warning', type: "warning",
}); });
return; return;
} }
// 计算估算年龄
const gradAgeMap = { const gradAgeMap = {
'高中及以下': 18, 高中及以下: 18,
'大专': 21, 大专: 21,
'本科': 22, 本科: 22,
'硕士': 25, 硕士: 25,
}; };
const baseAge = gradAgeMap[form.education] || 22; const baseAge = gradAgeMap[form.education] || 22;
const gradYearNum = parseInt(form.gradYear, 10); const gradYearNum = parseInt(form.gradYear, 10);
if (isNaN(gradYearNum)) { if (isNaN(gradYearNum)) {
Taro.atMessage({ Taro.atMessage({
message: '请填写所有必填项', message: "毕业年份格式错误",
type: 'warning', type: "warning",
}); });
return; return;
} }
@@ -312,144 +245,254 @@ const Salon = () => {
const userId = getOrCreateUserId(); const userId = getOrCreateUserId();
// 保存 pending 申请(用于支付后自动提交) const submitData = {
localStorage.setItem('pendingSalonApplication', JSON.stringify({ ...form,
form, calculated_age: calculatedAge,
calculatedAge, user_id: userId,
userId, };
}));
// 检查是否已支付 try {
const paid = await checkIfPaid(userId); await submitApplicationFirst(submitData);
if (paid) {
// 已支付,直接提交 Taro.showToast({
submitApplication({ ...form, calculatedAge, userId }); title: "申请已提交,正在跳转支付(88元场地管理费)",
} else { icon: "loading",
// 未支付,发起支付(99元,类型 meetup) duration: 3000,
payh5('meetup', 9900); });
Taro.showToast({ title: '正在跳转支付,请完成支付后返回', icon: 'loading', duration: 3000 });
payh5("meetup", 8800);
} catch {
// 提交失败已提示
} }
}; };
// 如果已加入(支付+提交成功),显示成功页面
if (meetupQR) { if (meetupQR) {
return ( return (
<View className="Model" style={{ padding: '20px', textAlign: 'center' }}> <View
<Image style="width: 100%;height: 100px;background: #fff;" src={loft} /> className="Model"
<View style={{ marginTop: '40px' }}> style={{ padding: "20px", textAlign: "center", background: "#f8f9fa" }}
<Text style={{ fontSize: '20px', fontWeight: 'bold' }}>欢迎加入数字游民社区!</Text> >
<Image
style="width: 100%; height: 120px; borderRadius: 12px; boxShadow: 0 4px 12px rgba(0,0,0,0.1);"
src={loft}
/>
<View style={{ marginTop: "40px" }}>
<Text
style={{ fontSize: "24px", fontWeight: "bold", color: "#2c3e50" }}
>
🎉 欢迎加入数字游民社区!
</Text>
</View> </View>
<View style={{ margin: '30px 0' }}> <View style={{ margin: "30px 0" }}>
{isWeChat ? ( {isWeChat ? (
<Image src={qrcode} mode="widthFix" style="width: 80%;" /> <Image
src={qrcode}
mode="widthFix"
style="width: 80%; borderRadius: 12px; boxShadow: 0 4px 12px rgba(0,0,0,0.1);"
/>
) : ( ) : (
<View> <View>
<Text>请在微信中打开本页面以查看社群二维码</Text> <Text style={{ fontSize: "16px", color: "#e74c3c" }}>
<Image src={joinGroup} mode="widthFix" style="width: 80%; marginTop: 20px;" /> 🔴 请在微信中打开本页面以查看社群二维码
</Text>
<Image
src={joinGroup}
mode="widthFix"
style="width: 80%; marginTop: 20px; borderRadius: 12px;"
/>
</View> </View>
)} )}
</View> </View>
<Text style={{ fontSize: "14px", color: "#7f8c8d", marginTop: "20px" }}>
💡 入群后请遵守社区规则,共同维护良好氛围~
</Text>
</View> </View>
); );
} }
// 默认显示表单
return ( return (
<View className="Model"> <View
<View className="meetup-left"> className="Model"
<Image style="width: 100%;height: 100px;background: #fff;" src={loft} /> style={{ background: "#f0f4f8", minHeight: "100vh" }}
>
<View
className="meetup-left"
style={{ background: "#fff", boxShadow: "0 4px 12px rgba(0,0,0,0.05)" }}
>
<Image
style="width: 100%; height: 140px; borderRadius: '12px 12px 0 0';"
src={loft}
/>
<View style={{ padding: "20px", textAlign: "center" }}>
<Text
style={{ fontSize: "22px", fontWeight: "bold", color: "#2c3e50" }}
>
🌍 数字游民社区报名
</Text>
<br />
<Text
style={{
fontSize: "14px",
color: "#7f8c8d",
marginTop: "10px",
lineHeight: "1.6",
whiteSpace: "pre-line",
}}
>
💼 结识志同道合的伙伴
<br />
📍 线下聚会 + 线上分享
<br />
🤝 互助成长,资源共享
</Text>
</View>
</View> </View>
<View className="index-page" style={{ padding: "20px" }}> <View
className="index-page"
style={{
padding: "20px",
background: "#fff",
marginTop: "20px",
borderRadius: "16px",
boxShadow: "0 4px 20px rgba(0,0,0,0.08)",
}}
>
<AtMessage /> <AtMessage />
<AtForm onSubmit={handleSubmit}> <AtForm onSubmit={handleSubmit}>
<AtInput <AtInput
title='昵称' title="🌟 昵称"
type='text' type="text"
placeholder='请输入您的昵称(必填)' placeholder="请输入您的昵称"
value={form.nickname} value={form.nickname}
onChange={updateField('nickname')} onChange={updateField("nickname")}
/> />
{/* 性别下拉(保持原样) */} <Picker
<Picker mode="selector" range={genderOptions} value={genderIndex} onChange={handleGenderChange}> mode="selector"
range={genderOptions}
value={genderIndex ?? 0}
onChange={handleGenderChange}
>
<View className="picker-item"> <View className="picker-item">
<AtListItem <AtListItem
title="性别" title="👤 性别"
extraText={genderOptions[genderIndex]} extraText={form.gender || "请选择"}
arrow="right" arrow="right"
/> />
</View> </View>
</Picker> </Picker>
<AtInput <AtInput
title='职业/专业' title="💼 职业"
type='text' type="text"
placeholder='您的职业' placeholder="您的职业或专业"
value={form.occupation} value={form.occupation}
onChange={updateField('occupation')} onChange={updateField("occupation")}
/> />
<View style={{ margin: '30px 0' }}> <View style={{ margin: "30px 0" }}>
<Text style={{ fontWeight: 'bold', marginBottom: '10px', display: 'block' }}> <Text
自我介绍 style={{
fontSize: "16px",
fontWeight: "bold",
color: "#2c3e50",
marginBottom: "10px",
display: "block",
}}
>
📝 自我介绍
</Text> </Text>
<AtTextarea <AtTextarea
value={form.reason} value={form.reason}
onChange={updateField('reason')} onChange={updateField("reason")}
maxLength={500} maxLength={500}
height={150} height={180}
placeholder='越详细越有助于审核' placeholder="分享您的经历、兴趣或加入原因,越详细越好哦~"
style={{ borderRadius: "12px", background: "#f8f9fa" }}
/> />
</View> </View>
<AtInput <AtInput
title='微信号' title="💬 微信号"
type='text' type="text"
placeholder='必填' placeholder="用于拉群"
value={form.wechatId} value={form.wechatId}
onChange={updateField('wechatId')} onChange={updateField("wechatId")}
/> />
{/* 学历下拉 */} <Picker
<Picker mode="selector" range={educationOptions} value={educationIndex} onChange={handleEducationChange}> mode="selector"
range={educationOptions}
value={educationIndex ?? 0}
onChange={handleEducationChange}
>
<View className="picker-item"> <View className="picker-item">
<AtListItem <AtListItem
title="学历" title="🎓 学历"
extraText={educationOptions[educationIndex]} extraText={form.education || "请选择"}
arrow="right" arrow="right"
/> />
</View> </View>
</Picker> </Picker>
{/* 毕业年份下拉 */} <Picker
<Picker mode="selector" range={yearOptions} value={gradYearIndex} onChange={handleGradYearChange}> mode="selector"
range={yearOptions}
value={gradYearIndex ?? 0}
onChange={handleGradYearChange}
>
<View className="picker-item"> <View className="picker-item">
<AtListItem <AtListItem
title="毕业时间" title="📅 毕业时间"
extraText={yearOptions[gradYearIndex]} extraText={form.gradYear || "请选择"}
arrow="right" arrow="right"
/> />
</View> </View>
</Picker> </Picker>
<AtInput <AtInput
title='手机号' title="📱 手机号"
type='phone' type="phone"
placeholder='用于紧急联系' placeholder="用于紧急联系"
value={form.phone} value={form.phone}
onChange={updateField('phone')} onChange={updateField("phone")}
/> />
<AtButton formType='submit' type='primary' style={{ marginTop: '40px' }}> <View style={{ marginTop: "40px" }}>
提交申请 <AtButton
</AtButton> formType="submit"
type="primary"
size="normal"
circle
style={{
background: "#27ae60",
borderColor: "#27ae60",
fontSize: "18px",
fontWeight: "bold",
}}
>
提交申请
</AtButton>
<View style={{ marginTop: "16px", textAlign: "center" }}>
<Text
style={{
fontSize: "14px",
color: "#e67e22",
lineHeight: "1.6",
whiteSpace: "pre-line",
}}
>
🔍 提交后将进行审核
<br />
💰 通过审核需支付88元场地管理费
<br />
❤️ 组织者纯公益,不赚钱,所有费用用于场地和活动
</Text>
</View>
</View>
</AtForm> </AtForm>
<View style={{ marginTop: '20px', color: '#666', fontSize: '12px' }}>
{/* <Text>支付后申请将自动提交,支付成功请返回本页面(或刷新)</Text> */}
</View>
</View> </View>
</View> </View>
); );