From ff31ef80015601e3c928276c7a3952f6af1e638b Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 16 Mar 2026 04:28:16 -0500 Subject: [PATCH] =?UTF-8?q?'=E5=B0=8F=E7=BA=A2=E4=B9=A6=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/join/route.ts | 56 +- app/api/xiaohongshu/profile/route.ts | 63 ++ app/api/xiaohongshu/validate/route.ts | 29 + app/components/AddressLink.tsx | 37 ++ app/components/Footer.tsx | 11 +- app/components/Header.tsx | 3 - app/components/HeroSection.tsx | 5 +- app/components/SessionCard.tsx | 3 +- app/globals.css | 9 + app/history/[id]/page.tsx | 5 +- app/host/[id]/page.tsx | 75 +++ app/join/page.tsx | 759 ++++++++++++------------ app/page.tsx | 34 +- config/home.config.ts | 9 + public/images/wechat-qr-placeholder.svg | 5 + 15 files changed, 700 insertions(+), 403 deletions(-) create mode 100644 app/api/xiaohongshu/profile/route.ts create mode 100644 app/api/xiaohongshu/validate/route.ts create mode 100644 app/components/AddressLink.tsx create mode 100644 app/host/[id]/page.tsx create mode 100644 public/images/wechat-qr-placeholder.svg diff --git a/app/api/join/route.ts b/app/api/join/route.ts index 7087fb3..84b257f 100644 --- a/app/api/join/route.ts +++ b/app/api/join/route.ts @@ -28,8 +28,15 @@ export async function POST(request: NextRequest) { const city = String(formData.city || "").trim(); const session = String(formData.session || "").trim(); const ageRange = String(formData.age_range || "").trim(); - const wechatOrPhone = String(formData.wechat_or_phone || "").trim(); + const wechatOrPhone = String(formData.wechat_or_phone || formData.wechat || "").trim(); + const wechat = String(formData.wechat || "").trim(); + const phone = String(formData.phone || "").trim(); + const profession = String(formData.profession || "").trim(); + const education = String(formData.education || "").trim(); + const graduationYear = String(formData.graduation_year || formData.gradYear || "").trim(); const xiaohongshuUrl = String(formData.xiaohongshu_url || "").trim(); + const xiaohongshuAvatarUrl = String(formData.xiaohongshu_avatar_url || formData.media || "").trim(); + const gender = String(formData.gender || "").trim() || "\u65e0"; let intro = String(formData.intro || "").trim(); const firstTime = String(formData.first_time || "").trim(); const agreeRules = !!formData.agree_rules; @@ -43,32 +50,59 @@ export async function POST(request: NextRequest) { intro = extra; } - if (!nickname || !wechatOrPhone || !intro || !session || !agreeRules || !xiaohongshuUrl) { + const isDigitalNomad = session === "digital-nomad"; + const contact = wechatOrPhone || wechat || phone; + if (!nickname || !intro || !session || !agreeRules || !xiaohongshuUrl) { return NextResponse.json( - { ok: false, error: "请填写必填项(含小红书主页地址)" }, + { ok: false, error: "请填写必填项(含小红书主页地址、自我介绍)" }, + { status: 400 } + ); + } + if (!contact) { + return NextResponse.json( + { ok: false, error: "请填写微信号或手机号" }, + { status: 400 } + ); + } + if (isDigitalNomad && (!education || !graduationYear)) { + return NextResponse.json( + { ok: false, error: "请选择学历和毕业时间" }, { status: 400 } ); } const userId = getUserIdFromCookie(request) || generateAnonId(); + const EDUCATION_GRAD_AGE: Record = { + "高中及以下": 18, + 大专: 21, + 本科: 22, + 硕士: 25, + 博士: 28, + }; + const gradAge = EDUCATION_GRAD_AGE[education] ?? 22; + const estimatedAge = new Date().getFullYear() - parseInt(graduationYear, 10) + gradAge; + const ageRangeForRecord = isDigitalNomad && education && graduationYear + ? `推算约${estimatedAge}岁(学历${education}+${graduationYear}毕业)` + : ageRange; + const record = { user_id: userId, nickname, - occupation: city, + occupation: isDigitalNomad ? profession : city, reason: intro, - wechatId: wechatOrPhone, + wechatId: wechat || wechatOrPhone, xiaohongshu_url: xiaohongshuUrl, - gender: "", - education: "", - gradYear: "", - phone: wechatOrPhone, + gender, + education: education, + gradYear: graduationYear, + phone: phone || wechatOrPhone, single: "", - media: "", + media: xiaohongshuAvatarUrl, type: session, order_id: "", amount: 0, - age_range: ageRange, + age_range: ageRangeForRecord, first_time: firstTime, }; diff --git a/app/api/xiaohongshu/profile/route.ts b/app/api/xiaohongshu/profile/route.ts new file mode 100644 index 0000000..b50a2df --- /dev/null +++ b/app/api/xiaohongshu/profile/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from "next/server"; +import { resolvePaymentApiUrl } from "@/config/domain.config"; + +export const maxDuration = 60; + +/** 开发环境优先使用本地 salonapi,避免 PAYMENT_API_URL 指向远程时 404 */ +function getSalonApiBase(): string { + if (process.env.NODE_ENV === "development") { + const port = process.env.PAYJSAPI_PORT || "8007"; + const host = process.env.SALONAPI_HOST || "127.0.0.1"; + return `http://${host}:${port}`; + } + return resolvePaymentApiUrl(); +} + +/** 代理到 salonapi 获取小红书用户资料 */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const url = String(body?.url || "").trim(); + + if (!url) { + return NextResponse.json({ ok: false, error: "请输入小红书链接" }, { status: 400 }); + } + + const apiBase = getSalonApiBase(); + const res = await fetch(`${apiBase}/api/salon/xiaohongshu/profile`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + signal: AbortSignal.timeout(50000), + }); + + const data = await res.json().catch(() => ({})); + + if (res.ok && data?.ok) { + return NextResponse.json({ + ok: true, + nickname: data.nickname, + gender: data.gender, + avatarUrl: data.avatarUrl ?? undefined, + followers: data.followersText ?? data.followers ?? 0, + followersText: data.followersText ?? undefined, + following: data.followingText ?? data.following ?? 0, + followingText: data.followingText ?? undefined, + likesAndCollects: data.likesAndCollectsText ?? data.likesAndCollects ?? 0, + likesAndCollectsText: data.likesAndCollectsText ?? undefined, + postCount: data.postCount ?? 0, + postCountText: data.postCountText ?? undefined, + redbookId: data.redbookId ?? undefined, + ipLocation: data.ipLocation ?? undefined, + personalLink: data.personalLink ?? undefined, + resolvedUrl: data.resolvedUrl ?? undefined, + }); + } + + const error = data?.detail ?? data?.error ?? "无法解析用户资料,请确认链接有效"; + return NextResponse.json({ ok: false, error }, { status: res.status >= 400 ? res.status : 400 }); + } catch (e) { + const msg = e instanceof Error ? e.message : "获取资料失败"; + return NextResponse.json({ ok: false, error: msg }, { status: 500 }); + } +} diff --git a/app/api/xiaohongshu/validate/route.ts b/app/api/xiaohongshu/validate/route.ts new file mode 100644 index 0000000..4ba2b33 --- /dev/null +++ b/app/api/xiaohongshu/validate/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** 支持:xiaohongshu.com/user/profile/xxx、xhslink.com/user/profile/xxx、xhslink.com/m/xxx 短链 */ +const XH_PROFILE_PATTERN = /^https?:\/\/(www\.)?(xiaohongshu\.com|xhslink\.com)\/user\/profile\/[a-zA-Z0-9_-]+/i; +const XH_SHORT_PATTERN = /^https?:\/\/xhslink\.com\/m\/[a-zA-Z0-9_-]+/i; + +function isValidUrl(url: string): boolean { + return XH_PROFILE_PATTERN.test(url) || XH_SHORT_PATTERN.test(url); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const url = String(body?.url || "").trim(); + + if (!url) { + return NextResponse.json({ valid: false, error: "请输入小红书链接" }, { status: 400 }); + } + + const valid = isValidUrl(url); + if (!valid) { + return NextResponse.json({ valid: false, error: "小红书link异常" }, { status: 200 }); + } + + return NextResponse.json({ valid: true }); + } catch { + return NextResponse.json({ valid: false, error: "校验失败" }, { status: 500 }); + } +} diff --git a/app/components/AddressLink.tsx b/app/components/AddressLink.tsx new file mode 100644 index 0000000..ae75833 --- /dev/null +++ b/app/components/AddressLink.tsx @@ -0,0 +1,37 @@ +"use client"; + +interface AddressLinkProps { + place: string; + city: string; + className?: string; +} + +/** 可点击的地址,点击打开地图(使用 span 避免嵌套在 Link 内时的 a 标签冲突) */ +export default function AddressLink({ place, city, className = "" }: AddressLinkProps) { + const query = encodeURIComponent(`${city}${place}`); + const mapUrl = `https://www.google.com/maps/search/?api=1&query=${query}`; + + return ( + { + e.preventDefault(); + e.stopPropagation(); + window.open(mapUrl, "_blank", "noopener,noreferrer"); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + window.open(mapUrl, "_blank", "noopener,noreferrer"); + } + }} + className={`inline-flex items-center gap-1 text-stone-500 dark:text-stone-400 hover:text-amber-600 dark:hover:text-amber-400 transition-colors cursor-pointer ${className}`} + title="点击打开地图" + > + 📍 + {place} + + ); +} diff --git a/app/components/Footer.tsx b/app/components/Footer.tsx index 48f866d..21c5d6a 100644 --- a/app/components/Footer.tsx +++ b/app/components/Footer.tsx @@ -4,11 +4,12 @@ export default function Footer() { return (
-
- - 历史活动 - -
+ + 🙋 招募志愿者 +

