change html

This commit is contained in:
2026-08-24 15:37:08 +08:00
parent 3325bc52ef
commit 0487879eee
139 changed files with 9789 additions and 1684 deletions
+359
View File
@@ -0,0 +1,359 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { en } from "../src/i18n/en.ts";
import { fr } from "../src/i18n/fr.ts";
import { ja } from "../src/i18n/ja.ts";
import { zh } from "../src/i18n/zh.ts";
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const outDir = path.join(root, "html");
const copies = { zh, en, ja, fr };
const LANGS = ["zh", "en", "ja", "fr"];
const htmlLang = { zh: "zh-CN", en: "en", ja: "ja", fr: "fr" };
const langLabels = { zh: "中文", en: "English", ja: "日本語", fr: "Français" };
function esc(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function assetPrefix(lang) {
return lang === "zh" ? "" : "../";
}
function pageHref(fromLang, toLang, page) {
const file = page === "home" ? "index.html" : "about.html";
if (fromLang === toLang) return file;
if (fromLang === "zh") return `${toLang}/${file}`;
if (toLang === "zh") return `../${file}`;
return `../${toLang}/${file}`;
}
function productHref(key, lang) {
if (key === "massiveedit" && (lang === "en" || lang === "fr")) {
return "/massiveedit/en/";
}
return `/${key}/`;
}
const ICONS = {
chevron: `<svg width="10" height="10" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6 9l6 6l6-6"/></svg>`,
moon: `<svg class="icon-light" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>`,
sun: `<svg class="icon-dark" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></g></svg>`,
menu: `<svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5h16M4 12h16M4 19h16"/></svg>`,
};
function header(lang, page) {
const L = copies[lang];
const prefix = assetPrefix(lang);
const home = pageHref(lang, lang, "home");
const about = pageHref(lang, lang, "about");
const products = `${home}#products`;
const nav = [
{ label: L.nav.home, href: home, current: page === "home" },
{ label: L.nav.products, href: products, current: false },
{ label: L.nav.about, href: about, current: page === "about" },
];
return `<header class="site-header">
<div class="container site-nav">
<a class="brand" href="${home}" aria-label="${esc(L.brand.ariaHome)}">
<img class="brand-mark" src="${prefix}images/logo.svg" alt="" />
<div class="brand-copy">
<strong>${esc(L.brand.strong)}</strong><span>|</span><span>${esc(L.brand.suffix)}</span>
</div>
</a>
<nav class="nav-links" aria-label="${esc(L.lang.menuLabel)}">
${nav
.map(
(item) =>
`<a class="nav-link${item.current ? " current" : ""}" href="${item.href}">${esc(item.label)}</a>`,
)
.join("\n ")}
</nav>
<div class="header-tools">
<details class="lang-switch">
<summary class="locale-pill" aria-label="${esc(L.lang.menuLabel)}">
<span>${langLabels[lang]}</span>
${ICONS.chevron}
</summary>
<div class="lang-switch-panel">
${LANGS.map((code) => {
const href = pageHref(lang, code, page);
const hreflang = htmlLang[code];
return `<a${code === lang ? ' class="current"' : ""} href="${href}" hreflang="${hreflang}" lang="${hreflang}">${langLabels[code]}</a>`;
}).join("\n ")}
</div>
</details>
<button class="theme-toggle-btn" type="button" data-theme-toggle aria-label="${esc(L.theme.toggle)}" title="${esc(L.theme.toggle)}">
${ICONS.moon}
${ICONS.sun}
</button>
</div>
<button class="menu-toggle" type="button" data-menu-toggle aria-expanded="false" aria-label="Menu">
${ICONS.menu}
</button>
</div>
<div class="container mobile-menu" data-mobile-menu>
${nav
.map(
(item) =>
`<a${item.current ? ' class="current"' : ""} href="${item.href}">${esc(item.label)}</a>`,
)
.join("\n ")}
<div class="mobile-lang">
<p>${esc(L.lang.menuLabel)}</p>
${LANGS.map((code) => {
const href = pageHref(lang, code, page);
return `<a${code === lang ? ' class="current"' : ""} href="${href}">${langLabels[code]}</a>`;
}).join("\n ")}
</div>
<button class="theme-toggle-btn" type="button" data-theme-toggle aria-label="${esc(L.theme.toggle)}">
${ICONS.moon}
${ICONS.sun}
</button>
</div>
</header>`;
}
function footer(lang) {
const L = copies[lang];
return `<footer class="site-footer">
<div class="container footer-main">
<div>
<h3>${esc(L.footer.company)}</h3>
<p>${esc(L.footer.addressLine)}</p>
<p>${esc(L.footer.emailLine)}</p>
</div>
<div>
<h3>${esc(L.footer.colAbout)}</h3>
<div class="footer-links">
<a href="${pageHref(lang, lang, "about")}">${esc(L.footer.linkAbout)}</a>
</div>
</div>
<div>
<h3>${esc(L.footer.colProducts)}</h3>
<div class="footer-links">
<a href="${productHref("smartbuddy", lang)}">${esc(L.footer.linkBuddy)}</a>
<a href="${productHref("smartinput", lang)}">${esc(L.footer.linkInput)}</a>
<a href="${productHref("smarthanzi", lang)}">${esc(L.footer.linkHanzi)}</a>
<a href="${productHref("massiveedit", lang)}">${esc(L.footer.linkEdit)}</a>
</div>
</div>
</div>
<div class="container footer-bottom">
<p class="footer-legal-line">
${esc(L.footer.copyrightMain)}
<a class="footer-icp-link" href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">${esc(L.footer.icpNumber)}</a>
</p>
<p class="footer-trademark">${esc(L.footer.trademarkNotice)}</p>
</div>
</footer>`;
}
function homeMain(lang) {
const L = copies[lang];
const products = [
{
key: "smartbuddy",
name: L.home.productBuddyName,
en: L.home.productBuddyEn,
body: L.home.productBuddyBody,
cta: L.home.productBuddyCta,
},
{
key: "smartinput",
name: L.home.productInputName,
en: L.home.productInputEn,
body: L.home.productInputBody,
cta: L.home.productInputCta,
},
{
key: "smarthanzi",
name: L.home.productHanziName,
en: L.home.productHanziEn,
body: L.home.productHanziBody,
cta: L.home.productHanziCta,
},
{
key: "massiveedit",
name: L.home.productEditName,
en: L.home.productEditEn,
body: L.home.productEditBody,
cta: L.home.productEditCta,
},
];
return `<section class="hero home-hero">
<div class="container">
<p class="home-kicker">${esc(L.brand.strong)} · ${esc(L.brand.suffix)}</p>
<h1 class="hero-title">${esc(L.home.heroTitle)}</h1>
<p class="home-lead">${esc(L.home.heroSubtitle)}</p>
<div class="hero-actions">
<a class="btn btn-primary" href="#products">${esc(L.home.ctaProducts)}</a>
<a class="btn btn-outline" href="${pageHref(lang, lang, "about")}">${esc(L.home.ctaAbout)}</a>
</div>
</div>
</section>
<section class="section compact" id="products">
<div class="container">
<div class="section-head">
<h2 class="section-title">${esc(L.home.productsTitle)}</h2>
<p class="section-copy">${esc(L.home.productsLead)}</p>
</div>
<div class="product-grid">
${products
.map(
(product) => `<article class="feature-card product-card">
<p class="product-kicker">${esc(product.en)}</p>
<h3>${esc(product.name)}</h3>
<p>${esc(product.body)}</p>
<a class="mini-link" href="${productHref(product.key, lang)}">${esc(product.cta)}</a>
</article>`,
)
.join("\n ")}
</div>
</div>
</section>
<section class="section compact" id="company">
<div class="container grid-two company-contact">
<article class="feature-card">
<h3>${esc(L.home.companyTitle)}</h3>
<p>${esc(L.home.companyBody)}</p>
<a class="mini-link" href="${pageHref(lang, lang, "about")}">${esc(L.home.ctaCompany)}</a>
</article>
<article class="feature-card">
<h3>${esc(L.home.contactTitle)}</h3>
<p>${esc(L.home.contactBody)}</p>
<a class="mini-link" href="mailto:liutingchao@fivequestions.cn">${esc(L.home.ctaContact)}</a>
</article>
</div>
</section>`;
}
function aboutMain(lang) {
const L = copies[lang];
return `<section class="hero home-hero about-hero">
<div class="container">
<p class="home-kicker">${esc(L.brand.strong)} · ${esc(L.brand.suffix)}</p>
<h1 class="hero-title">${esc(L.about.heroTitle)}</h1>
<p class="home-lead">${esc(L.about.heroLead)}</p>
</div>
</section>
<section class="section compact">
<div class="container about-stack">
<article class="feature-card">
<h2>${esc(L.about.visionTitle)}</h2>
<p>${esc(L.about.visionBody)}</p>
</article>
<article class="feature-card">
<h2>${esc(L.about.missionTitle)}</h2>
<p>${esc(L.about.missionBody)}</p>
</article>
<article class="feature-card">
<h2>${esc(L.about.storyTitle)}</h2>
<p>${esc(L.about.storyBody)}</p>
</article>
<article class="feature-card">
<h2>${esc(L.about.teamTitle)}</h2>
<p>${esc(L.about.teamBody)}</p>
</article>
</div>
</section>
<section class="section compact">
<div class="container">
<div class="about-products">
<a href="${productHref("smartbuddy", lang)}">${esc(L.footer.linkBuddy)}</a>
<a href="${productHref("smartinput", lang)}">${esc(L.footer.linkInput)}</a>
<a href="${productHref("smarthanzi", lang)}">${esc(L.footer.linkHanzi)}</a>
<a href="${productHref("massiveedit", lang)}">${esc(L.footer.linkEdit)}</a>
</div>
</div>
</section>`;
}
function documentFor(lang, page) {
const L = copies[lang];
const prefix = assetPrefix(lang);
const meta = page === "home" ? L.home : L.about;
const alternates = LANGS.map((code) => {
const href =
code === "zh"
? lang === "zh"
? page === "home"
? "index.html"
: "about.html"
: `../${page === "home" ? "index.html" : "about.html"}`
: lang === "zh"
? `${code}/${page === "home" ? "index.html" : "about.html"}`
: code === lang
? page === "home"
? "index.html"
: "about.html"
: `../${code}/${page === "home" ? "index.html" : "about.html"}`;
return `<link rel="alternate" hreflang="${htmlLang[code]}" href="${href}" />`;
}).join("\n ");
const defaultHref =
lang === "zh"
? page === "home"
? "index.html"
: "about.html"
: `../${page === "home" ? "index.html" : "about.html"}`;
return `<!DOCTYPE html>
<html lang="${htmlLang[lang]}" data-theme="light" class="site-lang-${lang}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${esc(meta.metaTitle)}</title>
<meta name="description" content="${esc(meta.metaDescription)}" />
<link rel="icon" type="image/svg+xml" href="${prefix}images/logo.svg" />
${alternates}
<link rel="alternate" hreflang="x-default" href="${defaultHref}" />
<link rel="stylesheet" href="${prefix}css/site.css" />
<script>
(function () {
try {
var t = localStorage.getItem("theme");
var dark =
t === "dark" ||
(!t && window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
} catch (_) {}
})();
</script>
</head>
<body>
<div class="page-shell bg-wash">
${header(lang, page)}
<main>
${page === "home" ? homeMain(lang) : aboutMain(lang)}
</main>
${footer(lang)}
</div>
<script src="${prefix}js/site.js"></script>
</body>
</html>
`;
}
function writePage(lang, page) {
const dir = lang === "zh" ? outDir : path.join(outDir, lang);
fs.mkdirSync(dir, { recursive: true });
const file = page === "home" ? "index.html" : "about.html";
fs.writeFileSync(path.join(dir, file), documentFor(lang, page));
}
for (const lang of LANGS) {
writePage(lang, "home");
writePage(lang, "about");
}
console.log("Wrote static portal pages to html/");
+115
View File
@@ -0,0 +1,115 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const PRODUCT_DIRS = ['smartbuddy', 'smartinput', 'smarthanzi', 'massiveedit', 'html'];
const SKIP_NAMES = new Set([
'.DS_Store',
'.texpadtmp',
'smartbuddy_readme',
]);
const SKIP_EXT = new Set([
'.tex',
'.aux',
'.log',
'.toc',
'.out',
'.synctex.gz',
]);
const MIME = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.woff2': 'font/woff2',
'.txt': 'text/plain; charset=utf-8',
'.md': 'text/plain; charset=utf-8',
'.apk': 'application/vnd.android.package-archive',
'.deb': 'application/vnd.debian.binary-package',
'.pkg': 'application/octet-stream',
'.exe': 'application/octet-stream',
'.zip': 'application/zip',
};
export function shouldCopy(src) {
const base = path.basename(src);
if (SKIP_NAMES.has(base)) return false;
if (base.startsWith('.')) return false;
if (SKIP_EXT.has(path.extname(base))) return false;
return true;
}
export function copyProductSites(destRoot, sourceRoot = process.cwd()) {
for (const dir of PRODUCT_DIRS) {
const from = path.join(sourceRoot, dir);
if (!fs.existsSync(from)) continue;
fs.cpSync(from, path.join(destRoot, dir), {
recursive: true,
filter: shouldCopy,
});
}
}
function resolveProductFile(root, urlPath) {
const clean = urlPath.split('?')[0];
const product = PRODUCT_DIRS.find(
(name) => clean === `/${name}` || clean === `/${name}/` || clean.startsWith(`/${name}/`),
);
if (!product) return null;
let rel = clean.slice(product.length + 2);
if (!rel || rel.endsWith('/')) rel = `${rel}index.html`;
const file = path.join(root, product, rel);
if (fs.existsSync(file) && fs.statSync(file).isFile()) return file;
const asIndex = path.join(root, product, rel, 'index.html');
if (fs.existsSync(asIndex) && fs.statSync(asIndex).isFile()) return asIndex;
return null;
}
function serveProduct(root, req, res, next) {
const file = resolveProductFile(root, req.url ?? '');
if (!file) return next();
res.statusCode = 200;
res.setHeader('Content-Type', MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream');
fs.createReadStream(file).pipe(res);
}
export function productSites() {
const root = process.cwd();
return {
name: 'product-sites',
hooks: {
'astro:config:setup'({ updateConfig }) {
updateConfig({
vite: {
plugins: [
{
name: 'serve-product-sites',
configureServer(server) {
server.middlewares.use((req, res, next) => serveProduct(root, req, res, next));
},
},
],
},
});
},
'astro:build:done'({ dir }) {
copyProductSites(fileURLToPath(dir));
},
},
};
}