Port NomadCNA city_details/content/routes seeding into PocketBase and surface full destination pages with related meetups and discussions. Co-authored-by: Cursor <cursoragent@cursor.com>
238 lines
8.9 KiB
Python
238 lines
8.9 KiB
Python
"""City detail depth for destinations — NomadCNA city_details adapted to nomadro slugs."""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from app.data.mock_data import DESTINATIONS
|
||
|
||
|
||
def _city_detail(dest: dict[str, Any], index: int) -> dict[str, Any]:
|
||
slug = dest["slug"]
|
||
name = dest["name"]
|
||
cost = int(dest.get("cost") or 5000)
|
||
speed = int(dest.get("speed") or 50)
|
||
temp = int(dest.get("temperature") or 22)
|
||
rating = float(dest.get("rating") or 8.5)
|
||
nomads_raw = str(dest.get("nomads_count") or "1000")
|
||
nomads = int("".join(c for c in nomads_raw if c.isdigit()) or "1000")
|
||
tags = dest.get("highlights") or []
|
||
tag_short = [t.replace("🏄 ", "").replace("🧘 ", "").replace("💰 ", "").replace("🌴 ", "")[:12] for t in tags][:4]
|
||
rent = max(1200, round(cost * 0.42))
|
||
food = max(800, round(cost * 0.28))
|
||
cowork = max(200, round(cost * 0.12))
|
||
transport = max(200, round(cost * 0.08))
|
||
leisure = max(300, cost - rent - food - cowork - transport)
|
||
hue = int(dest.get("hue") or 170)
|
||
|
||
return {
|
||
"citySlug": slug,
|
||
"guide": {
|
||
"summary": f"{name}适合想把远程工作和生活质量同时拉高的人。建议先用 2–4 周短住验证网络、社群和生活节奏,再决定是否长期停留。{dest.get('description', '')[:80]}",
|
||
"bestFor": tag_short or ["远程办公", "旅居试住"],
|
||
"workSetup": f"建议选择靠近核心生活区的住处,搭配 1 个固定共享办公点和 2 个备用咖啡馆。当前网速参考值为 {speed} Mbps。",
|
||
"arrivalChecklist": ["确认 4G/5G 覆盖", "预约第一周住宿", "加入本地社群", "收藏 2 个办公点", "报名一场线下活动"],
|
||
"linkedContent": ["digital-nomad-guide", "visa-playbook"],
|
||
},
|
||
"pros": {
|
||
"pros": [
|
||
f"月均生活成本约 ¥{cost:,},适合控制预算。",
|
||
f"社区活跃度参考 {nomads_raw},容易找到同频伙伴。",
|
||
f"综合评分 {rating}/10,亮点:{'、'.join(tag_short[:3]) or name}。",
|
||
],
|
||
"cons": [
|
||
"热门区域旺季住宿价格波动较大。",
|
||
"长期停留前仍需实测具体住处网络。",
|
||
"社交圈集中,需要主动参加活动融入。",
|
||
],
|
||
},
|
||
"reviews": {
|
||
"rating": rating,
|
||
"items": [
|
||
{"author": "小林", "role": "前端开发", "text": f"{name}最适合有稳定远程收入的人,白天专注工作,晚上参加小规模聚会。", "score": 5},
|
||
{"author": "Marco", "role": "UI 设计师", "text": "如果你需要高强度商务会面,建议先看交通和航班;如果是深度工作,这里很舒服。", "score": 4},
|
||
{"author": "阿静", "role": "内容创作者", "text": "生活素材很多,但要避免把旅行感当成生产力,最好保持固定作息。", "score": 4},
|
||
],
|
||
},
|
||
"cost": {
|
||
"monthlyTotal": cost,
|
||
"currency": "CNY",
|
||
"breakdown": [
|
||
{"label": "住宿", "amount": rent},
|
||
{"label": "餐饮", "amount": food},
|
||
{"label": "共享办公", "amount": cowork},
|
||
{"label": "交通", "amount": transport},
|
||
{"label": "社交/休闲", "amount": leisure},
|
||
],
|
||
"tip": "预算表与城市卡片成本字段关联,可按个人消费校准。",
|
||
},
|
||
"people": {
|
||
"nomadsNow": nomads,
|
||
"personas": [
|
||
{"name": "远程全职", "percent": 36},
|
||
{"name": "自由职业", "percent": 28},
|
||
{"name": "独立开发者", "percent": 18},
|
||
{"name": "内容创作者", "percent": 18},
|
||
],
|
||
"featuredProfiles": ["小林", "Marco", "阿静"],
|
||
},
|
||
"chat": {
|
||
"channels": [
|
||
{"name": f"{name}落地互助群", "members": min(999, nomads), "status": "open"},
|
||
{"name": f"{name}共享办公情报", "members": round(nomads * 0.42), "status": "open"},
|
||
{"name": "周末活动约局", "members": round(nomads * 0.28), "status": "members"},
|
||
],
|
||
"latestTopics": ["本周线下咖啡局", "哪里适合视频会议", "短租避坑清单"],
|
||
},
|
||
"photos": {
|
||
"items": [
|
||
f"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1200&h={hue}",
|
||
"https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=1200",
|
||
"https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?w=1200",
|
||
]
|
||
},
|
||
"weather": {
|
||
"temperature": temp,
|
||
"humidity": 55 + (index * 3) % 30,
|
||
"bestMonths": ["3月", "4月", "10月", "11月"],
|
||
"note": f"当前气候估算适合中长期试住,体感温度需结合季节和住宿条件判断。",
|
||
},
|
||
"trends": {
|
||
"growth": [
|
||
{"month": "1月", "value": max(50, nomads - 160)},
|
||
{"month": "2月", "value": max(70, nomads - 120)},
|
||
{"month": "3月", "value": max(90, nomads - 80)},
|
||
{"month": "4月", "value": max(120, nomads - 40)},
|
||
{"month": "5月", "value": nomads},
|
||
],
|
||
"insight": f"{name}的社群热度正在上升,新增用户主要来自远程全职和自由职业人群。",
|
||
},
|
||
"demographics": {
|
||
"age": [
|
||
{"label": "22-27", "value": 24},
|
||
{"label": "28-34", "value": 46},
|
||
{"label": "35-44", "value": 22},
|
||
{"label": "45+", "value": 8},
|
||
],
|
||
"work": [
|
||
{"label": "技术/产品", "value": 34},
|
||
{"label": "内容/设计", "value": 26},
|
||
{"label": "运营/市场", "value": 18},
|
||
{"label": "创业/自由职业", "value": 22},
|
||
],
|
||
},
|
||
"relatedContent": ["digital-nomad-guide", "visa-playbook"],
|
||
}
|
||
|
||
|
||
CITY_DETAILS: list[dict[str, Any]] = [
|
||
_city_detail(d, i) for i, d in enumerate(DESTINATIONS)
|
||
]
|
||
|
||
CITY_DETAILS_BY_SLUG: dict[str, dict[str, Any]] = {
|
||
d["citySlug"]: d for d in CITY_DETAILS
|
||
}
|
||
|
||
|
||
# Extra nomad routes aligned with destination slugs
|
||
EXTRA_ROUTES: list[dict[str, Any]] = [
|
||
{
|
||
"slug": "sea-slow-trail",
|
||
"title": "东南亚慢旅三角",
|
||
"titleEn": "SEA Slow Trail",
|
||
"citySlugs": ["chiangmai", "bali"],
|
||
"durationDays": 90,
|
||
"budget": 18000,
|
||
"description": "清迈 → 巴厘岛,低成本 + 高速网络 + 成熟游民社区。",
|
||
"descriptionEn": "Chiang Mai to Bali for cost and community.",
|
||
"stops": ["清迈 45 天", "巴厘岛 45 天"],
|
||
"status": "published",
|
||
"emoji": "🌴",
|
||
},
|
||
{
|
||
"slug": "euro-culture-loop",
|
||
"title": "欧洲文化环线",
|
||
"titleEn": "Euro Culture Loop",
|
||
"citySlugs": ["lisbon", "barcelona"],
|
||
"durationDays": 60,
|
||
"budget": 35000,
|
||
"description": "里斯本 → 巴塞罗那,签证友好、文化丰富。",
|
||
"descriptionEn": "Lisbon to Barcelona for visas and culture.",
|
||
"stops": ["里斯本 30 天", "巴塞罗那 30 天"],
|
||
"status": "published",
|
||
"emoji": "🏰",
|
||
},
|
||
{
|
||
"slug": "latam-budget-run",
|
||
"title": "拉美性价比冲刺",
|
||
"titleEn": "LATAM Budget Run",
|
||
"citySlugs": ["mexico"],
|
||
"durationDays": 45,
|
||
"budget": 12000,
|
||
"description": "墨西哥城深度试住,贴近北美时区、生活成本可控。",
|
||
"descriptionEn": "Mexico City deep stay near North America timezones.",
|
||
"stops": ["墨西哥城 45 天"],
|
||
"status": "published",
|
||
"emoji": "🌮",
|
||
},
|
||
{
|
||
"slug": "asia-metro-sprint",
|
||
"title": "亚洲都市效率线",
|
||
"titleEn": "Asia Metro Sprint",
|
||
"citySlugs": ["tokyo", "chiangmai"],
|
||
"durationDays": 40,
|
||
"budget": 28000,
|
||
"description": "东京高效率冲刺 + 清迈成本回调,适合产品与工程团队。",
|
||
"descriptionEn": "Tokyo sprint then Chiang Mai cooldown.",
|
||
"stops": ["东京 14 天", "清迈 26 天"],
|
||
"status": "published",
|
||
"emoji": "🚄",
|
||
},
|
||
]
|
||
|
||
|
||
CONTENT_ITEMS: list[dict[str, Any]] = [
|
||
{
|
||
"slug": "digital-nomad-guide",
|
||
"type": "guide",
|
||
"title": "数字游民落地手册",
|
||
"titleEn": "Digital Nomad Landing Guide",
|
||
"subtitle": "签证、网络、住宿、社群一次看懂",
|
||
"description": "覆盖东南亚与欧洲热门城市的 30 天落地清单。",
|
||
"coverImage": "",
|
||
"mediaUrl": "",
|
||
"targetUrl": "/digital",
|
||
"ctaLabel": "打开手册",
|
||
"citySlugs": ["bali", "chiangmai", "lisbon"],
|
||
"sortOrder": 1,
|
||
"status": "published",
|
||
"authorName": "nomadro",
|
||
},
|
||
{
|
||
"slug": "visa-playbook",
|
||
"type": "guide",
|
||
"title": "签证选址速查",
|
||
"titleEn": "Visa Playbook",
|
||
"subtitle": "D7 / Nomad Visa / LTR 对照",
|
||
"description": "按收入门槛与审批周期对比主流签证路径。",
|
||
"targetUrl": "/#visa",
|
||
"ctaLabel": "看签证",
|
||
"citySlugs": ["lisbon", "barcelona", "tokyo"],
|
||
"sortOrder": 2,
|
||
"status": "published",
|
||
"authorName": "nomadro",
|
||
},
|
||
{
|
||
"slug": "bali-cowork-week",
|
||
"type": "video",
|
||
"title": "巴厘岛一周联合办公实录",
|
||
"subtitle": "Canggu 节奏与咖啡馆踩点",
|
||
"description": "适合第一次去巴厘岛远程工作的人。",
|
||
"targetUrl": "/videos",
|
||
"ctaLabel": "看视频",
|
||
"citySlugs": ["bali"],
|
||
"sortOrder": 3,
|
||
"status": "published",
|
||
"authorName": "nomadro",
|
||
},
|
||
]
|