Polish map empty states, login hints, PWA install, live mobile.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9f2eee7e83
commit
dac99e8ef4
@ -8,6 +8,16 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LOGS = [
|
const LOGS = [
|
||||||
|
{
|
||||||
|
date: "2026-08-30",
|
||||||
|
tag: "三环可用深化",
|
||||||
|
items: [
|
||||||
|
"产品信息架构:Explore / Connect / Grow 渐进披露,导航与空态对齐三条环",
|
||||||
|
"Connect:RSVP 需登录、通知铃铛、匹配配额、私信与 VIP 支付加固",
|
||||||
|
"Grow:学院 VIP 课时客户端解锁、赏金/投稿表单校验、电子书购买闭环",
|
||||||
|
"Explore:目的地对比入口、AI 快捷提问、计划空态、周报订阅诚实提示",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
date: "2026-08-29",
|
date: "2026-08-29",
|
||||||
tag: "电子书融合",
|
tag: "电子书融合",
|
||||||
|
|||||||
@ -14,16 +14,43 @@ export default async function DigitalDayPage({ params }: { params: Promise<{ id:
|
|||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const day = await api.getDigitalDay(id).catch(() => null);
|
const day = await api.getDigitalDay(id).catch(() => null);
|
||||||
if (!day) notFound();
|
if (!day) notFound();
|
||||||
|
const n = Number(id);
|
||||||
|
const prev = Number.isFinite(n) && n > 1 ? String(n - 1) : null;
|
||||||
|
const next = Number.isFinite(n) && n < 3 ? String(n + 1) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteShell showFooter>
|
<SiteShell showFooter>
|
||||||
<div className="legal-page">
|
<div className="digital-page">
|
||||||
<nav className="detail-nav"><Link href="/digital">← 游民学院</Link></nav>
|
<div className="container" style={{ maxWidth: 720 }}>
|
||||||
<article className="legal-content reveal">
|
<nav className="detail-nav">
|
||||||
|
<Link href="/digital">← 游民学院</Link>
|
||||||
|
</nav>
|
||||||
|
<article className="digital-lesson reveal">
|
||||||
<span className="section-tag">📅 DAY {id}</span>
|
<span className="section-tag">📅 DAY {id}</span>
|
||||||
<h1>{day.title}</h1>
|
<h1>{day.title}</h1>
|
||||||
<p>{day.body}</p>
|
<div className="digital-lesson-body">
|
||||||
|
{day.body.split("\n").filter(Boolean).map((p, i) => (
|
||||||
|
<p key={`${i}-${p.slice(0, 12)}`}>{p}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<footer className="digital-lesson-nav">
|
||||||
|
{prev && (
|
||||||
|
<Link href={`/digital/day/${prev}`} className="btn">
|
||||||
|
← 上一篇
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{next && (
|
||||||
|
<Link href={`/digital/day/${next}`} className="btn btn-primary">
|
||||||
|
下一篇 →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link href="/digital/course" className="btn btn-ghost">
|
||||||
|
课程目录
|
||||||
|
</Link>
|
||||||
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</SiteShell>
|
</SiteShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5688,6 +5688,12 @@ img { max-width: 100%; display: block; }
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-pwd-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.auth-submit {
|
.auth-submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -6850,6 +6856,32 @@ img { max-width: 100%; display: block; }
|
|||||||
.phrase-zh { font-size: 0.85rem; color: var(--text-secondary); }
|
.phrase-zh { font-size: 0.85rem; color: var(--text-secondary); }
|
||||||
|
|
||||||
/* ===== Feedback Widget ===== */
|
/* ===== Feedback Widget ===== */
|
||||||
|
.pwa-install-hint {
|
||||||
|
position: fixed;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 24px;
|
||||||
|
z-index: 8700;
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: var(--border-glass);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-install-hint p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwa-install-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.feedback-fab {
|
.feedback-fab {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 100px;
|
bottom: 100px;
|
||||||
@ -10919,8 +10951,25 @@ img { max-width: 100%; display: block; }
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.join-grid { grid-template-columns: 1fr; }
|
.join-grid { grid-template-columns: 1fr; }
|
||||||
|
.live-toolbar {
|
||||||
|
padding: 10px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.live-toolbar strong {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.live-layout-btns {
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.live-stage {
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
.live-stage.layout-split { grid-template-columns: 1fr; }
|
.live-stage.layout-split { grid-template-columns: 1fr; }
|
||||||
.live-chat { border-left: none; border-top: var(--border-glass); min-height: 320px; }
|
.live-iframe { min-height: 280px; }
|
||||||
|
.live-chat { border-left: none; border-top: var(--border-glass); min-height: 280px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- Feature rings / progressive IA -- */
|
/* -- Feature rings / progressive IA -- */
|
||||||
|
|||||||
@ -34,10 +34,20 @@ function LoginForm() {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError("");
|
setError("");
|
||||||
|
if (mode === "register") {
|
||||||
|
if (!name.trim() || name.trim().length < 2) {
|
||||||
|
setError("昵称至少 2 个字符");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
setError("密码至少 6 位");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const ok = mode === "login"
|
const ok = mode === "login"
|
||||||
? await login(email, password)
|
? await login(email, password)
|
||||||
: await register(email, password, name);
|
: await register(email, password, name.trim());
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
goAfterAuth(
|
goAfterAuth(
|
||||||
@ -116,6 +126,17 @@ function LoginForm() {
|
|||||||
{showPwd ? "🙈" : "👁️"}
|
{showPwd ? "🙈" : "👁️"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{mode === "register" && (
|
||||||
|
<p className="auth-pwd-hint">
|
||||||
|
{password.length === 0
|
||||||
|
? "至少 6 位,建议包含字母与数字"
|
||||||
|
: password.length < 6
|
||||||
|
? `还差 ${6 - password.length} 位`
|
||||||
|
: password.length < 10
|
||||||
|
? "可用 · 再长一点更安全"
|
||||||
|
: "强度不错"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="auth-error">{error}</p>}
|
{error && <p className="auth-error">{error}</p>}
|
||||||
<button type="submit" className="btn btn-primary auth-submit" disabled={loading}>
|
<button type="submit" className="btn btn-primary auth-submit" disabled={loading}>
|
||||||
|
|||||||
@ -25,10 +25,14 @@ export default function CookieConsent() {
|
|||||||
<div className="cookie-consent-inner">
|
<div className="cookie-consent-inner">
|
||||||
<span className="cookie-emoji">🍪</span>
|
<span className="cookie-emoji">🍪</span>
|
||||||
<p>
|
<p>
|
||||||
我们使用 localStorage 保存主题偏好和行程数据,不会追踪你的浏览行为。
|
我们使用本地存储保存主题、行程与登录会话,不做广告追踪。
|
||||||
<Link href="/privacy">隐私政策</Link>
|
<Link href="/privacy">隐私政策</Link>
|
||||||
|
{" · "}
|
||||||
|
<Link href="/cookies">Cookie 说明</Link>
|
||||||
</p>
|
</p>
|
||||||
<button className="btn btn-primary btn-sm" onClick={accept}>知道了 ✓</button>
|
<button type="button" className="btn btn-primary btn-sm" onClick={accept}>
|
||||||
|
知道了 ✓
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -113,7 +113,30 @@ export default function Destinations({ destinations, onOpenMatcher }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
|
<div className="notif-empty reveal">
|
||||||
<p className="dest-empty">{t.home.destEmpty}</p>
|
<p className="dest-empty">{t.home.destEmpty}</p>
|
||||||
|
<div className="dating-empty-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
setFilter("all");
|
||||||
|
setSearch("");
|
||||||
|
setExpanded(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清除筛选
|
||||||
|
</button>
|
||||||
|
{onOpenMatcher && (
|
||||||
|
<button type="button" className="btn btn-primary btn-sm" onClick={onOpenMatcher}>
|
||||||
|
🎯 {t.home.destMatcher}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<Link href="/next-stop" className="btn btn-sm">
|
||||||
|
智能下一站
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canExpand && (
|
{canExpand && (
|
||||||
|
|||||||
@ -43,6 +43,11 @@ export default function DigitalHomeClient({ course }: { course: DigitalCourse })
|
|||||||
<h3>{t.digital.vip}</h3>
|
<h3>{t.digital.vip}</h3>
|
||||||
<p>{t.digital.vipDesc}</p>
|
<p>{t.digital.vipDesc}</p>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href="/digital/day/1" className="digital-card">
|
||||||
|
<span className="digital-card-emoji">📅</span>
|
||||||
|
<h3>每日指南</h3>
|
||||||
|
<p>从税务居民到落地验网,三天起步清单</p>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RingNext
|
<RingNext
|
||||||
|
|||||||
@ -33,6 +33,7 @@ const NomadTips = lazy(() => import("./NomadTips"));
|
|||||||
const FeedbackWidget = lazy(() => import("./FeedbackWidget"));
|
const FeedbackWidget = lazy(() => import("./FeedbackWidget"));
|
||||||
const OnboardingTour = lazy(() => import("./OnboardingTour"));
|
const OnboardingTour = lazy(() => import("./OnboardingTour"));
|
||||||
const KeyboardShortcuts = lazy(() => import("./KeyboardShortcuts"));
|
const KeyboardShortcuts = lazy(() => import("./KeyboardShortcuts"));
|
||||||
|
const PwaInstallHint = lazy(() => import("./PwaInstallHint"));
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
stats: Stats;
|
stats: Stats;
|
||||||
@ -139,6 +140,7 @@ export default function HomeClient(props: Props) {
|
|||||||
<>
|
<>
|
||||||
<KeyboardShortcuts onOpenMatcher={openMatcher} />
|
<KeyboardShortcuts onOpenMatcher={openMatcher} />
|
||||||
<CookieConsent />
|
<CookieConsent />
|
||||||
|
<PwaInstallHint />
|
||||||
<NomadTips />
|
<NomadTips />
|
||||||
<FeedbackWidget />
|
<FeedbackWidget />
|
||||||
<OnboardingTour />
|
<OnboardingTour />
|
||||||
|
|||||||
@ -5,29 +5,53 @@ import Link from "next/link";
|
|||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
import type { MemberMapItem } from "@/lib/types";
|
import type { MemberMapItem } from "@/lib/types";
|
||||||
|
import RingNext from "@/components/RingNext";
|
||||||
|
import { useRingSteps } from "@/lib/rings";
|
||||||
|
|
||||||
export default function MemberMapClient() {
|
export default function MemberMapClient() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const rings = useRingSteps();
|
||||||
const [members, setMembers] = useState<MemberMapItem[]>([]);
|
const [members, setMembers] = useState<MemberMapItem[]>([]);
|
||||||
const [weather, setWeather] = useState<{ name: string; temperature: number | null }[]>([]);
|
const [weather, setWeather] = useState<{ name: string; temperature: number | null }[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.getMemberMap().then((r) => setMembers(r.members || [])).catch(() => {});
|
Promise.all([
|
||||||
api.getWeatherCities().then(setWeather).catch(() => {});
|
api.getMemberMap().then((r) => setMembers(r.members || [])).catch(() => setMembers([])),
|
||||||
|
api.getWeatherCities().then(setWeather).catch(() => setWeather([])),
|
||||||
|
]).finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="community-page">
|
<div className="community-page">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<nav className="detail-nav"><Link href="/">{t.common.backHome}</Link></nav>
|
<nav className="detail-nav">
|
||||||
|
<Link href="/">{t.common.backHome}</Link>
|
||||||
|
<span> · </span>
|
||||||
|
<Link href="/report">{t.report.title}</Link>
|
||||||
|
</nav>
|
||||||
<div className="section-header reveal">
|
<div className="section-header reveal">
|
||||||
<span className="section-tag">{t.mapPage.tag}</span>
|
<span className="section-tag">{t.mapPage.tag}</span>
|
||||||
<h1>{t.mapPage.title}</h1>
|
<h1>{t.mapPage.title}</h1>
|
||||||
<p>{t.mapPage.subtitle}</p>
|
<p>{t.mapPage.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{loading && <p className="dest-empty">加载地图…</p>}
|
||||||
|
{!loading && members.length === 0 && (
|
||||||
|
<div className="notif-empty reveal">
|
||||||
|
<p className="dest-empty">暂无公开会员分布</p>
|
||||||
|
<div className="dating-empty-actions">
|
||||||
|
<Link href="/dating" className="btn btn-primary btn-sm">
|
||||||
|
去匹配
|
||||||
|
</Link>
|
||||||
|
<Link href="/meetups" className="btn btn-sm">
|
||||||
|
去活动
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="member-map-grid reveal">
|
<div className="member-map-grid reveal">
|
||||||
{members.map((m) => (
|
{members.map((m) => (
|
||||||
<div key={m.name} className="member-map-item" style={{ gridColumn: `span 1` }}>
|
<div key={m.name + m.city} className="member-map-item">
|
||||||
<span className="chat-avatar">{m.emoji}</span>
|
<span className="chat-avatar">{m.emoji}</span>
|
||||||
<div>
|
<div>
|
||||||
<strong>{m.name}</strong>
|
<strong>{m.name}</strong>
|
||||||
@ -36,9 +60,11 @@ export default function MemberMapClient() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{weather.length > 0 && (
|
|
||||||
<section className="weather-strip reveal">
|
<section className="weather-strip reveal">
|
||||||
<h3>{t.mapPage.weather}</h3>
|
<h3>{t.mapPage.weather}</h3>
|
||||||
|
{!loading && weather.length === 0 ? (
|
||||||
|
<p className="dest-empty">天气暂不可用</p>
|
||||||
|
) : (
|
||||||
<div className="weather-cards">
|
<div className="weather-cards">
|
||||||
{weather.map((w) => (
|
{weather.map((w) => (
|
||||||
<div key={w.name} className="weather-card">
|
<div key={w.name} className="weather-card">
|
||||||
@ -47,9 +73,10 @@ export default function MemberMapClient() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
<p className="map-hint reveal">{t.mapPage.hint}</p>
|
<p className="map-hint reveal">{t.mapPage.hint}</p>
|
||||||
|
<RingNext steps={rings.afterDiscover} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
55
frontend/src/components/PwaInstallHint.tsx
Normal file
55
frontend/src/components/PwaInstallHint.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useI18n } from "@/lib/i18n";
|
||||||
|
|
||||||
|
/** Soft install hint — shows once when browser fires beforeinstallprompt. */
|
||||||
|
export default function PwaInstallHint() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [deferred, setDeferred] = useState<{ prompt: () => Promise<void> } | null>(null);
|
||||||
|
const [hidden, setHidden] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.matchMedia("(display-mode: standalone)").matches) return;
|
||||||
|
if (localStorage.getItem("nomadro-pwa-hint") === "1") return;
|
||||||
|
|
||||||
|
const onBip = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const ev = e as Event & { prompt: () => Promise<void> };
|
||||||
|
setDeferred({ prompt: () => ev.prompt() });
|
||||||
|
setHidden(false);
|
||||||
|
};
|
||||||
|
window.addEventListener("beforeinstallprompt", onBip);
|
||||||
|
return () => window.removeEventListener("beforeinstallprompt", onBip);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (hidden || !deferred) return null;
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
localStorage.setItem("nomadro-pwa-hint", "1");
|
||||||
|
setHidden(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const install = async () => {
|
||||||
|
try {
|
||||||
|
await deferred.prompt();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
dismiss();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pwa-install-hint" role="status">
|
||||||
|
<p>{t.pwa.installHint}</p>
|
||||||
|
<div className="pwa-install-actions">
|
||||||
|
<button type="button" className="btn btn-primary btn-sm" onClick={() => void install()}>
|
||||||
|
安装
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-ghost btn-sm" onClick={dismiss}>
|
||||||
|
稍后再说
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -121,7 +121,11 @@ export const STATIC_PAGES: Record<string, StaticPageDef> = {
|
|||||||
},
|
},
|
||||||
{ heading: "投递", body: "hello@nomadro.com,标题注明「应聘 - 岗位名」。" },
|
{ heading: "投递", body: "hello@nomadro.com,标题注明「应聘 - 岗位名」。" },
|
||||||
],
|
],
|
||||||
cta: [{ label: "赏金任务", href: "/gigs" }],
|
cta: [
|
||||||
|
{ label: "赏金任务", href: "/gigs" },
|
||||||
|
{ label: "内容投稿", href: "/submit" },
|
||||||
|
{ label: "联系邮箱", href: "mailto:hello@nomadro.com" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
support: {
|
support: {
|
||||||
tag: "🤝 SUPPORT",
|
tag: "🤝 SUPPORT",
|
||||||
@ -130,7 +134,13 @@ export const STATIC_PAGES: Record<string, StaticPageDef> = {
|
|||||||
{ heading: "成为 VIP", body: "解锁匹配、直播、学院课程,支持平台持续运营。" },
|
{ heading: "成为 VIP", body: "解锁匹配、直播、学院课程,支持平台持续运营。" },
|
||||||
{ heading: "分享", body: "推荐给朋友,或在社交媒体分享你的旅居故事。" },
|
{ heading: "分享", body: "推荐给朋友,或在社交媒体分享你的旅居故事。" },
|
||||||
{ heading: "贡献内容", body: "投稿视频、电子书或博客,经审核后展示。" },
|
{ heading: "贡献内容", body: "投稿视频、电子书或博客,经审核后展示。" },
|
||||||
|
{ heading: "反馈问题", body: "发现 Bug 或有产品想法,欢迎直接提交反馈。" },
|
||||||
|
],
|
||||||
|
cta: [
|
||||||
|
{ label: "开通会员", href: "/join" },
|
||||||
|
{ label: "内容投稿", href: "/submit" },
|
||||||
|
{ label: "提交反馈", href: "/feedback" },
|
||||||
|
{ label: "电子书", href: "/book" },
|
||||||
],
|
],
|
||||||
cta: [{ label: "开通会员", href: "/join" }, { label: "内容投稿", href: "/submit" }],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user