's'
This commit is contained in:
@@ -37,7 +37,7 @@ export async function POST(request: NextRequest) {
|
||||
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();
|
||||
let intro = String(formData.intro || formData.reason || "").trim();
|
||||
const firstTime = String(formData.first_time || "").trim();
|
||||
const agreeRules = !!formData.agree_rules;
|
||||
const status = String(formData.status || "").trim();
|
||||
@@ -61,6 +61,24 @@ export async function POST(request: NextRequest) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!xiaohongshuUrl) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "请填写小红书主页链接或笔记链接" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!intro) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "请填写自我介绍" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (intro.length < 20) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, error: "自我介绍不少于20字" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = getUserIdFromCookie(request) || generateAnonId();
|
||||
|
||||
@@ -68,6 +86,7 @@ export async function POST(request: NextRequest) {
|
||||
/** occupation: 职业/城市,reason: 仅自我介绍 */
|
||||
const occupationValue = profession || city;
|
||||
|
||||
const userInfo = String(formData.userInfo || "").trim();
|
||||
const record: Record<string, unknown> = {
|
||||
user_id: userId,
|
||||
nickname,
|
||||
@@ -87,6 +106,7 @@ export async function POST(request: NextRequest) {
|
||||
first_time: firstTime,
|
||||
is_volunteer: isVolunteer,
|
||||
qualify,
|
||||
userInfo: userInfo || "",
|
||||
};
|
||||
if (phone.trim()) {
|
||||
record.phone = phone.trim();
|
||||
@@ -177,6 +197,7 @@ async function tryPocketBaseDirect(
|
||||
is_approved: qualify,
|
||||
age_range: record.age_range ?? "",
|
||||
first_time: record.first_time ?? "",
|
||||
userInfo: record.userInfo ?? "",
|
||||
};
|
||||
if (record.phone != null && String(record.phone).trim()) {
|
||||
body.phone = record.phone;
|
||||
|
||||
@@ -13,16 +13,15 @@ function getSalonApiBase(): string {
|
||||
return resolvePaymentApiUrl();
|
||||
}
|
||||
|
||||
/** 代理到 salonapi 获取小红书用户资料 */
|
||||
/** 代理到 salonapi 获取小红书用户资料,支持主页链接、笔记链接 xhslink.com/o/ */
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const url = String(body?.url || "").trim();
|
||||
const url = String(body?.url || body?.id || "").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",
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
/** 支持:xiaohongshu.com/user/profile/xxx、xhslink.com/user/profile/xxx、xhslink.com/m/xxx 短链 */
|
||||
/** 支持:主页 xiaohongshu.com/user/profile/xxx、xhslink.com/m/xxx;笔记 xhslink.com/o/xxx、xiaohongshu.com/explore/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;
|
||||
const XH_NOTE_PATTERN = /^https?:\/\/(xhslink\.com\/o\/|(www\.)?xiaohongshu\.com\/explore\/)[a-zA-Z0-9_-]+/i;
|
||||
|
||||
function isValidUrl(url: string): boolean {
|
||||
return XH_PROFILE_PATTERN.test(url) || XH_SHORT_PATTERN.test(url);
|
||||
const s = url.trim();
|
||||
if (!s) return false;
|
||||
if (XH_PROFILE_PATTERN.test(s) || XH_SHORT_PATTERN.test(s) || XH_NOTE_PATTERN.test(s)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const url = String(body?.url || "").trim();
|
||||
const url = String(body?.url || body?.id || "").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 });
|
||||
if (!isValidUrl(url)) {
|
||||
return NextResponse.json({ valid: false, error: "请输入有效的主页链接或笔记链接(xhslink.com/m/xxx 或 xhslink.com/o/xxx)" }, { status: 200 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ valid: true });
|
||||
return NextResponse.json({ valid: true, url });
|
||||
} catch {
|
||||
return NextResponse.json({ valid: false, error: "校验失败" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const ageRangeOptions = ["18-22", "23-27", "28-32", "33-37", "38+"];
|
||||
/** 微信号仅允许英文字母、数字、下划线、连字符 */
|
||||
const WECHAT_REGEX = /^[a-zA-Z0-9_-]*$/;
|
||||
|
||||
/** 从粘贴文本中提取小红书 URL,如 "我在小红书收获了536次赞与收藏>> https://xhslink.com/m/9H50QYiVOVx" */
|
||||
/** 从粘贴文本中提取小红书 URL(主页或笔记链接),清除前后空格 */
|
||||
function extractXiaohongshuUrl(text: string): string {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return "";
|
||||
@@ -133,6 +133,7 @@ export default function JoinPage() {
|
||||
const [checkingWechat, setCheckingWechat] = useState(false);
|
||||
|
||||
const [xiaohongshuUrl, setXiaohongshuUrl] = useState("");
|
||||
const [intro, setIntro] = useState("");
|
||||
const [profession, setProfession] = useState("");
|
||||
const [gender, setGender] = useState("");
|
||||
const [ageRange, setAgeRange] = useState("");
|
||||
@@ -146,6 +147,14 @@ export default function JoinPage() {
|
||||
ipLocation?: string;
|
||||
avatarUrl?: string;
|
||||
resolvedUrl?: string;
|
||||
followers?: number;
|
||||
followersText?: string;
|
||||
following?: number;
|
||||
followingText?: string;
|
||||
likesAndCollects?: number;
|
||||
likesAndCollectsText?: string;
|
||||
redbookId?: string;
|
||||
personalLink?: string;
|
||||
} | null>(null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
@@ -210,7 +219,7 @@ export default function JoinPage() {
|
||||
|
||||
const handleUrlBlur = async () => {
|
||||
const extracted = extractXiaohongshuUrl(xiaohongshuUrl);
|
||||
if (extracted !== xiaohongshuUrl) {
|
||||
if (extracted !== xiaohongshuUrl.trim()) {
|
||||
setXiaohongshuUrl(extracted);
|
||||
}
|
||||
const url = extracted;
|
||||
@@ -230,7 +239,7 @@ export default function JoinPage() {
|
||||
});
|
||||
const validateData = await validateRes.json();
|
||||
if (!validateData.valid) {
|
||||
setUrlError(validateData.error || "小红书link异常");
|
||||
setUrlError(validateData.error || "小红书链接异常");
|
||||
return;
|
||||
}
|
||||
const profileRes = await fetch("/api/xiaohongshu/profile", {
|
||||
@@ -248,6 +257,14 @@ export default function JoinPage() {
|
||||
ipLocation: profileData.ipLocation,
|
||||
avatarUrl: profileData.avatarUrl,
|
||||
resolvedUrl: profileData.resolvedUrl,
|
||||
followers: profileData.followers,
|
||||
followersText: profileData.followersText,
|
||||
following: profileData.following,
|
||||
followingText: profileData.followingText,
|
||||
likesAndCollects: profileData.likesAndCollects,
|
||||
likesAndCollectsText: profileData.likesAndCollectsText,
|
||||
redbookId: profileData.redbookId,
|
||||
personalLink: profileData.personalLink,
|
||||
});
|
||||
} else {
|
||||
setUrlError(profileData.error || "获取资料失败");
|
||||
@@ -270,10 +287,22 @@ export default function JoinPage() {
|
||||
setError("该微信号已报名,请勿重复提交");
|
||||
return;
|
||||
}
|
||||
if (!xiaohongshuUrl.trim()) {
|
||||
setError("请填写小红书主页链接或笔记链接");
|
||||
return;
|
||||
}
|
||||
if (xiaohongshuUrl.trim() && urlError) {
|
||||
setError("请先修正小红书链接");
|
||||
return;
|
||||
}
|
||||
if (!intro.trim()) {
|
||||
setError("请填写自我介绍");
|
||||
return;
|
||||
}
|
||||
if (intro.trim().length < 20) {
|
||||
setError("自我介绍不少于20字");
|
||||
return;
|
||||
}
|
||||
if (!profession.trim()) {
|
||||
setError("请填写职业");
|
||||
return;
|
||||
@@ -310,7 +339,7 @@ export default function JoinPage() {
|
||||
const postCountOk = (profile?.postCount ?? 0) > 10;
|
||||
const qualify = formGenderFemale && postCountOk;
|
||||
const genderLabel = gender === "女" ? "女" : gender === "男" ? "男" : "无";
|
||||
const submitXiaohongshuUrl = profile?.resolvedUrl || xiaohongshuUrl.trim() || "";
|
||||
const submitXiaohongshuUrl = profile?.resolvedUrl || extractXiaohongshuUrl(xiaohongshuUrl) || "";
|
||||
|
||||
const res = await fetch("/api/join", {
|
||||
method: "POST",
|
||||
@@ -323,7 +352,27 @@ export default function JoinPage() {
|
||||
xiaohongshu_url: submitXiaohongshuUrl,
|
||||
xiaohongshu_avatar_url: profile?.avatarUrl || "",
|
||||
gender: genderLabel,
|
||||
intro: "",
|
||||
intro: intro.trim(),
|
||||
reason: intro.trim(),
|
||||
userInfo: profile
|
||||
? JSON.stringify({
|
||||
昵称: profile.nickname,
|
||||
性别: profile.gender,
|
||||
笔记数: profile.postCount,
|
||||
笔记数文案: profile.postCountText,
|
||||
IP属地: profile.ipLocation,
|
||||
头像链接: profile.avatarUrl,
|
||||
主页链接: profile.resolvedUrl,
|
||||
粉丝数: profile.followers,
|
||||
粉丝数文案: profile.followersText,
|
||||
关注数: profile.following,
|
||||
关注数文案: profile.followingText,
|
||||
获赞与收藏: profile.likesAndCollects,
|
||||
获赞与收藏文案: profile.likesAndCollectsText,
|
||||
小红书号: profile.redbookId,
|
||||
个人链接: profile.personalLink,
|
||||
})
|
||||
: "",
|
||||
age_range: ageRange,
|
||||
city: "深圳",
|
||||
session: fromVolunteer ? "volunteer" : "digital-nomad",
|
||||
@@ -377,6 +426,7 @@ export default function JoinPage() {
|
||||
setUserRecord(null);
|
||||
setWechat("");
|
||||
setXiaohongshuUrl("");
|
||||
setIntro("");
|
||||
setProfession("");
|
||||
setGender("");
|
||||
setAgeRange("");
|
||||
@@ -813,19 +863,79 @@ export default function JoinPage() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">📱 小红书主页链接</label>
|
||||
<p className="text-xs text-stone-500 dark:text-stone-400 mb-1.5">💡 建议填写,可加快审核</p>
|
||||
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">📱 小红书主页链接 或 笔记链接 <span className="text-red-500">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
value={xiaohongshuUrl}
|
||||
onChange={(e) => { setXiaohongshuUrl(e.target.value); setUrlError(null); }}
|
||||
onBlur={handleUrlBlur}
|
||||
placeholder="https://www.xiaohongshu.com/user/profile/xxx 或粘贴分享文案"
|
||||
required
|
||||
className="form-input"
|
||||
/>
|
||||
{validating && <p className="mt-1.5 text-xs text-amber-600 dark:text-amber-400">🔄 校验中…</p>}
|
||||
{urlError && !validating && <p className="mt-1.5 text-xs text-red-600 dark:text-red-400">⚠️ {urlError}</p>}
|
||||
{profile && !urlError && <p className="mt-1.5 text-xs text-emerald-600 dark:text-emerald-400">已验证</p>}
|
||||
{profile && !urlError && (
|
||||
<>
|
||||
<p className="mt-1.5 text-xs text-emerald-600 dark:text-emerald-400">已验证</p>
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
<div className="mt-3 rounded-xl border border-stone-200 dark:border-stone-600 bg-stone-50 dark:bg-stone-800/50 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{profile.avatarUrl ? (
|
||||
<img
|
||||
src={profile.avatarUrl}
|
||||
alt=""
|
||||
className="h-14 w-14 rounded-full object-cover border border-stone-200 dark:border-stone-600"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/40 text-2xl">
|
||||
👤
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-stone-800 dark:text-stone-100">{profile.nickname}</p>
|
||||
{profile.gender && <p className="text-xs text-stone-500 dark:text-stone-400">性别 {profile.gender}</p>}
|
||||
{profile.redbookId && <p className="text-xs text-stone-500 dark:text-stone-400">小红书号 {profile.redbookId}</p>}
|
||||
{profile.ipLocation && <p className="text-xs text-stone-500 dark:text-stone-400">IP属地 {profile.ipLocation}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-stone-600 dark:text-stone-400">
|
||||
<span>关注 {profile.followingText ?? profile.following ?? "—"}</span>
|
||||
<span>粉丝 {profile.followersText ?? profile.followers ?? "—"}</span>
|
||||
<span>获赞与收藏 {profile.likesAndCollectsText ?? profile.likesAndCollects ?? "—"}</span>
|
||||
<span>笔记 {profile.postCountText ?? profile.postCount ?? "—"}</span>
|
||||
</div>
|
||||
{profile.personalLink && (
|
||||
<p className="mt-2 text-xs text-stone-500 dark:text-stone-400 truncate">个人链接 {profile.personalLink}</p>
|
||||
)}
|
||||
{profile.resolvedUrl && (
|
||||
<p className="mt-1 text-xs text-stone-400 dark:text-stone-500 truncate">Profile {profile.resolvedUrl}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">✍️ 自我介绍 <span className="text-red-500">*</span></label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
minLength={20}
|
||||
placeholder="不少于20字"
|
||||
value={intro}
|
||||
onChange={(e) => setIntro(e.target.value)}
|
||||
required
|
||||
className="form-input resize-y min-h-[80px] pr-14"
|
||||
/>
|
||||
<span
|
||||
className={`absolute bottom-2 right-3 text-xs ${
|
||||
intro.length < 20 ? "text-amber-600 dark:text-amber-400" : "text-stone-400 dark:text-stone-500"
|
||||
}`}
|
||||
>
|
||||
{intro.length}/500
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-700 dark:text-stone-300 mb-2">💼 职业 <span className="text-red-500">*</span></label>
|
||||
|
||||
Reference in New Issue
Block a user