update
This commit is contained in:
41
Dockerfile
41
Dockerfile
@ -1,21 +1,17 @@
|
|||||||
# ---- 前端构建阶段 ----
|
# ---- 构建阶段 ----
|
||||||
FROM node:22-alpine AS build
|
FROM node:22-alpine AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 构建期 SSG 拉数据用的「后端 SSG 内部地址」。
|
# 构建期 SSG 取数地址:走宿主回环的代理 18091(deploy.sh 传 build-arg,配合 build.network=host)。
|
||||||
# 在 docker-compose 里通过 build.args.BASE_API_SSG 传入(= http://localhost:8091,
|
# 由代理转发到「当前活跃的后端槽」,所以构建期不受正在更新哪个槽影响。
|
||||||
# 配合 build.network: host,让前端构建容器能访问已启动的 backend 容器的 SSG 内部端口)。
|
ARG BASE_API_SSG=http://127.0.0.1:18091
|
||||||
# 该端口只绑宿主回环(127.0.0.1:8091),nginx 绝不反代,因此 SSG 全量数据不会外泄。
|
|
||||||
# BASE_API_SSG 与运行期 BASE_API(8090)完全分离,二者指向不同端口。
|
|
||||||
ARG BASE_API_SSG=http://localhost:8091
|
|
||||||
ENV BASE_API_SSG=$BASE_API_SSG
|
ENV BASE_API_SSG=$BASE_API_SSG
|
||||||
|
|
||||||
# SSG 内部端口的可选访问令牌(防御纵深)。仅构建期 node fetch 使用,且不带 PUBLIC_ 前缀,
|
# SSG 内部接口可选令牌(无 PUBLIC_ 前缀,绝不会进入前端 bundle)
|
||||||
# 故绝不会进入前端 bundle。置空时后端不校验(依赖回环绑定即可)。
|
|
||||||
ARG SSG_TOKEN=""
|
ARG SSG_TOKEN=""
|
||||||
ENV SSG_TOKEN=$SSG_TOKEN
|
ENV SSG_TOKEN=$SSG_TOKEN
|
||||||
|
|
||||||
# 运行期浏览器调用地址:置空 = 同源相对路径 /api(由下方 nginx 反代到 backend:8090)
|
# 运行期浏览器调用地址:空 = 同源相对 /api(由 proxy 反代到后端)
|
||||||
ENV BASE_API=""
|
ENV BASE_API=""
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
@ -23,15 +19,22 @@ RUN npm ci
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# ---- 运行阶段 ----
|
# ---- 运行阶段:只跑 Astro node server,nginx 已独立成 proxy 容器 ----
|
||||||
# 前端以 @astrojs/node standalone server 运行(监听 3000),nginx 反代 / 到该端口、
|
|
||||||
# 反代 /api 到 backend:8090。这样 on-demand SSR 详情页 / sitemap / robots 才能正确产出 HTML。
|
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
RUN apk add --no-cache nginx
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
COPY --from=build /app/dist ./dist
|
COPY --from=build /app/dist ./dist
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
EXPOSE 3000
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
EXPOSE 80
|
# 只探端口存活:SSR 取数临时失败不应该把容器判死(否则蓝绿切换会误判)
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
HEALTHCHECK --interval=15s --timeout=3s --start-period=15s --retries=3 \
|
||||||
|
CMD nc -z 127.0.0.1 3000 || exit 1
|
||||||
|
|
||||||
|
CMD ["node", "./dist/server/entry.mjs"]
|
||||||
|
|||||||
48
README.md
48
README.md
@ -1,19 +1,20 @@
|
|||||||
# Xisoa — 时尚档案(Fashion Archive)
|
# Xisoa — 时尚档案(Fashion Archive)
|
||||||
|
|
||||||
记录走秀(runway looks)与街拍(street style)的时尚档案站。前端为 **Astro 静态站**,
|
记录走秀(runway looks)与街拍(street style)的时尚档案站。前端为 **Astro**(列表静态生成,详情页走按需 SSR),
|
||||||
数据来自独立的 **Go + MySQL 后端**(走秀/街拍列表、详情、收藏、历史等)。
|
数据来自独立的 **Go 后端 + PostgreSQL(pgvector)**,两者各自独立仓库、通过 Docker Compose 一起上线。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
| 层 | 选型 |
|
| 层 | 选型 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 前端框架 | Astro 7(静态输出,详情页走 `@astrojs/node` 按需 SSR) |
|
| 前端框架 | Astro 7(`@astrojs/node` standalone,详情页按需 SSR) |
|
||||||
| 构建 | Vite 8(Rolldown) |
|
| 构建 | Vite 8(Rolldown) |
|
||||||
| 样式 | Tailwind CSS v4 |
|
| 样式 | Tailwind CSS v4 |
|
||||||
| 交互 | Alpine.js(导航菜单、登录弹窗、收藏、灯箱等轻量交互) |
|
| 交互 | Alpine.js(导航菜单、登录弹窗、收藏、灯箱等轻量交互) |
|
||||||
| 轮播 | Swiper |
|
| 轮播 | Swiper |
|
||||||
| 后端 | Go + MySQL(独立仓库,不在本仓库内) |
|
| 后端 | Go + Gin + GORM + PostgreSQL 16 + pgvector(独立仓库 `../backend`) |
|
||||||
| 部署 | Docker Compose + nginx(见 `DEPLOY.md`) |
|
| 图片存储 | 缤纷云 S4(对象存储,不落服务器磁盘) |
|
||||||
|
| 部署 | Docker Compose,蓝绿双槽 + 常驻 nginx,零停机发布(**部署入口在后端仓**) |
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
@ -37,28 +38,32 @@ src/
|
|||||||
└── env.d.ts
|
└── env.d.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
本仓库只负责前端构建产物(`Dockerfile` 只跑 Astro node server)。代理、数据库、发布脚本都在后端仓。
|
||||||
|
|
||||||
## 环境与配置
|
## 环境与配置
|
||||||
|
|
||||||
- **非密配置**:`src/lib/config/env.dev.ts`(dev)与 `env.prod.ts`(prod),按 `import.meta.env.PROD`
|
- **非密配置**:`src/lib/config/env.dev.ts`(dev)与 `env.prod.ts`(prod),按 `import.meta.env.PROD` 自动选择。
|
||||||
自动选择,缺项即抛异常。字段说明见 `src/lib/config/types.ts`:
|
|
||||||
- `baseApi`:浏览器运行期 API 基址(生产为空串 = 同源 `/api`)。
|
- `baseApi`:浏览器运行期 API 基址(生产为空串 = 同源 `/api`)。
|
||||||
- `baseApiSsr`:运行期按需 SSR 取数基址(Node fetch 需绝对 URL)。
|
- `baseApiSsr`:运行期 SSR 取数基址,可用环境变量 `BASE_API_SSR` 覆盖(容器内必须是 `http://proxy:18090`)。
|
||||||
- `baseApiSsg`:构建期 SSG 内部端口基址(仅回环,不对公网暴露)。
|
- `baseApiSsg`:构建期 SSG 内部端口基址,可用 `BASE_API_SSG` 覆盖(Docker 构建时走 `127.0.0.1:18091`)。
|
||||||
- `idleLogoutDays` / `siteName`。
|
- `idleLogoutDays` / `siteName`。
|
||||||
- **密钥**:不进 `config`(会打包进客户端 bundle),仅走构建期 env,例如 `SSG_TOKEN`。
|
- **密钥**:不进 `config`(会打包进客户端 bundle),仅走构建期 env,例如 `SSG_TOKEN`。
|
||||||
|
|
||||||
## 常用命令
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:4321
|
||||||
|
```
|
||||||
|
|
||||||
|
> 本地需先启动后端:接口默认 `localhost:8090`,SSG 内部端口 `localhost:8091`(见 `env.dev.ts`)。
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `npm install` | 安装依赖 |
|
| `npm run dev` | 本地开发 |
|
||||||
| `npm run dev` | 本地开发,`http://localhost:4321` |
|
| `npm run build` | 构建到 `./dist/` |
|
||||||
| `npm run build` | 构建静态产物到 `./dist/` |
|
| `npm run preview` | 预览构建产物 |
|
||||||
| `npm run preview` | 本地预览构建产物 |
|
| `npm run check` | 类型 / 模板检查 |
|
||||||
| `npm run check` | `astro check`:类型 / 模板检查 |
|
|
||||||
| `npm run astro ...` | 直接调用 Astro CLI(如 `astro add`) |
|
|
||||||
|
|
||||||
> 本地运行需先启动后端:接口默认落在 `localhost:8090`,SSG 内部端口 `localhost:8091`(见 `env.dev.ts`)。
|
|
||||||
|
|
||||||
## 国际化
|
## 国际化
|
||||||
|
|
||||||
@ -68,4 +73,9 @@ src/
|
|||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
Docker Compose + Gitea Actions,前后端两个仓库联动上线。完整步骤见 [`DEPLOY.md`](./DEPLOY.md)。
|
部署入口在**后端仓**,完整说明见 `../backend/DEPLOY.md`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/fashion/backend
|
||||||
|
./deploy.sh all # 会自动把前端仓一起拉到位并构建
|
||||||
|
```
|
||||||
|
|||||||
38
deploy.sh
38
deploy.sh
@ -1,38 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# 一键上线脚本(在服务器上执行,或本地 SSH 调它)
|
|
||||||
# 服务器目录约定(见 DEPLOY.md):
|
|
||||||
# /opt/fashion/test -> 前端仓(本脚本所在)
|
|
||||||
# /opt/fashion/admin/backend -> 后端仓
|
|
||||||
#
|
|
||||||
# 流程:拉代码 -> 起 db+backend -> 等后端健康 -> 构建前端(SSR 拉数据) -> 起前端
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
cd "$DIR"
|
|
||||||
|
|
||||||
echo "==> [1/5] 拉取最新代码"
|
|
||||||
git pull --ff-only
|
|
||||||
git -C ../admin/backend pull --ff-only || true
|
|
||||||
|
|
||||||
echo "==> [2/5] 启动 db + backend(供前端构建期 SSR 取数)"
|
|
||||||
docker compose up -d db backend
|
|
||||||
|
|
||||||
echo "==> [3/5] 等待 backend :8090 健康"
|
|
||||||
for i in $(seq 1 60); do
|
|
||||||
if curl -fsS "http://localhost:8090/api/public/brands?size=1" >/dev/null 2>&1; then
|
|
||||||
echo " backend 已就绪"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo " 等待中... ($i/60)"
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "==> [4/5] 构建前端镜像(构建期网络=host,经 BASE_API_SSG 拉取 SSG 数据)"
|
|
||||||
docker compose build frontend
|
|
||||||
|
|
||||||
echo "==> [5/5] 启动前端"
|
|
||||||
docker compose up -d frontend
|
|
||||||
|
|
||||||
echo "==> 部署完成,当前状态:"
|
|
||||||
docker compose ps
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
# 生产部署 compose(放在前端仓库根;服务器结构见仓库 README)
|
|
||||||
# 前端仓: /opt/fashion/test (本文件所在)
|
|
||||||
# 后端仓: /opt/fashion/backend (已对齐到当前活跃开发的后端,仅支持 PostgreSQL + pgvector)
|
|
||||||
services:
|
|
||||||
# ---- 数据库 (PostgreSQL + pgvector) ----
|
|
||||||
# 注意:后端代码仅支持 PostgreSQL + pgvector(gorm pgx 驱动),切勿换回 MySQL。
|
|
||||||
db:
|
|
||||||
image: ankane/pgvector:latest # 预编译好 pgvector 的 PG 镜像(latest 当前底层为 PG17)
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: fashion
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-fashion_dev_2026} # 生产务必换强密码
|
|
||||||
POSTGRES_DB: fashion
|
|
||||||
volumes:
|
|
||||||
- db_data:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U fashion -d fashion"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
# ---- 后端 (Go) ----
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ../backend
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
SERVER_PORT: 8090
|
|
||||||
SSG_PORT: 8091 # SSG 内部端口(见下 ports 绑定)
|
|
||||||
SSG_TOKEN: ${SSG_TOKEN:-} # 可选:非空则 SSG 接口需带 token(防御纵深)
|
|
||||||
HASHID_SECRET: ${HASHID_SECRET:-} # 公开 ID 混淆盐值:生产务必注入随机串(改值会使旧链接失效,谨慎变更)
|
|
||||||
GIN_MODE: release
|
|
||||||
JWT_SECRET: ${JWT_SECRET:-change-me-in-prod}
|
|
||||||
CORS_ALLOW_ORIGINS: ${CORS_ALLOW_ORIGINS:-https://xisoa.com,https://www.xisoa.com}
|
|
||||||
RATE_LIMIT_ENABLED: ${RATE_LIMIT_ENABLED:-false}
|
|
||||||
RATE_LIMIT_RPS: ${RATE_LIMIT_RPS:-20}
|
|
||||||
RATE_LIMIT_BURST: ${RATE_LIMIT_BURST:-40}
|
|
||||||
DB_HOST: db
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_USER: ${DB_USER:-fashion}
|
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-fashion_dev_2026}
|
|
||||||
DB_NAME: ${DB_NAME:-fashion}
|
|
||||||
ports:
|
|
||||||
- "8090:8090" # 对外公开 API(运行期由 nginx 反代);生产可去掉此发布,改由前端 nginx 直接反代 backend:8090
|
|
||||||
- "127.0.0.1:8091:8091" # ⚠️ SSG 内部端口:只绑宿主回环,外部网络不可达;nginx 也绝不反代它
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:8090/api/v1/public/brands?size=1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
# ---- 前端 (Astro node server + nginx 反代) ----
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
args:
|
|
||||||
BASE_API_SSG: http://localhost:8091 # 构建期 SSG 拉数据:指向 backend 的 SSG 内部端口(配合 network: host 访问回环绑定的 8091)
|
|
||||||
SSG_TOKEN: ${SSG_TOKEN:-} # 与 backend 的 SSG_TOKEN 保持一致;为空则构建期无需携带 token
|
|
||||||
network: host
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
depends_on:
|
|
||||||
backend:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data:
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# 前端运行期入口:同容器内先后拉起
|
|
||||||
# 1) node standalone server(Astro 构建产物,监听 127.0.0.1:3000,承载 SSR 详情页/静态资源)
|
|
||||||
# 2) nginx(监听 80,反代 / 到 node、/api 到 backend:8090)
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# node server 后台运行;一旦退出,整个容器随之退出(便于编排器感知故障)
|
|
||||||
node ./dist/server/entry.mjs &
|
|
||||||
NODE_PID=$!
|
|
||||||
|
|
||||||
# 捕获 SIGTERM/SIGINT 优雅关闭 node 与 nginx
|
|
||||||
trap 'kill -TERM $NODE_PID 2>/dev/null; nginx -s stop 2>/dev/null; exit 0' TERM INT
|
|
||||||
|
|
||||||
# nginx 前台运行(容器主进程)
|
|
||||||
nginx -g 'daemon off;' &
|
|
||||||
NGINX_PID=$!
|
|
||||||
|
|
||||||
wait -n
|
|
||||||
29
nginx.conf
29
nginx.conf
@ -1,29 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# 前端 node server(Astro standalone,同容器内 127.0.0.1:3000):承载 SSR 详情页 / 静态资源。
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:3000;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 网关隔离:SSG 内部接口只存在于 backend 的 8091 端口(回环绑定,nginx 不反代)。
|
|
||||||
# 若有人从公网域名探测 /api/internal/ssg/*(或旧 /api/v1/ssg/*),此处直接 404,
|
|
||||||
# 确保构建期全量数据绝不从公网出口。
|
|
||||||
location ~ ^/api/(internal/ssg/|v1/ssg/) {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 反向代理后端 API:浏览器走同源 /api,由 nginx 转发到 backend 容器(仅 8090 公开端口)
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://backend:8090/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +1,22 @@
|
|||||||
// src/lib/config/env.prod.ts — 生产环境配置(astro build / MODE=production)
|
// src/lib/config/env.prod.ts — 生产环境配置(astro build / MODE=production)
|
||||||
import type { AppConfig } from "./types"
|
import type { AppConfig } from "./types"
|
||||||
|
|
||||||
|
// SSR 取数地址:容器里 127.0.0.1/localhost 指的是容器自己,不是后端。
|
||||||
|
// docker-compose 会注入 BASE_API_SSR=http://backend:8090(服务名解析);
|
||||||
|
// 非容器部署不注入时回落到本机回环,行为与之前一致。
|
||||||
|
// 用 globalThis 取 process,避免把 Node 专有的 process 直接写进浏览器 bundle。
|
||||||
|
const ssrBaseApi: string =
|
||||||
|
(globalThis as any)?.process?.env?.BASE_API_SSR || "http://localhost:8090"
|
||||||
|
|
||||||
const config: AppConfig = {
|
const config: AppConfig = {
|
||||||
// 空串 = 同源相对:生产由 nginx 把 /api 反代到后端,浏览器直接打同源 /api/v1/...
|
// 空串 = 同源相对:生产由 nginx 把 /api 反代到后端,浏览器直接打同源 /api/v1/...
|
||||||
baseApi: "",
|
baseApi: "",
|
||||||
// 按需渲染 SSR 在 Node 里 fetch,必须是绝对 URL(后端在本机/Docker 内部)。
|
// 按需渲染 SSR 在 Node 里 fetch,必须是绝对 URL(后端在本机/Docker 内部)。
|
||||||
baseApiSsr: "http://localhost:8090",
|
baseApiSsr: ssrBaseApi,
|
||||||
// 构建期 SSG 内部端口:绑定回环,绝不对公网暴露。
|
// 构建期 SSG 内部端口:绑定回环,绝不对公网暴露。
|
||||||
baseApiSsg: "http://127.0.0.1:8091",
|
// Docker 构建时由 build-arg BASE_API_SSG 注入(配合 build.network=host 访问宿主回环)。
|
||||||
|
baseApiSsg:
|
||||||
|
(globalThis as any)?.process?.env?.BASE_API_SSG || "http://127.0.0.1:8091",
|
||||||
idleLogoutDays: 7, // 超过 7 天没访问则登出(0 = 关闭空闲登出)
|
idleLogoutDays: 7, // 超过 7 天没访问则登出(0 = 关闭空闲登出)
|
||||||
siteName: "Xisoa", // 站点名(TDK 的 title 后缀 / description 前缀统一来源)
|
siteName: "Xisoa", // 站点名(TDK 的 title 后缀 / description 前缀统一来源)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,27 +40,32 @@ export function toAbs(url: string): string {
|
|||||||
// 响应式 srcset:基于同一图片 URL 生成多档宽度的候选集,交给浏览器按设备 DPR/视口挑选,
|
// 响应式 srcset:基于同一图片 URL 生成多档宽度的候选集,交给浏览器按设备 DPR/视口挑选,
|
||||||
// 避免「一张大图在所有屏幕都下载」造成的带宽浪费(图片站性能关键项)。
|
// 避免「一张大图在所有屏幕都下载」造成的带宽浪费(图片站性能关键项)。
|
||||||
//
|
//
|
||||||
// 适配本站两类远程图 URL:
|
// 仅对「带查询串」的远程图 URL 生效(?w=1080&q=80 → 改写 w= 参数);
|
||||||
// - 缤纷云 S4 CoreIX 样式串:key!style:high → key!w:480 / !w:768 ...(替换 style 段为宽度段)
|
// 其余一律返回空(不生成 srcset,退回单图):
|
||||||
// - 带查询串:?w=1080&q=80 → 改写 w= 参数
|
// - 本地 /uploads:没有缩放服务;
|
||||||
// 本地 /uploads 没有缩放服务,返回空(不生成 srcset,退回单图)。
|
// - 缤纷云 S4:**桶上没有开启动态缩放**,只有命名样式(!style:thumb / !style:high),
|
||||||
|
// 任何 !w:480 这类宽度变体都会 404。
|
||||||
|
//
|
||||||
|
// ⚠️ 历史坑(2026-10-15):曾把列表图的 !style:thumb 改写成 !w:480/768/1080/1600 四档,
|
||||||
|
// 而桶上并未配置动态缩放 → 四档全部 404。关键点是 `<img srcset>` 一旦有候选集,浏览器
|
||||||
|
// 只从候选里挑、**不会回退到 src**,于是列表整片裂图(封面与缩略图都裂)。
|
||||||
|
// 故带 !xxx: 样式段的 URL 一律不产出 srcset,直接用后端给的单图。
|
||||||
const SRCSET_WIDTHS = [480, 768, 1080, 1600]
|
const SRCSET_WIDTHS = [480, 768, 1080, 1600]
|
||||||
export function toSrcset(url: string): string {
|
export function toSrcset(url: string): string {
|
||||||
if (!url) return ""
|
if (!url) return ""
|
||||||
const abs = toAbs(url)
|
const abs = toAbs(url)
|
||||||
if (!/^https?:\/\//i.test(abs)) return ""
|
if (!/^https?:\/\//i.test(abs)) return ""
|
||||||
const at = (u: string, w: number): string => {
|
// 缤纷云样式串(!style:…)与历史宽度串(!w:…)都不支持按宽度改写,退回单图。
|
||||||
if (/\!style:/.test(u)) return u.replace(/\!style:[^!]+/, `!w:${w}`)
|
if (/![a-z]+:/i.test(abs)) return ""
|
||||||
if (/\!w:\d+/.test(u)) return u.replace(/\!w:\d+/, `!w:${w}`)
|
return SRCSET_WIDTHS.map((w) => {
|
||||||
try {
|
try {
|
||||||
const u2 = new URL(u)
|
const u2 = new URL(abs)
|
||||||
u2.searchParams.set("w", String(w))
|
u2.searchParams.set("w", String(w))
|
||||||
return u2.toString()
|
return `${u2.toString()} ${w}w`
|
||||||
} catch {
|
} catch {
|
||||||
return u + (u.includes("?") ? "&" : "?") + "w=" + w
|
return `${abs + (abs.includes("?") ? "&" : "?") + "w=" + w} ${w}w`
|
||||||
}
|
}
|
||||||
}
|
}).join(", ")
|
||||||
return SRCSET_WIDTHS.map((w) => `${at(abs, w)} ${w}w`).join(", ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按需渲染(on-demand SSR)详情 base:必须是绝对 URL(Node fetch 不吃相对路径)。
|
// 按需渲染(on-demand SSR)详情 base:必须是绝对 URL(Node fetch 不吃相对路径)。
|
||||||
|
|||||||
Reference in New Issue
Block a user