PIXEL BEAD STUDIO
把喜欢的图片,
变成一格一格的快乐。
上传图片,选择图纸大小和颜色数量;点击任意像素格,就能单独查看该颜色需要摆放的区域。
LOCAL PATTERN ARCHIVE
历史图纸
历史保存在当前浏览器,最多保留 30 份。
diff --git a/.gitignore b/.gitignore index 622d022..c24f723 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ # next.js /.next/ /.vinext/ -/out/ +# /out is intentionally tracked: it is built locally and deployed by Gitea. # misc .DS_Store diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..805f505 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,25 @@ +# 部署说明 + +本项目在本地 Windows 电脑构建,服务器只运行静态成品。 + +## 本地更新 + +```powershell +npm ci +npm run build +git add . +git commit -m "Update bead pattern web" +git push +``` + +`npm run build` 会重新生成 `out/`。提交前必须包含最新的 `out/`。 + +## 服务器更新 + +```bash +cd /projects/pixel-bead-pattern +git pull +docker compose up -d --build +``` + +服务器构建只把 `out/` 复制到 Nginx 镜像,不安装 npm 依赖,也不编译网页。 diff --git a/Dockerfile b/Dockerfile index a1a4d2a..9856e20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,7 @@ -FROM node:22-alpine AS builder +FROM node:22-alpine WORKDIR /app -COPY package*.json ./ -RUN npm ci --ignore-scripts -COPY . . -RUN npm run build - -FROM node:22-alpine AS runner -WORKDIR /app -ENV NODE_ENV=production -ENV HOST=0.0.0.0 -ENV PORT=3200 -COPY --from=builder /app/package*.json ./ -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/dist ./dist +ENV APP_PORT=3200 +COPY deploy/server.mjs ./server.mjs +COPY out ./public EXPOSE 3200 -CMD ["npm", "run", "start", "--", "--hostname", "0.0.0.0", "--port", "3200"] +CMD ["node", "server.mjs"] diff --git a/deploy/server.mjs b/deploy/server.mjs new file mode 100644 index 0000000..a0570da --- /dev/null +++ b/deploy/server.mjs @@ -0,0 +1,42 @@ +import { createReadStream, existsSync, statSync } from "node:fs"; +import { createServer } from "node:http"; +import { extname, join, normalize } from "node:path"; + +const packagedRoot = join(process.cwd(), "public"); +const root = existsSync(join(packagedRoot, "index.html")) ? packagedRoot : join(process.cwd(), "out"); +const port = Number(process.env.APP_PORT || 3200); +const mimeTypes = { + ".css": "text/css; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".rsc": "text/x-component", + ".svg": "image/svg+xml", + ".webp": "image/webp", +}; + +createServer((request, response) => { + const pathname = decodeURIComponent(new URL(request.url || "/", "http://localhost").pathname); + const relativePath = normalize(pathname).replace(/^([/\\])+/, ""); + let filePath = join(root, relativePath || "index.html"); + + if (!filePath.startsWith(root) || !existsSync(filePath)) filePath = join(root, "index.html"); + if (existsSync(filePath) && statSync(filePath).isDirectory()) filePath = join(filePath, "index.html"); + + if (!existsSync(filePath)) { + response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); + response.end("Not found"); + return; + } + + response.writeHead(200, { + "Content-Type": mimeTypes[extname(filePath)] || "application/octet-stream", + "Cache-Control": filePath.endsWith("index.html") ? "no-cache" : "public, max-age=31536000, immutable", + }); + if (request.method === "HEAD") response.end(); + else createReadStream(filePath).pipe(response); +}).listen(port, "0.0.0.0", () => { + console.log(`Pixel bead pattern web listening on ${port}`); +}); diff --git a/next.config.ts b/next.config.ts index e9ffa30..587a45d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + // Export a browser-only site so the small server only needs to serve files. + output: "export", }; export default nextConfig; diff --git a/out/.assetsignore b/out/.assetsignore new file mode 100644 index 0000000..e737bcb --- /dev/null +++ b/out/.assetsignore @@ -0,0 +1,3 @@ +wrangler.json +.dev.vars +.vite diff --git a/out/.vite/manifest.json b/out/.vite/manifest.json new file mode 100644 index 0000000..6110997 --- /dev/null +++ b/out/.vite/manifest.json @@ -0,0 +1,49 @@ +{ + "_framework-BgSIrAUN.js": { + "file": "_next/static/chunks/framework-BgSIrAUN.js", + "name": "framework", + "imports": [ + "_rolldown-runtime-C60lm6uB.js" + ] + }, + "_rolldown-runtime-C60lm6uB.js": { + "file": "_next/static/chunks/rolldown-runtime-C60lm6uB.js", + "name": "rolldown-runtime" + }, + "app/page.tsx": { + "file": "_next/static/chunks/page-DOJToWWt.js", + "name": "page", + "src": "app/page.tsx", + "isDynamicEntry": true, + "imports": [ + "_rolldown-runtime-C60lm6uB.js", + "_framework-BgSIrAUN.js" + ] + }, + "node_modules/vinext/dist/shims/layout-segment-context.js": { + "file": "_next/static/chunks/layout-segment-context-BAGcnabH.js", + "name": "layout-segment-context", + "src": "node_modules/vinext/dist/shims/layout-segment-context.js", + "isDynamicEntry": true, + "imports": [ + "_rolldown-runtime-C60lm6uB.js", + "virtual:vinext-app-browser-entry", + "_framework-BgSIrAUN.js" + ] + }, + "virtual:vinext-app-browser-entry": { + "file": "_next/static/chunks/index-BdxXUyhE.js", + "name": "index", + "src": "virtual:vinext-app-browser-entry", + "isEntry": true, + "imports": [ + "_rolldown-runtime-C60lm6uB.js", + "_framework-BgSIrAUN.js" + ], + "dynamicImports": [ + "virtual:vinext-app-browser-entry", + "node_modules/vinext/dist/shims/layout-segment-context.js", + "app/page.tsx" + ] + } +} \ No newline at end of file diff --git a/out/404.html b/out/404.html new file mode 100644 index 0000000..6f9cc4a --- /dev/null +++ b/out/404.html @@ -0,0 +1,2 @@ +
PIXEL BEAD STUDIO
上传图片,选择图纸大小和颜色数量;点击任意像素格,就能单独查看该颜色需要摆放的区域。
LOCAL PATTERN ARCHIVE
历史保存在当前浏览器,最多保留 30 份。