© {new Date().getFullYear()} Salon · 深圳 周末轻社交

diff --git a/app/components/Header.tsx b/app/components/Header.tsx index edfb51c..88648a8 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -11,9 +11,6 @@ export default function Header() { Salon diff --git a/app/components/HeroSection.tsx b/app/components/HeroSection.tsx index eab4a0c..d32edc0 100644 --- a/app/components/HeroSection.tsx +++ b/app/components/HeroSection.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import SessionCoverImage from "./SessionCoverImage"; +import AddressLink from "./AddressLink"; import { SESSIONS, RECENT_JOINERS } from "@/config/home.config"; const MOCK_JOINERS = RECENT_JOINERS.slice(0, 3); @@ -35,7 +36,7 @@ export default function HeroSection() {

周末留两小时
- 去loft民宿,认识几位同频的人 + 认识几位同频的人

深圳 · 6–8 人小圈子 @@ -92,7 +93,7 @@ export default function HeroSection() {

{mainSession.title}

- {mainSession.date} {mainSession.time} · {mainSession.place} + {mainSession.date} {mainSession.time} ·

报名这场 → diff --git a/app/components/SessionCard.tsx b/app/components/SessionCard.tsx index dfc4920..f430730 100644 --- a/app/components/SessionCard.tsx +++ b/app/components/SessionCard.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import SessionCoverImage from "./SessionCoverImage"; +import AddressLink from "./AddressLink"; import { SESSIONS, ACTIVITY_TYPES, SESSION_TAGS, RECENT_JOINERS } from "@/config/home.config"; interface JoinStats { @@ -73,7 +74,7 @@ export default function SessionCard({ {session.title}

- {session.date} {session.time} · {session.place} + {session.date} {session.time} ·

报名这场 → diff --git a/app/globals.css b/app/globals.css index 6f9b5ce..c2a2394 100644 --- a/app/globals.css +++ b/app/globals.css @@ -127,3 +127,12 @@ body { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); } + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-fade-in { + animation: fadeIn 0.4s ease-out forwards; +} diff --git a/app/history/[id]/page.tsx b/app/history/[id]/page.tsx index 0d85fda..f1f5a0f 100644 --- a/app/history/[id]/page.tsx +++ b/app/history/[id]/page.tsx @@ -3,6 +3,7 @@ import { useParams } from "next/navigation"; import Link from "next/link"; import SessionCoverImage from "../../components/SessionCoverImage"; +import AddressLink from "../../components/AddressLink"; import Header from "../../components/Header"; import Footer from "../../components/Footer"; import { PAST_SESSIONS } from "@/config/home.config"; @@ -64,7 +65,9 @@ export default function HistoryDetailPage() {
地点
-
{session.place}
+
+ +
城市
diff --git a/app/host/[id]/page.tsx b/app/host/[id]/page.tsx new file mode 100644 index 0000000..4374997 --- /dev/null +++ b/app/host/[id]/page.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useParams } from "next/navigation"; +import Image from "next/image"; +import Link from "next/link"; +import Header from "../../components/Header"; +import Footer from "../../components/Footer"; +import { HOSTS } from "@/config/home.config"; + +export default function HostDetailPage() { + const params = useParams(); + const id = typeof params.id === "string" ? params.id : ""; + const host = HOSTS.find((h) => h.id === id); + + if (!host) { + return ( +
+
+

主理人不存在

+ + 返回首页 + +
+
+ ); + } + + return ( +
+
+
+ + ← 返回首页 + + +
+
+
+ {host.name} +
+

+ {host.name} +

+

+ {host.bio} +

+
+

+ {host.intro} +

+
+
+
+ + + 看看本周活动 + +
+
+
+ ); +} diff --git a/app/join/page.tsx b/app/join/page.tsx index ca43656..a50c598 100644 --- a/app/join/page.tsx +++ b/app/join/page.tsx @@ -1,139 +1,222 @@ "use client"; -import { useState, useEffect, type FormEvent } from "react"; +import { useState, type FormEvent } from "react"; import Link from "next/link"; -import SessionCoverImage from "../components/SessionCoverImage"; -import { SESSIONS } from "@/config/home.config"; -import CitySelect from "../components/CitySelect"; import ThemeToggle from "../components/ThemeToggle"; +import { DIGITAL_NOMAD_WECHAT_QR } from "@/config/home.config"; -const ageOptions = ["18-24", "25-30", "31-35", "36-40", "40+"]; -const statusOptions = [ - { value: "student", label: "学生" }, - { value: "working", label: "上班" }, - { value: "freelance", label: "自由职业" }, - { value: "startup", label: "创业" }, - { value: "other", label: "其他" }, -]; -const activityTypeOptions = [ - { value: "loft", label: "loft民宿" }, - { value: "side-hustle", label: "轻副业交流" }, - { value: "any", label: "都可以" }, -]; -const firstTimeOptions = ["是,第一次参加", "否,参加过类似活动"]; -const availableTimeOptions = [ - { value: "sat-pm", label: "周六下午" }, - { value: "sun-pm", label: "周日下午" }, - { value: "both", label: "都可以" }, -]; - -const REGIONS: Record = { - 深圳: ["南山区", "福田区", "罗湖区", "宝安区", "龙岗区", "其他"], +/** 各学历对应的典型毕业年龄,用于推算年龄 */ +const EDUCATION_GRAD_AGE: Record = { + "高中及以下": 18, + "大专": 21, + "本科": 22, + "硕士": 25, + "博士": 28, }; +const educationOptions = ["高中及以下", "大专", "本科", "硕士", "博士"]; +const graduationYears = Array.from({ length: 41 }, (_, i) => String(2026 - i)); -interface FormData { - city: string; - session: string; - ageRange: string; - status: string; - activityType: string; - intro: string; - whyJoin: string; - nickname: string; - wechatOrPhone: string; - xiaohongshuUrl: string; - region: string; - firstTime: string; - availableTime: string; - agreeRules: boolean; +function getDisplayCount(text?: string, value?: number | string, suffix = ""): string | null { + if (typeof text === "string" && text.trim()) { + return `${text}${suffix}`; + } + if (typeof value === "number" || typeof value === "string") { + return `${value}${suffix}`; + } + return null; } -function getSessionFromUrl(): string { - if (typeof window === "undefined") return ""; - return new URLSearchParams(window.location.search).get("session") || ""; -} - -function getSessionInfo(sessionId: string) { - return SESSIONS.find((s) => s.id === sessionId); +function getGenderLabel(gender?: string): string { + if (gender === "female") return "\u5973"; + if (gender === "male") return "\u7537"; + return "\u65e0"; } export default function JoinPage() { - const [step, setStep] = useState<1 | 2>(1); - const [form, setForm] = useState({ - city: "", - session: "", - ageRange: "", - status: "", - activityType: "", - intro: "", - whyJoin: "", - nickname: "", - wechatOrPhone: "", - xiaohongshuUrl: "", - region: "", - firstTime: "", - availableTime: "", - agreeRules: false, - }); + const [xiaohongshuUrl, setXiaohongshuUrl] = useState(""); + const [profession, setProfession] = useState(""); + const [wechat, setWechat] = useState(""); + const [phone, setPhone] = useState(""); + const [intro, setIntro] = useState(""); + const [education, setEducation] = useState(""); + const [graduationYear, setGraduationYear] = useState(""); + const [agreeRules, setAgreeRules] = useState(false); + const [urlError, setUrlError] = useState(null); + const [profile, setProfile] = useState<{ + nickname: string; + gender?: string; + followers: number | string; + followersText?: string; + following?: number | string; + followingText?: string; + likesAndCollects?: number | string; + likesAndCollectsText?: string; + postCount: number; + postCountText?: string; + redbookId?: string; + ipLocation?: string; + personalLink?: string; + avatarUrl?: string; + resolvedUrl?: string; + } | null>(null); + const [validating, setValidating] = useState(false); const [submitted, setSubmitted] = useState(false); const [submitting, setSubmitting] = useState(false); + const [qualify, setQualify] = useState(false); const [error, setError] = useState(null); - useEffect(() => { - const sid = getSessionFromUrl(); - if (sid) { - const s = SESSIONS.find((x) => x.id === sid); - setForm((prev) => ({ - ...prev, - session: sid, - city: s?.city || prev.city, - })); + const handleUrlBlur = async () => { + const url = xiaohongshuUrl.trim(); + if (!url) { + setUrlError(null); + setProfile(null); + return; } - }, []); - const set = (key: keyof FormData, value: string | boolean) => - setForm((prev) => ({ ...prev, [key]: value })); + setValidating(true); + setUrlError(null); + setProfile(null); - const canProceedStep1 = form.city && form.session && form.ageRange && form.status && form.activityType && form.intro.trim() && form.whyJoin.trim(); + try { + const validateRes = await fetch("/api/xiaohongshu/validate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + }); + const validateData = await validateRes.json(); + + if (!validateData.valid) { + setUrlError(validateData.error || "小红书link异常"); + return; + } + + const profileRes = await fetch("/api/xiaohongshu/profile", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url }), + }); + const profileData = await profileRes.json(); + + if (profileData.ok) { + setProfile({ + nickname: profileData.nickname, + gender: profileData.gender, + followers: profileData.followers ?? 0, + followersText: profileData.followersText, + following: profileData.following, + followingText: profileData.followingText, + likesAndCollects: profileData.likesAndCollects, + likesAndCollectsText: profileData.likesAndCollectsText, + postCount: profileData.postCount ?? 0, + postCountText: profileData.postCountText, + redbookId: profileData.redbookId, + ipLocation: profileData.ipLocation, + personalLink: profileData.personalLink, + avatarUrl: profileData.avatarUrl, + resolvedUrl: profileData.resolvedUrl, + }); + } else { + setUrlError(profileData.error || "获取资料失败"); + } + } catch { + setUrlError("网络异常,请重试"); + } finally { + setValidating(false); + } + }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); + + if (!xiaohongshuUrl.trim()) { + setError("请填写小红书主页链接"); + return; + } + if (urlError) { + setError("请先修正小红书链接"); + return; + } + if (!education || !graduationYear) { + setError("请选择学历和毕业时间"); + return; + } + if (!profession.trim()) { + setError("请填写职业"); + return; + } + if (!wechat.trim() && !phone.trim()) { + setError("请填写微信号或手机号"); + return; + } + if (!intro.trim()) { + setError("请填写自我介绍"); + return; + } + if (!agreeRules) { + setError("请同意社区规则"); + return; + } + setSubmitting(true); try { + const gradAge = EDUCATION_GRAD_AGE[education] ?? 22; + const estimatedAge = new Date().getFullYear() - parseInt(graduationYear, 10) + gradAge; + const ageOk = estimatedAge < 28; + const isFemale = profile?.gender === "female"; + const genderLabel = getGenderLabel(profile?.gender); + const postCountOk = (profile?.postCount ?? 0) > 5; + const postCountLabel = getDisplayCount(profile?.postCountText, profile?.postCount) ?? "0"; + const submitXiaohongshuUrl = profile?.resolvedUrl || xiaohongshuUrl.trim(); + const reasonParts = [ - form.intro, - form.whyJoin, - form.status && `状态: ${statusOptions.find((o) => o.value === form.status)?.label || form.status}`, - form.activityType && `活动偏好: ${activityTypeOptions.find((o) => o.value === form.activityType)?.label || form.activityType}`, - form.region && `常活动区域: ${form.region}`, - form.availableTime && `可参加时间: ${availableTimeOptions.find((o) => o.value === form.availableTime)?.label || form.availableTime}`, + intro, + `学历:${education}`, + `毕业:${graduationYear}`, + profile?.gender && `性别:${profile.gender}`, + `发帖:${profile?.postCount ?? 0}`, + ].filter(Boolean); + + const submitReasonParts = [ + reasonParts[0], + `\u5b66\u5386:${education}`, + `\u6bd5\u4e1a:${graduationYear}`, + `\u6027\u522b:${genderLabel}`, + `\u53d1\u5e16:${postCountLabel}`, ].filter(Boolean); const res = await fetch("/api/join", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - city: form.city, - session: form.session, - nickname: form.nickname, - age_range: form.ageRange, - wechat_or_phone: form.wechatOrPhone, - xiaohongshu_url: form.xiaohongshuUrl.trim(), - intro: reasonParts.join(" | "), - first_time: form.firstTime, - agree_rules: form.agreeRules, - status: form.status, - activity_type: form.activityType, - why_join: form.whyJoin, - region: form.region, - available_time: form.availableTime, + nickname: profile?.nickname || "匿名", + wechat: wechat.trim(), + phone: phone.trim(), + profession: profession.trim(), + xiaohongshu_url: submitXiaohongshuUrl, + xiaohongshu_avatar_url: profile?.avatarUrl || "", + gender: genderLabel, + intro: submitReasonParts.join(" | "), + city: "深圳", + session: "digital-nomad", + education, + graduation_year: graduationYear, + agree_rules: true, + first_time: "", + status: "", + activity_type: "", + why_join: "", + region: "", + available_time: "", }), }); const data = await res.json(); + if (!res.ok || !data?.ok) { throw new Error(data?.error || "提交失败,请稍后重试"); } + + setQualify(isFemale && ageOk && postCountOk); setSubmitted(true); } catch (err) { setError(err instanceof Error ? err.message : "网络错误,请重试"); @@ -142,20 +225,53 @@ export default function JoinPage() { } }; - const selectedSession = getSessionInfo(form.session); - const regions = form.city ? (REGIONS[form.city] || []) : []; + const followingDisplay = getDisplayCount(profile?.followingText, profile?.following); + const followersDisplay = getDisplayCount(profile?.followersText, profile?.followers); + const likesDisplay = getDisplayCount(profile?.likesAndCollectsText, profile?.likesAndCollects); + const safePostCountDisplay = getDisplayCount(profile?.postCountText, profile?.postCount, " \u7bc7"); + const postCountDisplay = getDisplayCount(profile?.postCountText, profile?.postCount, " 篇"); + + const profileGenderLabel = getGenderLabel(profile?.gender); if (submitted) { return (
-
- ✅ -
-

报名成功

-

- 我们会尽快通过您留下的联系方式与您确认。不是所有报名都会自动确认,我们会根据场次与匹配度联系合适报名者。 -

+ {qualify ? ( + <> +
+ ✅ +
+

+ 恭喜,符合条件 +

+

+ 扫码添加微信,加入活动群 +

+
+ 微信二维码 +
+

+ 添加时请备注:活动报名 +

+ + ) : ( + <> +
+ ⏳ +
+

+ 资料审核中 +

+

+ 我们会在 1-3 个工作日内审核您的资料,通过后将通过小红书私信联系您。 +

+ + )} - {/* 顶部栏 */}
@@ -179,301 +294,204 @@ export default function JoinPage() {
-
- {/* Trust bar */} -
- 📍 公开场地 - 👥 6–8 人小组 - 🆓 不收费 - 🤝 尊重边界 -
- -
-
+
+
+
-
-
- 📍 深圳 -
+
🌍
-
-

- 报名参加 -

-

- 填个表,我们会根据场次联系你~ -

-

- 不是所有报名都会确认,看场次匹配 -

- - {/* 进度提示 */} -
-
- - {step === 1 ? "第一步" : "第二步"} - -
-
-
-
- - {selectedSession && ( -
- {selectedSession.coverImage && ( -
- -
- )} -
-

已选场次

-

{selectedSession.title}

-

{selectedSession.date} {selectedSession.time} · {selectedSession.place}

-
-
- )} - -
- {step === 1 ? ( -
-
-

基本信息

- - set("city", v)} required /> - - - - - - - - - - - - - +
+ +
+ + { + setXiaohongshuUrl(e.target.value); + setUrlError(null); + }} + onBlur={handleUrlBlur} + placeholder="https://www.xiaohongshu.com/user/profile/xxx" + required + className="form-input" + /> + {validating && ( +

校验中…

+ )} + {urlError && !validating && ( +

{urlError}

+ )} + {profile && !urlError && ( +

+ {"\u2713 \u5df2\u83b7\u53d6\uff1a"}{profile.nickname} + {profile.redbookId && ` \u00b7 @${profile.redbookId}`} + {profile.ipLocation && ` \u00b7 ${profile.ipLocation}`} + {profile.personalLink && ` \u00b7 ${profile.personalLink}`} + {followingDisplay && ` \u00b7 \u5173\u6ce8 ${followingDisplay}`} + {followersDisplay && ` \u00b7 \u7c89\u4e1d ${followersDisplay}`} + {likesDisplay && ` \u00b7 \u83b7\u8d5e\u4e0e\u6536\u85cf ${likesDisplay}`} + {safePostCountDisplay && ` \u00b7 \u53d1\u5e16 ${safePostCountDisplay}`} + {` \u00b7 ${profileGenderLabel}`} +

+ )} + {((profile) => false ? ( +

+ ✓ 已获取:{profile.nickname} + {profile.redbookId && ` · @${profile.redbookId}`} + {profile.ipLocation && ` · ${profile.ipLocation}`} + {profile.personalLink && ` · ${profile.personalLink}`} + {profile.following != null && ` · 关注 ${profile.following}`} + {profile.followers != null && ` · 粉丝 ${profile.followers}`} + {profile.likesAndCollects != null && ` · 获赞与收藏 ${profile.likesAndCollects}`} + {profile.postCount != null && ` · 发帖 ${profile.postCount} 篇`} + {profile.gender && ` · ${profile.gender === "female" ? "女" : "男"}`} +

+ ) : null)(profile as NonNullable)}
-
-

简单介绍

- +
+ + setProfession(e.target.value)} + required + className="form-input" + /> +
+ +
+
+ set("intro", e.target.value)} - required + placeholder="用于拉群" + value={wechat} + onChange={(e) => setWechat(e.target.value)} className="form-input" /> - - +
+
+ set("whyJoin", e.target.value)} - required + type="tel" + maxLength={11} + pattern="^\d{11}$" + placeholder="请输入11位手机号" + value={phone} + onChange={(e) => setPhone(e.target.value.replace(/\D/g, "").slice(0, 11))} className="form-input" /> - +
+
+

+ 微信号和手机号至少填写一项 +

+ +
+ +