chore: init ledger project with docs and gitignore
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
web/dist/
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
ledger.local.json
|
||||||
67
README.md
67
README.md
@@ -1,3 +1,66 @@
|
|||||||
# ledger
|
# Node Ledger
|
||||||
|
|
||||||
记账项目
|
一个基于 Node.js 的轻量记账项目,包含:
|
||||||
|
|
||||||
|
- 命令行记账(新增、删除、列表、汇总)
|
||||||
|
- HTTP API(增删改查、汇总、分类统计)
|
||||||
|
- 前端页面(Vite 构建后由后端静态托管)
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- npm 10+
|
||||||
|
|
||||||
|
## 本地运行
|
||||||
|
|
||||||
|
### 1. 安装前端依赖并构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node index.js serve 3001
|
||||||
|
```
|
||||||
|
|
||||||
|
打开 `http://localhost:3001`。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 新增一条记录
|
||||||
|
node index.js add 50 午饭
|
||||||
|
|
||||||
|
# 查看记录(可按日期)
|
||||||
|
node index.js list
|
||||||
|
node index.js list --date 2026-05-01
|
||||||
|
|
||||||
|
# 汇总(可按日期)
|
||||||
|
node index.js sum
|
||||||
|
node index.js sum --date 2026-05-01
|
||||||
|
|
||||||
|
# 删除
|
||||||
|
node index.js remove <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 示例
|
||||||
|
|
||||||
|
- `GET /api/entries`
|
||||||
|
- `POST /api/entries`
|
||||||
|
- `PUT /api/entries/:id`
|
||||||
|
- `DELETE /api/entries/:id`
|
||||||
|
- `GET /api/summary`
|
||||||
|
- `GET /api/stats/categories`
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
Ubuntu 服务器建议使用:
|
||||||
|
|
||||||
|
- `pm2` 守护 Node 进程
|
||||||
|
- `nginx` 反向代理
|
||||||
|
- `certbot` 配置 HTTPS
|
||||||
|
|||||||
352
index.js
Normal file
352
index.js
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import http from "node:http";
|
||||||
|
import { URL } from "node:url";
|
||||||
|
import path from "node:path";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
|
||||||
|
const [, , cmd, ...args] = process.argv;
|
||||||
|
const DB_FILE = "./ledger.json";
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
const text = await readFile(DB_FILE, "utf-8");
|
||||||
|
return JSON.parse(text || "[]");
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(data) {
|
||||||
|
await writeFile(DB_FILE, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDateArg(argv) {
|
||||||
|
const dateFlagIdx = argv.indexOf("--date");
|
||||||
|
if (dateFlagIdx === -1) return null;
|
||||||
|
return argv[dateFlagIdx + 1] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidDateString(dateText) {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateText)) return false;
|
||||||
|
const [y, m, d] = dateText.split("-").map(Number);
|
||||||
|
const dt = new Date(y, m - 1, d);
|
||||||
|
return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDayText(isoTime) {
|
||||||
|
const d = new Date(isoTime);
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log("用法:");
|
||||||
|
console.log(" node index.js add <金额> <备注>");
|
||||||
|
console.log(" node index.js list [--date YYYY-MM-DD]");
|
||||||
|
console.log(" node index.js sum [--date YYYY-MM-DD]");
|
||||||
|
console.log(" node index.js remove <id>");
|
||||||
|
console.log(" node index.js serve [port]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function withDateFilter(data, dateText) {
|
||||||
|
if (!dateText) return data;
|
||||||
|
return data.filter((x) => toDayText(x.createdAt) === dateText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters(data, url) {
|
||||||
|
const dateText = url.searchParams.get("date");
|
||||||
|
const q = (url.searchParams.get("q") || "").trim().toLowerCase();
|
||||||
|
const type = url.searchParams.get("type");
|
||||||
|
const category = (url.searchParams.get("category") || "").trim();
|
||||||
|
|
||||||
|
let selected = dateText ? withDateFilter(data, dateText) : data;
|
||||||
|
if (type === "income") selected = selected.filter((x) => x.amount >= 0);
|
||||||
|
if (type === "expense") selected = selected.filter((x) => x.amount < 0);
|
||||||
|
if (category) selected = selected.filter((x) => x.category === category);
|
||||||
|
if (q) selected = selected.filter((x) => x.note.toLowerCase().includes(q));
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonBody(req) {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of req) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
||||||
|
return raw ? JSON.parse(raw) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendJson(res, statusCode, payload) {
|
||||||
|
res.writeHead(statusCode, {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentType(filePath) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
if (ext === ".html") return "text/html; charset=utf-8";
|
||||||
|
if (ext === ".js") return "application/javascript; charset=utf-8";
|
||||||
|
if (ext === ".css") return "text/css; charset=utf-8";
|
||||||
|
if (ext === ".json") return "application/json; charset=utf-8";
|
||||||
|
if (ext === ".svg") return "image/svg+xml";
|
||||||
|
if (ext === ".png") return "image/png";
|
||||||
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
||||||
|
if (ext === ".ico") return "image/x-icon";
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveStaticFile(res, pathname) {
|
||||||
|
const distDir = path.resolve("./web/dist");
|
||||||
|
const safePath = pathname === "/" ? "/index.html" : pathname;
|
||||||
|
const filePath = path.resolve(path.join(distDir, `.${safePath}`));
|
||||||
|
if (!filePath.startsWith(distDir)) {
|
||||||
|
sendJson(res, 403, { error: "forbidden" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetPath = existsSync(filePath) ? filePath : path.join(distDir, "index.html");
|
||||||
|
if (!existsSync(targetPath)) {
|
||||||
|
sendJson(res, 404, { error: "前端资源不存在,请先执行: cd web && npm run build" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await readFile(targetPath);
|
||||||
|
res.writeHead(200, { "Content-Type": getContentType(targetPath) });
|
||||||
|
res.end(content);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startApiServer(port = 3001) {
|
||||||
|
const server = http.createServer(async (req, res) => {
|
||||||
|
if (!req.url || !req.method) {
|
||||||
|
sendJson(res, 400, { error: "bad request" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
res.writeHead(204, {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(req.url, `http://localhost:${port}`);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (req.method === "GET" && pathname === "/api/entries") {
|
||||||
|
const dateText = url.searchParams.get("date");
|
||||||
|
if (dateText && !isValidDateString(dateText)) {
|
||||||
|
sendJson(res, 400, { error: "日期格式错误,请使用 YYYY-MM-DD" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await load();
|
||||||
|
sendJson(res, 200, { data: applyFilters(data, url) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/summary") {
|
||||||
|
const dateText = url.searchParams.get("date");
|
||||||
|
if (dateText && !isValidDateString(dateText)) {
|
||||||
|
sendJson(res, 400, { error: "日期格式错误,请使用 YYYY-MM-DD" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await load();
|
||||||
|
const selected = applyFilters(data, url);
|
||||||
|
const total = selected.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
const income = selected
|
||||||
|
.filter((x) => x.amount >= 0)
|
||||||
|
.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
const expense = selected
|
||||||
|
.filter((x) => x.amount < 0)
|
||||||
|
.reduce((sum, item) => sum + item.amount, 0);
|
||||||
|
sendJson(res, 200, { total, count: selected.length, income, expense });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && pathname === "/api/stats/categories") {
|
||||||
|
const data = applyFilters(await load(), url);
|
||||||
|
const bucket = {};
|
||||||
|
for (const item of data) {
|
||||||
|
const key = item.category || "未分类";
|
||||||
|
bucket[key] = (bucket[key] || 0) + item.amount;
|
||||||
|
}
|
||||||
|
const items = Object.entries(bucket)
|
||||||
|
.map(([name, total]) => ({ name, total }))
|
||||||
|
.sort((a, b) => Math.abs(b.total) - Math.abs(a.total));
|
||||||
|
sendJson(res, 200, { items });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && pathname === "/api/entries") {
|
||||||
|
const body = await readJsonBody(req);
|
||||||
|
const amount = Number(body.amount);
|
||||||
|
const note = String(body.note ?? "").trim();
|
||||||
|
const category = String(body.category ?? "未分类").trim() || "未分类";
|
||||||
|
if (Number.isNaN(amount)) {
|
||||||
|
sendJson(res, 400, { error: "amount 必须是数字" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!note) {
|
||||||
|
sendJson(res, 400, { error: "备注不能为空" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await load();
|
||||||
|
const item = {
|
||||||
|
id: Date.now(),
|
||||||
|
amount,
|
||||||
|
note,
|
||||||
|
category,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
data.push(item);
|
||||||
|
await save(data);
|
||||||
|
sendJson(res, 201, { item });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT" && pathname.startsWith("/api/entries/")) {
|
||||||
|
const idText = pathname.split("/").pop();
|
||||||
|
const id = Number(idText);
|
||||||
|
if (!idText || Number.isNaN(id)) {
|
||||||
|
sendJson(res, 400, { error: "id 必须是数字" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = await readJsonBody(req);
|
||||||
|
const amount = Number(body.amount);
|
||||||
|
const note = String(body.note ?? "").trim();
|
||||||
|
const category = String(body.category ?? "未分类").trim() || "未分类";
|
||||||
|
if (Number.isNaN(amount) || !note) {
|
||||||
|
sendJson(res, 400, { error: "参数错误" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await load();
|
||||||
|
const idx = data.findIndex((x) => x.id === id);
|
||||||
|
if (idx === -1) {
|
||||||
|
sendJson(res, 404, { error: "未找到该 id" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data[idx] = { ...data[idx], amount, note, category };
|
||||||
|
await save(data);
|
||||||
|
sendJson(res, 200, { item: data[idx] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "DELETE" && pathname.startsWith("/api/entries/")) {
|
||||||
|
const idText = pathname.split("/").pop();
|
||||||
|
const id = Number(idText);
|
||||||
|
if (!idText || Number.isNaN(id)) {
|
||||||
|
sendJson(res, 400, { error: "id 必须是数字" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await load();
|
||||||
|
const next = data.filter((x) => x.id !== id);
|
||||||
|
if (next.length === data.length) {
|
||||||
|
sendJson(res, 404, { error: "未找到该 id" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await save(next);
|
||||||
|
sendJson(res, 200, { ok: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET") {
|
||||||
|
await serveStaticFile(res, pathname);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendJson(res, 404, { error: "not found" });
|
||||||
|
} catch (err) {
|
||||||
|
sendJson(res, 500, { error: err.message || "internal error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`API 服务已启动: http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const data = await load();
|
||||||
|
|
||||||
|
if (cmd === "serve") {
|
||||||
|
const port = Number(args[0]) || 3001;
|
||||||
|
startApiServer(port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === "add") {
|
||||||
|
const [amountText, ...noteParts] = args;
|
||||||
|
const amount = Number(amountText);
|
||||||
|
const note = noteParts.join(" ").trim();
|
||||||
|
|
||||||
|
if (!amountText || Number.isNaN(amount)) {
|
||||||
|
console.log("amount 必须是数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!note) {
|
||||||
|
console.log("备注不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: Date.now(),
|
||||||
|
amount,
|
||||||
|
note,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
data.push(item);
|
||||||
|
await save(data);
|
||||||
|
console.log("已添加:", item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === "remove") {
|
||||||
|
const id = Number(args[0]);
|
||||||
|
if (!args[0] || Number.isNaN(id)) {
|
||||||
|
console.log("id 必须是数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = data.filter((x) => x.id !== id);
|
||||||
|
if (next.length === data.length) {
|
||||||
|
console.log("未找到该 id:", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await save(next);
|
||||||
|
console.log("删除成功:", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === "list" || cmd === "sum") {
|
||||||
|
const dateText = pickDateArg(args);
|
||||||
|
if (dateText && !isValidDateString(dateText)) {
|
||||||
|
console.log("日期格式错误,请使用 YYYY-MM-DD");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = withDateFilter(data, dateText);
|
||||||
|
|
||||||
|
if (cmd === "list") {
|
||||||
|
console.table(selected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = selected.reduce((s, x) => s + x.amount, 0);
|
||||||
|
console.log("总计:", total);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printHelp();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("程序出错:", err.message);
|
||||||
|
});
|
||||||
30
ledger.json
Normal file
30
ledger.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1777563638102,
|
||||||
|
"amount": 25,
|
||||||
|
"note": "午饭",
|
||||||
|
"createdAt": "2026-04-30T15:40:38.102Z",
|
||||||
|
"category": "购物"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1777563676630,
|
||||||
|
"amount": -10,
|
||||||
|
"note": "咖啡退款",
|
||||||
|
"createdAt": "2026-04-30T15:41:16.630Z",
|
||||||
|
"category": "娱乐"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1777564979416,
|
||||||
|
"amount": -12,
|
||||||
|
"note": "饮料",
|
||||||
|
"createdAt": "2026-04-30T16:02:59.416Z",
|
||||||
|
"category": "其他"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1777567863041,
|
||||||
|
"amount": -63,
|
||||||
|
"note": "雨云🌧️服务器",
|
||||||
|
"category": "学习",
|
||||||
|
"createdAt": "2026-04-30T16:51:03.041Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "ledger",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"add": "node index.js add",
|
||||||
|
"list": "node index.js list",
|
||||||
|
"sum": "node index.js sum",
|
||||||
|
"remove": "node index.js remove",
|
||||||
|
"serve": "node index.js serve"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
13
web/index.html
Normal file
13
web/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>web</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
872
web/package-lock.json
generated
Normal file
872
web/package-lock.json
generated
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^8.0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@oxc-project/types": {
|
||||||
|
"version": "0.127.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
||||||
|
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "1.10.0",
|
||||||
|
"@emnapi/runtime": "1.10.0",
|
||||||
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rolldown/pluginutils": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
|
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"lightningcss-android-arm64": "1.32.0",
|
||||||
|
"lightningcss-darwin-arm64": "1.32.0",
|
||||||
|
"lightningcss-darwin-x64": "1.32.0",
|
||||||
|
"lightningcss-freebsd-x64": "1.32.0",
|
||||||
|
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||||
|
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||||
|
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||||
|
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||||
|
"lightningcss-linux-x64-musl": "1.32.0",
|
||||||
|
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-android-arm64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
|
"version": "1.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||||
|
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||||
|
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rolldown": {
|
||||||
|
"version": "1.0.0-rc.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||||
|
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@oxc-project/types": "=0.127.0",
|
||||||
|
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rolldown": "bin/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
||||||
|
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||||
|
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "8.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||||
|
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lightningcss": "^1.32.0",
|
||||||
|
"picomatch": "^4.0.4",
|
||||||
|
"postcss": "^8.5.10",
|
||||||
|
"rolldown": "1.0.0-rc.17",
|
||||||
|
"tinyglobby": "^0.2.16"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^20.19.0 || >=22.12.0",
|
||||||
|
"@vitejs/devtools": "^0.1.0",
|
||||||
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
|
"jiti": ">=1.21.0",
|
||||||
|
"less": "^4.0.0",
|
||||||
|
"sass": "^1.70.0",
|
||||||
|
"sass-embedded": "^1.70.0",
|
||||||
|
"stylus": ">=0.54.8",
|
||||||
|
"sugarss": "^5.0.0",
|
||||||
|
"terser": "^5.16.0",
|
||||||
|
"tsx": "^4.8.1",
|
||||||
|
"yaml": "^2.4.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitejs/devtools": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jiti": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass-embedded": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"tsx": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"yaml": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
web/package.json
Normal file
14
web/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^8.0.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
web/public/favicon.svg
Normal file
1
web/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
24
web/public/icons.svg
Normal file
24
web/public/icons.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||||
|
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||||
|
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||||
|
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
BIN
web/src/assets/hero.png
Normal file
BIN
web/src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
1
web/src/assets/javascript.svg
Normal file
1
web/src/assets/javascript.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="32" height="32" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"/><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"/></svg>
|
||||||
|
After Width: | Height: | Size: 863 B |
1
web/src/assets/vite.svg
Normal file
1
web/src/assets/vite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
9
web/src/counter.js
Normal file
9
web/src/counter.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function setupCounter(element) {
|
||||||
|
let counter = 0
|
||||||
|
const setCounter = (count) => {
|
||||||
|
counter = count
|
||||||
|
element.innerHTML = `Count is ${counter}`
|
||||||
|
}
|
||||||
|
element.addEventListener('click', () => setCounter(counter + 1))
|
||||||
|
setCounter(0)
|
||||||
|
}
|
||||||
281
web/src/main.js
Normal file
281
web/src/main.js
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
const API_BASE = "/api";
|
||||||
|
const CATEGORIES = ["餐饮", "交通", "住房", "购物", "娱乐", "学习", "工资", "其他"];
|
||||||
|
const CATEGORY_COLORS = {
|
||||||
|
餐饮: "#f97316",
|
||||||
|
交通: "#3b82f6",
|
||||||
|
住房: "#8b5cf6",
|
||||||
|
购物: "#ec4899",
|
||||||
|
娱乐: "#14b8a6",
|
||||||
|
学习: "#22c55e",
|
||||||
|
工资: "#eab308",
|
||||||
|
其他: "#64748b",
|
||||||
|
未分类: "#94a3b8",
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector("#app").innerHTML = `
|
||||||
|
<main class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>Ledger</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<input id="filter-date" type="date" />
|
||||||
|
<select id="filter-type">
|
||||||
|
<option value="">全部类型</option>
|
||||||
|
<option value="income">收入</option>
|
||||||
|
<option value="expense">支出</option>
|
||||||
|
</select>
|
||||||
|
<select id="filter-category"><option value="">全部分类</option></select>
|
||||||
|
<input id="filter-q" type="text" placeholder="搜索备注" />
|
||||||
|
<button id="filter-btn">筛选</button>
|
||||||
|
<button id="reset-btn" class="btn-muted">重置</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="cards">
|
||||||
|
<article class="card"><span>净额</span><strong id="total">0</strong></article>
|
||||||
|
<article class="card"><span>收入</span><strong id="income">0</strong></article>
|
||||||
|
<article class="card"><span>支出</span><strong id="expense">0</strong></article>
|
||||||
|
<article class="card"><span>条数</span><strong id="count">0</strong></article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<form id="add-form" class="grid">
|
||||||
|
<div class="segmented" id="type-switch">
|
||||||
|
<button type="button" data-type="expense" class="active">支出</button>
|
||||||
|
<button type="button" data-type="income">收入</button>
|
||||||
|
</div>
|
||||||
|
<input id="amount" type="number" step="0.01" placeholder="金额" required />
|
||||||
|
<select id="category"></select>
|
||||||
|
<input id="note" type="text" placeholder="备注" required />
|
||||||
|
<button type="submit">新增记录</button>
|
||||||
|
</form>
|
||||||
|
<div id="message" class="message"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="layout">
|
||||||
|
<section class="panel">
|
||||||
|
<h2>记录</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>金额</th><th>分类</th><th>备注</th><th>时间</th><th>操作</th></tr></thead>
|
||||||
|
<tbody id="rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<section class="panel">
|
||||||
|
<h2>分类统计</h2>
|
||||||
|
<div id="category-stats" class="stats"></div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<dialog id="edit-dialog">
|
||||||
|
<form method="dialog" id="edit-form" class="dialog-form">
|
||||||
|
<h3>编辑记录</h3>
|
||||||
|
<input id="edit-amount" type="number" step="0.01" required />
|
||||||
|
<select id="edit-category"></select>
|
||||||
|
<input id="edit-note" type="text" required />
|
||||||
|
<menu>
|
||||||
|
<button value="cancel" class="btn-muted">取消</button>
|
||||||
|
<button id="save-edit" value="default">保存</button>
|
||||||
|
</menu>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const $ = (id) => document.querySelector(id);
|
||||||
|
const rowsEl = $("#rows");
|
||||||
|
const msgEl = $("#message");
|
||||||
|
const categoryStatsEl = $("#category-stats");
|
||||||
|
const addForm = $("#add-form");
|
||||||
|
const filterBtn = $("#filter-btn");
|
||||||
|
const resetBtn = $("#reset-btn");
|
||||||
|
const typeSwitch = $("#type-switch");
|
||||||
|
const dialog = $("#edit-dialog");
|
||||||
|
const editForm = $("#edit-form");
|
||||||
|
const state = { type: "expense", editingId: null };
|
||||||
|
|
||||||
|
function fillCategoryOptions(el) {
|
||||||
|
el.innerHTML = CATEGORIES.map((x) => `<option value="${x}">${x}</option>`).join("");
|
||||||
|
}
|
||||||
|
fillCategoryOptions($("#category"));
|
||||||
|
fillCategoryOptions($("#edit-category"));
|
||||||
|
$("#filter-category").insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
CATEGORIES.map((x) => `<option value="${x}">${x}</option>`).join("")
|
||||||
|
);
|
||||||
|
|
||||||
|
function setMessage(text, error = false) {
|
||||||
|
msgEl.textContent = text;
|
||||||
|
msgEl.className = error ? "message error" : "message";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(n) {
|
||||||
|
return Number(n).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
function qs() {
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
const date = $("#filter-date").value;
|
||||||
|
const type = $("#filter-type").value;
|
||||||
|
const category = $("#filter-category").value;
|
||||||
|
const q = $("#filter-q").value.trim();
|
||||||
|
if (date) p.set("date", date);
|
||||||
|
if (type) p.set("type", type);
|
||||||
|
if (category) p.set("category", category);
|
||||||
|
if (q) p.set("q", q);
|
||||||
|
const s = p.toString();
|
||||||
|
return s ? `?${s}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function req(path, options) {
|
||||||
|
const res = await fetch(`${API_BASE}${path}`, options);
|
||||||
|
const json = await res.json();
|
||||||
|
if (!res.ok) throw new Error(json.error || "请求失败");
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRows(items) {
|
||||||
|
if (!items.length) {
|
||||||
|
rowsEl.innerHTML = `<tr><td colspan="5" class="empty">暂无数据</td></tr>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rowsEl.innerHTML = items
|
||||||
|
.map(
|
||||||
|
(x) => `<tr>
|
||||||
|
<td class="${x.amount >= 0 ? "income" : "expense"}">${x.amount >= 0 ? "+" : ""}${fmt(x.amount)}</td>
|
||||||
|
<td>${x.category || "未分类"}</td>
|
||||||
|
<td>${x.note}</td>
|
||||||
|
<td>${new Date(x.createdAt).toLocaleString()}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button data-action="edit" data-id="${x.id}">编辑</button>
|
||||||
|
<button data-action="delete" data-id="${x.id}" class="danger">删除</button>
|
||||||
|
</td>
|
||||||
|
</tr>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCategoryStats(items) {
|
||||||
|
if (!items.length) {
|
||||||
|
categoryStatsEl.innerHTML = `<div class="empty">暂无统计</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const max = Math.max(...items.map((x) => Math.abs(x.total))) || 1;
|
||||||
|
categoryStatsEl.innerHTML = items
|
||||||
|
.map((x) => {
|
||||||
|
const width = Math.max(8, Math.round((Math.abs(x.total) / max) * 100));
|
||||||
|
const color = CATEGORY_COLORS[x.name] || CATEGORY_COLORS["未分类"];
|
||||||
|
return `<div class="stat-row">
|
||||||
|
<span>${x.name}</span>
|
||||||
|
<div class="bar-wrap"><div class="bar" style="width:${width}%;background:${color}"></div></div>
|
||||||
|
<strong>${x.total >= 0 ? "+" : ""}${fmt(x.total)}</strong>
|
||||||
|
</div>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const q = qs();
|
||||||
|
const [entries, summary, categories] = await Promise.all([
|
||||||
|
req(`/entries${q}`),
|
||||||
|
req(`/summary${q}`),
|
||||||
|
req(`/stats/categories${q}`),
|
||||||
|
]);
|
||||||
|
renderRows(entries.data);
|
||||||
|
$("#total").textContent = fmt(summary.total);
|
||||||
|
$("#income").textContent = fmt(summary.income);
|
||||||
|
$("#expense").textContent = fmt(summary.expense);
|
||||||
|
$("#count").textContent = summary.count;
|
||||||
|
renderCategoryStats(categories.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSwitch.addEventListener("click", (e) => {
|
||||||
|
const btn = e.target;
|
||||||
|
if (!(btn instanceof HTMLButtonElement) || !btn.dataset.type) return;
|
||||||
|
state.type = btn.dataset.type;
|
||||||
|
[...typeSwitch.querySelectorAll("button")].forEach((x) => x.classList.remove("active"));
|
||||||
|
btn.classList.add("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
addForm.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const raw = Number($("#amount").value);
|
||||||
|
const signedAmount = state.type === "expense" ? -Math.abs(raw) : Math.abs(raw);
|
||||||
|
await req("/entries", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
amount: signedAmount,
|
||||||
|
note: $("#note").value.trim(),
|
||||||
|
category: $("#category").value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
addForm.reset();
|
||||||
|
$("#category").value = CATEGORIES[0];
|
||||||
|
setMessage("新增成功");
|
||||||
|
await refresh();
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err.message, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rowsEl.addEventListener("click", async (e) => {
|
||||||
|
const btn = e.target;
|
||||||
|
if (!(btn instanceof HTMLButtonElement)) return;
|
||||||
|
const id = Number(btn.dataset.id);
|
||||||
|
const action = btn.dataset.action;
|
||||||
|
if (!id || !action) return;
|
||||||
|
try {
|
||||||
|
if (action === "delete") {
|
||||||
|
await req(`/entries/${id}`, { method: "DELETE" });
|
||||||
|
setMessage("删除成功");
|
||||||
|
await refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === "edit") {
|
||||||
|
const tr = btn.closest("tr");
|
||||||
|
if (!tr) return;
|
||||||
|
state.editingId = id;
|
||||||
|
$("#edit-amount").value = tr.children[0].textContent.replace("+", "").replaceAll(",", "");
|
||||||
|
$("#edit-category").value = tr.children[1].textContent;
|
||||||
|
$("#edit-note").value = tr.children[2].textContent;
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err.message, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editForm.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!state.editingId) return;
|
||||||
|
try {
|
||||||
|
await req(`/entries/${state.editingId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
amount: Number($("#edit-amount").value),
|
||||||
|
category: $("#edit-category").value,
|
||||||
|
note: $("#edit-note").value.trim(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
dialog.close();
|
||||||
|
setMessage("更新成功");
|
||||||
|
await refresh();
|
||||||
|
} catch (err) {
|
||||||
|
setMessage(err.message, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filterBtn.addEventListener("click", () => refresh().catch((e) => setMessage(e.message, true)));
|
||||||
|
resetBtn.addEventListener("click", () => {
|
||||||
|
$("#filter-date").value = "";
|
||||||
|
$("#filter-type").value = "";
|
||||||
|
$("#filter-category").value = "";
|
||||||
|
$("#filter-q").value = "";
|
||||||
|
refresh().catch((e) => setMessage(e.message, true));
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#category").value = CATEGORIES[0];
|
||||||
|
refresh().catch((e) => setMessage(e.message, true));
|
||||||
177
web/src/style.css
Normal file
177
web/src/style.css
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
color: #111827;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; }
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 24px auto;
|
||||||
|
padding: 0 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { margin: 0; font-size: 28px; }
|
||||||
|
h2 { margin: 0 0 12px; font-size: 16px; }
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select, button {
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
background: #2563eb;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-muted { background: #64748b; }
|
||||||
|
.danger { background: #ef4444; }
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card span { color: #475569; font-size: 12px; font-weight: 600; }
|
||||||
|
.card strong { font-size: 22px; }
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 210px 1fr 1fr 2fr 130px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented button {
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented button.active {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
min-height: 20px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error { color: #b91c1c; }
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||||
|
th, td { padding: 10px 8px; border-top: 1px solid #e2e8f0; text-align: left; }
|
||||||
|
th { background: #f8fafc; color: #334155; }
|
||||||
|
.actions { display: flex; gap: 6px; }
|
||||||
|
.actions button { height: 30px; font-size: 12px; }
|
||||||
|
.income { color: #14b8a6; font-weight: 700; }
|
||||||
|
.expense { color: #dc2626; font-weight: 700; }
|
||||||
|
.empty { text-align: center; color: #94a3b8; }
|
||||||
|
|
||||||
|
.stats { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.stat-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px 1fr 82px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.bar-wrap {
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.bar { height: 100%; border-radius: 999px; }
|
||||||
|
.bar-income { background: #14b8a6; }
|
||||||
|
.bar-expense { background: #ef4444; }
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: min(420px, 92vw);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop { background: rgba(15, 23, 42, 0.32); }
|
||||||
|
|
||||||
|
.dialog-form {
|
||||||
|
padding: 16px;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-form h3 { margin: 0 0 4px; }
|
||||||
|
.dialog-form menu {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
.layout { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user