Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4940278ea5 | |||
| 6a46e3109a |
@@ -24,19 +24,18 @@ jobs:
|
||||
SITE_URL: https://pangolin.yanmeiai.com
|
||||
run: bash scripts/ci/compile-site.sh
|
||||
|
||||
# 用户中心(web/usercenter,basePath=/user,http 模式)构建 → out/。
|
||||
- name: Compile (用户中心 Next.js)
|
||||
run: bash scripts/ci/compile-usercenter.sh
|
||||
|
||||
# 合并:用户中心并入官网 dist/user/ + _headers 按 /user/* 分域(见脚本)。
|
||||
# 域名迁移:原独立子域 app.yanmeiai.com → 主站子路径 pangolin.yanmeiai.com/user/。
|
||||
- name: Combine (官网 + 用户中心)
|
||||
run: bash scripts/ci/combine-site.sh
|
||||
|
||||
# 单次部署合并产物到 pangolin-site:/ 官网、/user/ 用户中心。
|
||||
- name: Deploy → Cloudflare Pages
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: bash scripts/ci/deploy-site.sh
|
||||
|
||||
# 用户中心(web/usercenter → CF Pages pangolin-usercenter / app.yanmeiai.com)。
|
||||
# 与官网同一发版(site-v*),避免两处各自漂移。⚠️ 必须 http 模式构建(见脚本)。
|
||||
- name: Compile (用户中心 Next.js)
|
||||
run: bash scripts/ci/compile-usercenter.sh
|
||||
|
||||
- name: Deploy usercenter → Cloudflare Pages
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: bash scripts/ci/deploy-usercenter.sh
|
||||
|
||||
@@ -8,3 +8,11 @@ const String kApiBaseUrl = String.fromEnvironment(
|
||||
'PANGOLIN_API_URL',
|
||||
defaultValue: 'https://api.yanmeiai.com',
|
||||
);
|
||||
|
||||
/// 用户中心(网页)基址(单源)。已从独立子域 app.yanmeiai.com 迁到主站子路径
|
||||
/// pangolin.yanmeiai.com/user/(旧子域已停用)。末尾不带斜杠;调用方自行拼 path。
|
||||
/// 本地联调可 --dart-define=PANGOLIN_USERCENTER_URL=http://127.0.0.1:3000 覆盖。
|
||||
const String kWebUserCenterBaseUrl = String.fromEnvironment(
|
||||
'PANGOLIN_USERCENTER_URL',
|
||||
defaultValue: 'https://pangolin.yanmeiai.com/user',
|
||||
);
|
||||
|
||||
@@ -8,9 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../state/account_providers.dart';
|
||||
|
||||
/// 用户中心(网页版)基址。
|
||||
const String kWebUserCenterBaseUrl = 'https://app.yanmeiai.com';
|
||||
import 'api_config.dart';
|
||||
|
||||
/// 打开用户中心网页版 [path](默认首页),尝试免登录(SSO 换票)。
|
||||
Future<void> openWebUserCenter(WidgetRef ref, {String path = '/'}) async {
|
||||
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# combine-site.sh — 把用户中心(web/usercenter/out/)并入官网产物,供**单次** CF Pages
|
||||
# 部署到 pangolin-site:官网在 /、用户中心在 /user/(域名迁移:原独立子域
|
||||
# app.yanmeiai.com → pangolin.yanmeiai.com/user/)。
|
||||
#
|
||||
# 关键:两个 app 的 CSP 不同(官网用脚本哈希 + connect-src 'self';用户中心要
|
||||
# 'unsafe-inline' + connect-src https 调 api)。CF Pages 只读产物根 _headers,故把
|
||||
# 用户中心的 _headers 规则前缀重定到 /user/*,追加在官网规则之后 —— CF Pages 对同
|
||||
# 一路径「后写规则覆盖先写」,于是 /user/* 命中用户中心 CSP,其余走官网 CSP。
|
||||
#
|
||||
# 由 compile-site.sh + compile-usercenter.sh 先产出 dist/ 与 out/;从 repo 根调用。
|
||||
set -euo pipefail
|
||||
|
||||
DIST=web/website/dist
|
||||
UC=web/usercenter/out
|
||||
|
||||
[ -d "$DIST" ] || { echo "==> combine-site: $DIST 不存在(先跑 compile-site.sh)" >&2; exit 1; }
|
||||
[ -d "$UC" ] || { echo "==> combine-site: $UC 不存在(先跑 compile-usercenter.sh)" >&2; exit 1; }
|
||||
|
||||
# 1) 用户中心产物挂到 /user/。
|
||||
rm -rf "${DIST}/user"
|
||||
mkdir -p "${DIST}/user"
|
||||
cp -R "${UC}/." "${DIST}/user/"
|
||||
|
||||
# 2) 合并 _headers:用户中心路径规则前缀 /user,追加到官网 _headers 之后。
|
||||
# 删掉并入产物里那份会被 CF 忽略的 /user/_headers,避免误导。
|
||||
rm -f "${DIST}/user/_headers"
|
||||
{
|
||||
echo ""
|
||||
echo "# ==== 用户中心(/user/*)独立 CSP,combine-site.sh 从 usercenter/_headers 重定 ===="
|
||||
# 行首 '/'(路径规则)前缀 /user;'#' 注释与缩进的 header 行不动。
|
||||
sed -E 's#^/#/user/#' "${UC}/_headers"
|
||||
} >> "${DIST}/_headers"
|
||||
|
||||
echo "==> combine-site: done — 用户中心并入 ${DIST}/user/,_headers 已按 /user/* 分域合并"
|
||||
ls -la "${DIST}/user" | head
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# compile-usercenter.sh — build the Next.js 用户中心 (web/usercenter) as a static
|
||||
# export (output: web/usercenter/out/). Deployed to Cloudflare Pages
|
||||
# (project pangolin-usercenter, 自定义域 app.yanmeiai.com) by deploy-usercenter.sh.
|
||||
# export (output: web/usercenter/out/, basePath=/user)。由 combine-site.sh 并入官网
|
||||
# dist/user/,随官网一起部署到 CF Pages pangolin-site(pangolin.yanmeiai.com/user/)。
|
||||
# 域名迁移:原独立子域 app.yanmeiai.com 已停用。
|
||||
#
|
||||
# ⚠️ 必须以「真实 API」模式构建:NEXT_PUBLIC_API_MODE=http。否则 lib/api/client.ts
|
||||
# 默认落到 mock(内存假数据),部署上去登录/订阅全是演示数据。API 域名是公开信息
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy-usercenter.sh — 部署 Next.js 用户中心 (web/usercenter/out/) 到 Cloudflare
|
||||
# Pages(项目 pangolin-usercenter,自定义域 app.yanmeiai.com)。纯静态导出,全程
|
||||
# HTTPS,不落 VPS,与节点 :443(sing-box)无冲突。
|
||||
#
|
||||
# 需环境变量:
|
||||
# CLOUDFLARE_API_TOKEN 带 Account > Cloudflare Pages > Edit 权限的 CF token
|
||||
# CLOUDFLARE_ACCOUNT_ID CF 账户 ID
|
||||
# 由 compile-usercenter.sh 先产出 web/usercenter/out/;从 repo 根调用。
|
||||
set -euo pipefail
|
||||
|
||||
if [ ! -d web/usercenter/out ]; then
|
||||
echo "==> deploy-usercenter: web/usercenter/out/ 不存在 — 拒绝部署" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! find web/usercenter/out -mindepth 1 -print -quit | grep -q .; then
|
||||
echo "==> deploy-usercenter: web/usercenter/out/ 为空 — 拒绝部署" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
: "${CLOUDFLARE_API_TOKEN:?deploy-usercenter: CLOUDFLARE_API_TOKEN 未设(需带 Pages:Edit)}"
|
||||
: "${CLOUDFLARE_ACCOUNT_ID:?deploy-usercenter: CLOUDFLARE_ACCOUNT_ID 未设}"
|
||||
|
||||
echo "==> deploy-usercenter: wrangler pages deploy → project pangolin-usercenter (branch main)"
|
||||
npx --yes wrangler@4 pages deploy web/usercenter/out \
|
||||
--project-name=pangolin-usercenter --branch=main --commit-dirty=true
|
||||
|
||||
echo "==> deploy-usercenter: done"
|
||||
@@ -123,7 +123,7 @@ func main() {
|
||||
r.Use(chimw.Logger)
|
||||
r.Use(chimw.Recoverer)
|
||||
r.Use(apierr.Middleware)
|
||||
// CORS:Web 用户中心(app.yanmeiai.com)跨域调 /v1/*;原生端不受影响。
|
||||
// CORS:Web 用户中心(pangolin.yanmeiai.com/user)跨域调 /v1/*;原生端不受影响。
|
||||
r.Use(httpapi.NewCORS())
|
||||
|
||||
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -16,7 +16,9 @@ import (
|
||||
func NewCORS() func(http.Handler) http.Handler {
|
||||
raw := os.Getenv("CORS_ORIGINS")
|
||||
if raw == "" {
|
||||
raw = "https://app.yanmeiai.com,https://pangolin-usercenter.pages.dev"
|
||||
// 用户中心已迁到主站子路径 pangolin.yanmeiai.com/user/,其 origin 即主站域名
|
||||
// (旧 app.yanmeiai.com 已停用)。pages.dev 为 CF Pages 直连兜底。
|
||||
raw = "https://pangolin.yanmeiai.com,https://pangolin-site.pages.dev"
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
|
||||
@@ -7,17 +7,17 @@ import (
|
||||
)
|
||||
|
||||
func TestCORS(t *testing.T) {
|
||||
t.Setenv("CORS_ORIGINS", "https://app.yanmeiai.com")
|
||||
t.Setenv("CORS_ORIGINS", "https://pangolin.yanmeiai.com")
|
||||
mw := NewCORS()
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
|
||||
h := mw(next)
|
||||
|
||||
// 允许的 Origin:补 Allow-Origin,普通请求继续。
|
||||
r := httptest.NewRequest("POST", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
r.Header.Set("Origin", "https://pangolin.yanmeiai.com")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.yanmeiai.com" {
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://pangolin.yanmeiai.com" {
|
||||
t.Fatalf("allowed origin: want header, got %q", got)
|
||||
}
|
||||
if w.Code != 200 {
|
||||
@@ -26,7 +26,7 @@ func TestCORS(t *testing.T) {
|
||||
|
||||
// OPTIONS 预检:204,不落到业务。
|
||||
r = httptest.NewRequest("OPTIONS", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
r.Header.Set("Origin", "https://pangolin.yanmeiai.com")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusNoContent {
|
||||
|
||||
@@ -12,13 +12,17 @@ export const viewport: Viewport = {
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
// basePath 前缀(与 next.config.mjs 同源)。public/ 的根绝对资源(colors_and_type.css)
|
||||
// 不会被 Next 自动加 basePath,需手动拼,否则挂到 /user/ 下会 404。
|
||||
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '/user';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" data-theme="light" suppressHydrationWarning>
|
||||
<head>
|
||||
{/* 设计令牌单一真相源,原样链入(SRI 由构建期注入) */}
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/colors_and_type.css" />
|
||||
<link rel="stylesheet" href={`${BASE_PATH}/colors_and_type.css`} />
|
||||
</head>
|
||||
<body>
|
||||
<UIProvider>{children}</UIProvider>
|
||||
|
||||
@@ -34,15 +34,17 @@ export function LangSeg({ lang, setLang }: { lang: Lang; setLang: (l: Lang) => v
|
||||
onChange={(e) => setLang(e.target.value as Lang)}
|
||||
aria-label="Language"
|
||||
style={{
|
||||
border: '1px solid var(--border)',
|
||||
// 圆角矩形 + 柔和边框(原纯 --border 太生硬)。
|
||||
border: '1px solid color-mix(in oklab, var(--border), transparent 55%)',
|
||||
cursor: 'pointer',
|
||||
borderRadius: 999,
|
||||
padding: '5px 9px',
|
||||
borderRadius: 10,
|
||||
padding: '5px 10px',
|
||||
fontFamily: 'var(--font-sans)',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 700,
|
||||
background: 'var(--bg-subtle)',
|
||||
color: 'var(--fg2)',
|
||||
boxShadow: '0 1px 2px rgba(45, 30, 20, 0.05)',
|
||||
}}
|
||||
>
|
||||
{langs.map(([v, l]) => (
|
||||
|
||||
@@ -5,6 +5,10 @@ const nextConfig = {
|
||||
output: 'export',
|
||||
reactStrictMode: true,
|
||||
trailingSlash: true,
|
||||
// 用户中心从独立子域 app.yanmeiai.com 迁到主站子路径:与官网合并部署到同一
|
||||
// CF Pages(pangolin-site),挂在 pangolin.yanmeiai.com/user/。basePath 让所有
|
||||
// 路由/资源前缀带 /user;可用 NEXT_PUBLIC_BASE_PATH 覆盖(如本地 dev 置空)。
|
||||
basePath: process.env.NEXT_PUBLIC_BASE_PATH ?? '/user',
|
||||
// 静态导出无图片优化服务,关闭以保证产物无服务端依赖。
|
||||
images: { unoptimized: true },
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export const SITE = {
|
||||
store: { label: 'shop.pangolin.vpn' },
|
||||
|
||||
/** Web 用户中心(web/usercenter,Next.js 静态导出 → CF Pages)。登录/订阅/设备管理入口。 */
|
||||
usercenter: 'https://app.yanmeiai.com',
|
||||
usercenter: 'https://pangolin.yanmeiai.com/user/',
|
||||
|
||||
/**
|
||||
* 客户端安装包直链。控制面 pangolin-server 通过 Cloudflare Tunnel 对外暴露
|
||||
|
||||
@@ -40,19 +40,24 @@
|
||||
}
|
||||
.langseg a.on { background: var(--accent); color: var(--fg-on-accent); }
|
||||
|
||||
/* 6 语用原生下拉(横排段控会挤);沿用 langseg 的药丸视觉。 */
|
||||
/* 6 语用原生下拉(横排段控会挤)。圆角矩形 + 柔和边框(原纯 --border 太生硬)。 */
|
||||
.langsel {
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid color-mix(in oklab, var(--border), transparent 55%);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-full);
|
||||
padding: 5px 9px;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 5px 10px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: var(--fg2);
|
||||
background: var(--bg-subtle);
|
||||
box-shadow: 0 1px 2px rgba(45, 30, 20, 0.05);
|
||||
transition: border-color 140ms ease, box-shadow 140ms ease;
|
||||
}
|
||||
.langsel:hover {
|
||||
border-color: color-mix(in oklab, var(--accent), transparent 40%);
|
||||
box-shadow: 0 1px 4px rgba(45, 30, 20, 0.08);
|
||||
}
|
||||
.langsel:hover { border-color: var(--accent); }
|
||||
|
||||
/* 视觉隐藏(无障碍用,当前未强依赖) */
|
||||
.visually-hidden {
|
||||
|
||||
Reference in New Issue
Block a user