feat(gallery): 原图查看与下载——仓外 originals store 双轨制
- git 仓只进压缩版(构建用);原图落 ~/docker_apps/blog/photos-originals/
(迁移器/ingest 双入口写入,幂等),build.sh 独立 rsync 到
staging/photos-originals/(同一静态根,零 nginx 改动)
- lightbox 增「原图」(新标签全尺寸)+「下载」(download 属性带文件名)
- 原图持久保障:store + Phase 0 备份 tar 双留档;存量 21 张已回填(196M)
验证: astro build 11 页 exit 0;dist/gallery 含 lb-original×2 与
photos-originals 引用;staging/photos-originals 21 张实测
Spec: none (用户功能补req:原图查看/下载,跨 Phase 独立小特性)
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
382e56431f
commit
44bfb23efb
4 files changed
+49
-3
No files matched your search
@@ -13,5 +13,8 @@ pnpm astro build
|
|||||||
echo "[build] rsync -> $STAGING"
|
echo "[build] rsync -> $STAGING"
|
||||||
mkdir -p "$STAGING"
|
mkdir -p "$STAGING"
|
||||||
rsync -a --delete "$REPO_DIR/dist/" "$STAGING/"
|
rsync -a --delete "$REPO_DIR/dist/" "$STAGING/"
|
||||||
|
# 原图仓独立同步(不进 git/dist):/photos-originals/ 供图库"查看原图/下载"
|
||||||
|
ORIG="$HOME/docker_apps/blog/photos-originals"
|
||||||
|
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
|
||||||
printf 'built at %s\n' "$(date -Is)" > "$STAGING/.build-stamp"
|
printf 'built at %s\n' "$(date -Is)" > "$STAGING/.build-stamp"
|
||||||
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files"
|
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files"
|
||||||
@@ -61,9 +61,11 @@ def main() -> int:
|
|||||||
fname = unquote(url.rsplit("/", 1)[1])
|
fname = unquote(url.rsplit("/", 1)[1])
|
||||||
slug = slugify(spec.get("displayName") or fname)
|
slug = slugify(spec.get("displayName") or fname)
|
||||||
data = urllib.request.urlopen(BASE + url, timeout=60).read()
|
data = urllib.request.urlopen(BASE + url, timeout=60).read()
|
||||||
# 入仓前压到长边 2400/q86:原图总计 196M(单张最大 28M)会永久膨胀
|
# 原图双留档:仓外 originals store(构建时独立 rsync,服务"查看原图/
|
||||||
# 公开仓历史并撞死 nginx 100M 推送上限;原图已随 Phase 0 备份归档,
|
# 下载")+ Phase 0 备份 tar;git 仓只进压缩版(196M 事故教训)
|
||||||
# 站内展示由 sharp 出 srcset,2400px 足够 lightbox 全屏
|
STORE = Path.home() / "docker_apps/blog/photos-originals"
|
||||||
|
STORE.mkdir(parents=True, exist_ok=True)
|
||||||
|
(STORE / f"{slug}.jpg").write_bytes(data)
|
||||||
(OUT / f"{slug}.jpg").write_bytes(compress(data))
|
(OUT / f"{slug}.jpg").write_bytes(compress(data))
|
||||||
|
|
||||||
(OUT / f"{slug}.json").write_text(json.dumps({
|
(OUT / f"{slug}.json").write_text(json.dumps({
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ def main() -> int:
|
|||||||
if size < SKIP_BELOW_BYTES:
|
if size < SKIP_BELOW_BYTES:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
STORE = Path.home() / "docker_apps/blog/photos-originals"
|
||||||
|
STORE.mkdir(parents=True, exist_ok=True)
|
||||||
|
# 压缩覆写仓内文件前原件先进 store(幂等:store 已有同名则跳过)
|
||||||
|
if not (STORE / jpg.name).exists():
|
||||||
|
(STORE / jpg.name).write_bytes(jpg.read_bytes())
|
||||||
img = Image.open(jpg)
|
img = Image.open(jpg)
|
||||||
# EXIF 显式保留:重存默认剥离会抽走图库参数条的构建期数据源
|
# EXIF 显式保留:重存默认剥离会抽走图库参数条的构建期数据源
|
||||||
exif = img.info.get("exif")
|
exif = img.info.get("exif")
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const cards = await Promise.all(
|
|||||||
image: photo.data.image,
|
image: photo.data.image,
|
||||||
exif: formatExif(photo.data.exif),
|
exif: formatExif(photo.data.exif),
|
||||||
fullSrc: full.src,
|
fullSrc: full.src,
|
||||||
|
// 原图走仓外 store(构建时独立 rsync 到 /photos-originals/),git 不背 196M
|
||||||
|
original: `/photos-originals/${photo.id}.jpg`,
|
||||||
w,
|
w,
|
||||||
h,
|
h,
|
||||||
s3: spanFor(w, h, 3),
|
s3: spanFor(w, h, 3),
|
||||||
@@ -84,6 +86,7 @@ const first = cards[0];
|
|||||||
data-title={card.title}
|
data-title={card.title}
|
||||||
data-exif={card.exif}
|
data-exif={card.exif}
|
||||||
data-full={card.fullSrc}
|
data-full={card.fullSrc}
|
||||||
|
data-original={card.original}
|
||||||
aria-label={`查看大图:${card.title}${card.exif ? `(${card.exif})` : ''}`}
|
aria-label={`查看大图:${card.title}${card.exif ? `(${card.exif})` : ''}`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -119,6 +122,10 @@ const first = cards[0];
|
|||||||
<div class="lb-meta">
|
<div class="lb-meta">
|
||||||
<strong id="lb-title">{first?.title}</strong>
|
<strong id="lb-title">{first?.title}</strong>
|
||||||
<span id="lb-exif">{first?.exif}</span>
|
<span id="lb-exif">{first?.exif}</span>
|
||||||
|
<span class="lb-orig">
|
||||||
|
<a id="lb-original" href={first?.original} target="_blank" rel="noopener">原图</a>
|
||||||
|
<a id="lb-download" href={first?.original} download>下载</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="lb-next" class="tap lbnav" type="button" aria-label="下一张(→)">
|
<button id="lb-next" class="tap lbnav" type="button" aria-label="下一张(→)">
|
||||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
@@ -150,6 +157,11 @@ const first = cards[0];
|
|||||||
img.alt = btn.ariaLabel ?? '';
|
img.alt = btn.ariaLabel ?? '';
|
||||||
titleEl.textContent = btn.dataset.title ?? '';
|
titleEl.textContent = btn.dataset.title ?? '';
|
||||||
exifEl.textContent = btn.dataset.exif ?? '';
|
exifEl.textContent = btn.dataset.exif ?? '';
|
||||||
|
const orig = btn.dataset.original ?? '';
|
||||||
|
(document.getElementById('lb-original') as HTMLAnchorElement).href = orig;
|
||||||
|
const dl = document.getElementById('lb-download') as HTMLAnchorElement;
|
||||||
|
dl.href = orig;
|
||||||
|
dl.setAttribute('download', btn.dataset.title ?? 'photo');
|
||||||
if (!reduced.matches) {
|
if (!reduced.matches) {
|
||||||
img.style.opacity = '0';
|
img.style.opacity = '0';
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -315,6 +327,18 @@ const first = cards[0];
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
}
|
}
|
||||||
|
.lb-orig {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-left: var(--space-2);
|
||||||
|
}
|
||||||
|
.lb-orig a {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
.lb-meta {
|
.lb-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -352,6 +376,18 @@ const first = cards[0];
|
|||||||
}
|
}
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.lb-bottom { flex-wrap: wrap; }
|
.lb-bottom { flex-wrap: wrap; }
|
||||||
|
.lb-orig {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-left: var(--space-2);
|
||||||
|
}
|
||||||
|
.lb-orig a {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
.lb-meta { order: 3; flex-basis: 100%; }
|
.lb-meta { order: 3; flex-basis: 100%; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user