feat: 沙香科技官网 v2.2 基线(深色科技风/双语/局域网+公网双部署)

This commit is contained in:
dafe
2026-11-02 14:57:08 +08:00
commit 924bca9f98
91 changed files with 1632 additions and 0 deletions

25
tools/deploy.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# 官网统一部署脚本:bump 静态资源版本号(击穿浏览器缓存)+ 同步双服务器
# 用法: bash tools/deploy.sh
set -euo pipefail
cd "$(dirname "$0")/.."
STAMP=$(date "+%Y%m%d%H%M")
# bump 资源版本参数:HTML 更新(no-cache)会带新 URL,旧缓存自然失效
python3 - "$STAMP" <<'PYEOF'
import re, sys
stamp = sys.argv[1]
path = 'index.html'
html = open(path, encoding='utf-8').read()
# 正则一次性规范化:无论已有无版本参数,统一替换为当前时间戳
html = re.sub(r'assets/css/style\.css(?:\?v=\d+)?\d*', 'assets/css/style.css?v=' + stamp, html)
html = re.sub(r'assets/js/main\.js(?:\?v=\d+)?\d*', 'assets/js/main.js?v=' + stamp, html)
open(path, 'w', encoding='utf-8').write(html)
PYEOF
echo "[deploy] 版本号 ${STAMP},同步公网 + 局域网"
# 注意:assets 不带尾斜杠(带斜杠会把目录拍平,曾导致整站 404)
rsync -a --delete index.html assets ubuntu3:/var/www/shashann-homepage/
rsync -a --delete index.html assets ubuntu:/home/ubuntu/homepage-site/
echo "[deploy] 完成: https://www.shashann.com/ 与 https://192.168.51.66/homepage/"

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
"""
生成 contact 公共邮箱头像:品牌深底 + 青→红渐变 @ 符号
视觉规范与官网一致(assets/css/style.css 的 --bg/--cyan/--brand)
用法: uv run --with pillow python3 tools/gen_contact_avatar.py
"""
from PIL import Image, ImageDraw, ImageFilter, ImageFont
SIZE = 512
BG = (7, 11, 22) # 官网 --bg-1
CYAN = (76, 195, 255) # 官网 --cyan
RED = (255, 61, 90) # 官网 --brand
OUT = 'assets/brand/contact-avatar.png'
# ---------- 背景:深底 + 左上青辉光 / 右下红辉光 ----------
bg = Image.new('RGB', (SIZE, SIZE), BG)
def glow_blob(box, blur):
blob = Image.new('L', (SIZE, SIZE), 0)
ImageDraw.Draw(blob).ellipse(box, fill=80)
return blob.filter(ImageFilter.GaussianBlur(blur))
bg.paste(Image.new('RGB', (SIZE, SIZE), CYAN), (0, 0), glow_blob((-220, -220, 240, 240), 100))
bg.paste(Image.new('RGB', (SIZE, SIZE), RED), (0, 0), glow_blob((SIZE - 240, SIZE - 240, SIZE + 220, SIZE + 220), 100))
# ---------- 对角渐变图(青 → 红),用作 @ 的填充色 ----------
c1, c2 = CYAN, RED
span = 2 * (SIZE - 1)
grad = Image.new('RGB', (SIZE, SIZE))
grad.putdata([
(
round(c1[0] + (c2[0] - c1[0]) * (x + y) / span),
round(c1[1] + (c2[1] - c1[1]) * (x + y) / span),
round(c1[2] + (c2[2] - c1[2]) * (x + y) / span),
)
for y in range(SIZE) for x in range(SIZE)
])
# ---------- 渲染 @ 文字 mask ----------
# macOS 西文字体按优先级找,@ 字形各字体差异不大
FONT_CANDIDATES = [
'/System/Library/Fonts/SFNS.ttf',
'/System/Library/Fonts/SFNSText.ttf',
'/System/Library/Fonts/Helvetica.ttc',
]
font_path = next((p for p in FONT_CANDIDATES if __import__('os').path.exists(p)), None)
if not font_path:
raise SystemExit('找不到可用系统字体: ' + ', '.join(FONT_CANDIDATES))
font = ImageFont.truetype(font_path, 400)
mask = Image.new('L', (SIZE, SIZE), 0)
ImageDraw.Draw(mask).text((SIZE // 2, SIZE // 2), '@', font=font, fill=255, anchor='mm')
# 自检:@ 必须真的渲染出来了,否则静默输出空底图
ink = sum(1 for v in mask.getdata() if v > 0)
ratio = ink / (SIZE * SIZE)
if not 0.05 < ratio < 0.45:
raise SystemExit(f'@ 渲染异常,墨迹占比 {ratio:.1%}(预期 5%~45%),检查字体 {font_path}')
bg.paste(grad, (0, 0), mask)
import os
os.makedirs(os.path.dirname(OUT), exist_ok=True)
bg.save(OUT, 'PNG', optimize=True)
print(f'完成: {OUT} {SIZE}x{SIZE} @{ratio:.0%} 字体={font_path}')

39
tools/make_favicon.py Normal file
View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""
从反白 logo 提取左侧图形生成 favicon(180x180 PNG):
深色圆角底(与站内卡片底色一致)+ 图形居中占 72%
标签栏明暗模式均以深色块呈现,反白图形恒清晰。
用法: uv run --with pillow python3 tools/make_favicon.py
"""
from PIL import Image, ImageDraw
SRC = 'assets/img/logo-light.png'
OUT = 'assets/img/favicon.png'
SIZE = 180
PAD_GRAPHIC = 0.72 # 图形占画布比例
# logo 左侧图形实测内容段(x 投影分析):x[47:224] y[44:260],取 bbox 略收边
CROP = (47, 44, 224, 260)
BG = (7, 11, 22) # 站内 --bg-1
RADIUS = 38
graphic = Image.open(SRC).convert('RGBA').crop(CROP)
canvas = Image.new('RGBA', (SIZE, SIZE), (0, 0, 0, 0))
# 圆角深色底
mask = Image.new('L', (SIZE, SIZE), 0)
ImageDraw.Draw(mask).rounded_rectangle((0, 0, SIZE, SIZE), RADIUS, fill=255)
canvas.paste(Image.new('RGBA', (SIZE, SIZE), BG + (255,)), (0, 0), mask)
# 图形等比缩放到 72% 高度内居中
target = round(SIZE * PAD_GRAPHIC)
scale = min(target / graphic.width, target / graphic.height)
graphic = graphic.resize((round(graphic.width * scale), round(graphic.height * scale)), Image.LANCZOS)
canvas.alpha_composite(graphic, ((SIZE - graphic.width) // 2, (SIZE - graphic.height) // 2))
# 自检:中心应有不透明图形像素,四角透明
px = canvas.load()
assert px[SIZE // 2, SIZE // 2][3] > 0, '中心无图形'
assert px[2, 2][3] == 0 and px[SIZE - 3, SIZE - 3][3] == 0, '圆角未生效'
canvas.save(OUT, 'PNG', optimize=True)
print(f'完成: {OUT} {SIZE}x{SIZE},图形 {graphic.size}')

71
tools/make_logo_light.py Normal file
View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""
生成深色站反白版 logo(logo-light.png):
1. flood-fill 镂空深蓝底(容差 84 = v1 验证成功参数,能吃掉 #000020 底色)
2. 不透明内容亮度反转 L' = 1 - L(黑字→白字,红/青/绿提亮,色相保留)
深色页黑字看不清、白底块又突兀,反白是深色站标准解法。
用法: uv run --with pillow python3 tools/make_logo_light.py
"""
import colorsys
from collections import deque
from PIL import Image
SRC = 'assets/img/logo.png'
OUT = 'assets/img/logo-light.png'
TOL = 84 # 曼哈顿距离容差(v1 实测:清除 83%、红色主体完整保留)
im = Image.open(SRC).convert('RGBA')
w, h = im.size
px = im.load()
# --- flood-fill 清底 ---
corners = [px[0, 0], px[w-1, 0], px[0, h-1], px[w-1, h-1]]
bg = tuple(sum(c[i] for c in corners) // 4 for i in range(3))
def is_bg(p):
if p[3] < 10:
return True
return abs(p[0]-bg[0]) + abs(p[1]-bg[1]) + abs(p[2]-bg[2]) <= TOL
seen = [[False]*w for _ in range(h)]
q = deque()
for x in range(w):
q.extend(((x, 0), (x, h-1)))
for y in range(h):
q.extend(((0, y), (w-1, y)))
cleared = 0
while q:
x, y = q.popleft()
if x < 0 or x >= w or y < 0 or y >= h or seen[y][x]:
continue
seen[y][x] = True
p = px[x, y]
if not is_bg(p):
continue
px[x, y] = (0, 0, 0, 0)
cleared += 1
q.extend(((x+1, y), (x-1, y), (x, y+1), (x, y-1)))
ratio = cleared / (w * h)
assert 0.10 < ratio < 0.92, f'镂空异常: 清除 {ratio:.0%}'
# --- 暗部提亮、亮部保留(logo 含 ~20% 浅色元素,全量反转会把它们变黑) ---
# L' = max(L, 1-L):暗像素翻到亮侧(黑→白),亮像素不动,色相全保留
opaque = [px[x, y] for y in range(h) for x in range(w) if px[x, y][3] > 128]
assert opaque, '镂空后内容为空'
for y in range(h):
for x in range(w):
r, g, b, a = px[x, y]
if a == 0:
continue
hh, ll, ss = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
r2, g2, b2 = colorsys.hls_to_rgb(hh, max(ll, 1 - ll), ss)
px[x, y] = (round(r2 * 255), round(g2 * 255), round(b2 * 255), a)
im.save(OUT, 'PNG', optimize=True)
# 验证:反白后所有不透明像素亮度下限
final = [px[x, y] for y in range(h) for x in range(w) if px[x, y][3] > 128]
dark = sum(1 for p in final if 0.299*p[0]+0.587*p[1]+0.114*p[2] < 90) / len(final)
print(f'完成: {OUT} 镂空 {ratio:.0%},暗部像素剩 {dark:.1%}(应为 0)')

119
tools/process_images.py Normal file
View File

@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
"""
从 PPT 提取的原始素材生成网站用图:
1. logo 与 7 张黑底产品图:边缘 flood-fill 镂空(背景变透明,适配深色网页)
2. 水培架图背景复杂(39% 均匀度),不镂空,走统一描边容器展示
3. 截图/场景/照片转 WebP 压缩;二维码保持 PNG 无损
用法: uv run --with pillow python3 tools/process_images.py <pptx解压目录>/ppt/media <输出目录>
"""
import sys
from collections import deque
from pathlib import Path
from PIL import Image
src = Path(sys.argv[1])
out = Path(sys.argv[2])
out.mkdir(parents=True, exist_ok=True)
RENAME = {
'image2.png': 'logo.png', # 原样无损输出(见 PASS_THROUGH)
'image1.jpeg': 'hero-bg.webp', # v2 起不再做 hero 背景,仅作 og:image
'image3.png': 'about-main.webp', # v2 起弃用(PPT 拼贴图有白缝)
'image4.png': 'about-base.webp',
'image5.png': 'collar-sg03-mini.webp',
'image13.png': 'collar-sg03.webp',
'image10.png': 'ear-tag-st01.webp',
'image11.png': 'base-station-bg01.webp',
'image12.png': 'grazing-scene-1.webp',
'image18.png': 'grazing-scene-2.webp',
'image19.png': 'hydroponics-hs01.webp',
'image20.png': 'planting-scene.webp',
'image31.png': 'terminal-bb03.webp',
'image32.png': 'uwb-tag-sl01.webp',
'image33.png': 'bt-tag-st02.webp',
'image29.png': 'asset-scene-1.webp',
'image30.jpeg': 'asset-scene-2.webp',
'image34.png': 'platform-grazing-1.webp',
'image35.png': 'platform-grazing-app.webp',
'image36.png': 'platform-planting-1.webp',
'image37.png': 'platform-planting-2.webp',
'image38.png': 'platform-asset-1.webp',
'image39.png': 'platform-asset-2.webp',
'image40.png': 'custom-liangchang-1.webp',
'image41.png': 'custom-liangchang-2.webp',
'image42.png': 'custom-simulation.webp',
'image43.png': 'custom-agent.webp',
'image44.png': 'qrcode.png',
}
# 这些图做边缘 flood-fill 镂空(检测过:边缘色 96-100% 均匀的纯黑底)
# 注意:logo 不镂空——其底色 #000020 与四角检测色差超容差,v2 曾清成斑驳残留导致看不清
CUTOUT = {
'image5.png', 'image13.png', 'image10.png', 'image11.png',
'image31.png', 'image32.png', 'image33.png',
}
CUTOUT_TOL = 30 # 曼哈顿距离阈值,宁小勿大:黑底纯度高,产品暗部 (#20 系) 距离约 96 不会被误清
def cutout_bg(path: Path, tol: int) -> Image.Image:
"""从画布四边缘 flood-fill 清除均匀底色,只清与边缘连通的背景,不动被包围的内容"""
im = Image.open(path).convert('RGBA')
w, h = im.size
px = im.load()
corners = [px[0, 0], px[w-1, 0], px[0, h-1], px[w-1, h-1]]
bg = tuple(sum(c[i] for c in corners) // 4 for i in range(3))
def is_bg(p):
if p[3] < 10:
return True # 原本就是透明的像素直接视为背景
return abs(p[0]-bg[0]) + abs(p[1]-bg[1]) + abs(p[2]-bg[2]) <= tol
seen = [[False]*w for _ in range(h)]
q = deque()
for x in range(w):
q.extend(((x, 0), (x, h-1)))
for y in range(h):
q.extend(((0, y), (w-1, y)))
cleared = 0
while q:
x, y = q.popleft()
if x < 0 or x >= w or y < 0 or y >= h or seen[y][x]:
continue
seen[y][x] = True
p = px[x, y]
if not is_bg(p):
continue
px[x, y] = (0, 0, 0, 0)
cleared += 1
q.extend(((x+1, y), (x-1, y), (x, y+1), (x, y-1)))
ratio = cleared / (w * h)
# 镂空比例健全性检查:太低=没清干净,太高=误伤内容
if not 0.10 < ratio < 0.92:
raise SystemExit(f'镂空异常 {path.name}: 清除 {ratio:.0%},需人工检查')
return im
# 原样无损拷贝:logo 色块文字经不起有损压缩,二维码必须无损
PASS_THROUGH = {'image2.png', 'image44.png'}
for src_name, dst_name in RENAME.items():
src_path = src / src_name
if not src_path.exists():
print(f' 跳过缺失: {src_name}')
continue
dst_path = out / dst_name
if src_name in PASS_THROUGH:
Image.open(src_path).save(dst_path)
elif src_name in CUTOUT:
im = cutout_bg(src_path, CUTOUT_TOL)
# 镂空图必须带 alpha,webp 无损存 alpha(有损会弄脏半透明边缘)
im.save(dst_path, 'WEBP', quality=100, lossless=True, method=6)
else:
im = Image.open(src_path).convert('RGB')
if im.width > 1600:
im = im.resize((1600, round(im.height * 1600 / im.width)), Image.LANCZOS)
im.save(dst_path, 'WEBP', quality=82, method=6)
kb = dst_path.stat().st_size // 1024
tag = ' [镂空]' if src_name in CUTOUT else ''
print(f' {src_name:14s} -> {dst_name:28s} {kb}KB{tag}')
print(f'完成,共 {len(RENAME)} 张 -> {out}')

View File

@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
"""
处理用户补充的新素材(assets/img 内):
- 7 张透明底产品图 p-*:无损 WebP 保留 alpha(已是镂空,无需再处理)
- p-hs01 背景复杂不透明:有损压缩(页面走 framed 描边容器)
- 12 张照片 JPG:有损 WebP(宽度 >1600 的缩到 1600)
- qr-wechat.png 二维码:不处理,HTML 直接引用原 PNG
原图不删除(用户提供的源文件)。用法: uv run --with pillow python3 tools/process_new_assets.py
"""
from pathlib import Path
from PIL import Image
IMG = Path('assets/img')
# 已检测为真透明的产品图(透明像素 37%~88%)
ALPHA_PRODUCTS = [
'p-bb03.png', 'p-bg01.png', 'p-sg03.png', 'p-sg03mini.png',
'p-sl01.png', 'p-st01.png', 'p-st02.png',
]
# 有损压缩的照片/截图
PHOTOS = [
'p-hs01.png', # 背景复杂不透明,同照片处理
'ai-agent.jpg', 'app-livestock.jpg', 'building.jpg', 'cctv.jpg',
'digital-twin.jpg', 'factory-line.jpg', 'factory.jpg', 'goat.jpg',
'hero-grazing.jpg', 'hydro-class.jpg', 'liangchang.jpg',
'machine-install.jpg', 'platform-asset.jpg', 'platform-hydro.jpg',
'platform-pasture.jpg',
]
for name in ALPHA_PRODUCTS:
src = IMG / name
dst = IMG / (name[:-4] + '.webp')
im = Image.open(src).convert('RGBA')
# 自检:透明像素占比必须显著,否则说明误判为透明图
trans = sum(1 for p in im.getdata() if p[3] < 250) / (im.width * im.height)
assert trans > 0.2, f'{name} 透明像素仅 {trans:.0%},不符合透明底预期'
im.save(dst, 'WEBP', lossless=True, method=6)
print(f'{name:18s} -> {dst.name:22s} 透明{trans:.0%} {dst.stat().st_size // 1024}KB')
for name in PHOTOS:
src = IMG / name
dst = IMG / (name[:-4] + '.webp')
im = Image.open(src).convert('RGB')
if im.width > 1600:
im = im.resize((1600, round(im.height * 1600 / im.width)), Image.LANCZOS)
im.save(dst, 'WEBP', quality=82, method=6)
print(f'{name:18s} -> {dst.name:22s} {im.width}x{im.height} {dst.stat().st_size // 1024}KB')
print('完成')