add junks produced these days

This commit is contained in:
Konuri Maki 2024-12-17 23:33:24 +08:00
parent 9d7eae14db
commit 1985566ae5
29 changed files with 1724 additions and 0 deletions

47
entemu-legacy/index.html Normal file
View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>产品规划</title>
<link rel="stylesheet" href="main.css" />
</head>
<body>
<table class="dashboard">
<tr>
<td>厂房建成日:</td>
<td><input type="text" id="datestart" value="1/4/21" placeholder="1/5/21" /></td>
</tr>
<tr>
<td>生产效率:</td>
<td><input type="number" id="efficiency" value="56" /></td>
</tr>
<tr>
<td>模拟次数:</td>
<td><input type="number" id="limit" placeholder="limit" value="30" /></td>
</tr>
<tr>
<td>产出量:</td>
<td><input type="number" id="amount" value="1" /></td>
</tr>
<tr>
<td>产品:</td>
<td>
<select id="product">
<option value="P1">P1</option>
<option value="P2" selected>P2</option>
<option value="P3">P3</option>
<option value="P4">P4</option>
<option value="P5">P5</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td><button id="ok">进行模拟</button></td>
</tr>
</table>
<div id="tables"></div>
<script src="main.js"></script>
</body>
</html>

36
entemu-legacy/main.css Normal file
View file

@ -0,0 +1,36 @@
body {
font-size: large;
margin: 1em auto;
width: 29.7cm;
}
input, button, select {
font: inherit;
}
table.dashboard {
float: right;
}
table.dashboard td:nth-child(1) {
text-align: end;
}
#tables {
display: flex;
max-width: 100%;
}
table.result {
text-align: center;
margin: 0 1em;
}
table.result td {
min-width: 6em;
padding: 4px 8px;
}
table.result tr.highlight {
background-color: yellow !important;
}
table.result thead tr {
background-color: aquamarine;
font-weight: bold;
}
table.result tbody tr:nth-of-type(2n) {
background-color: #ededed;
}

189
entemu-legacy/main.js Normal file
View file

@ -0,0 +1,189 @@
/**
*
* @param {string} s
* @returns {HTMLElement}
*/
function E(s) {
return document.querySelector(s);
}
/**
*
* @param {string} tag
* @returns {HTMLElement}
*/
function A(tag) {
return document.createElement(tag);
}
/**
*
* @param {string} s
* @returns {[number, number, number]}
*/
function parse_date(s) {
return s.split("/").map(x => parseInt(x));
}
/**
*
* @param {[number, number, number]} d
* @returns {string}
*/
function fmt_date(d) {
return "" + d[0] + "/" + d[1] + "/" + d[2];
}
/**
*
* @param {[number, number, number]} d
* @param {number} n
* @returns {[number, number, number]}
*/
function add_day(d, n) {
d = d.concat();
d[2] += n;
d[1] += d[2] / 30 | 0;
d[2] = d[2] % 30;
d[0] += d[1] / 12 | 0;
d[1] = d[1] % 12;
if (d[2] < 1) {
d[2] += 30;
d[1] -= 1;
}
if (d[1] < 1) {
d[1] += 12;
d[0] -= 1;
}
return d;
}
const db = {
rprice: {
R1: 12,
R2: 12,
R3: 12,
R4: 12,
},
rdelay: {
R1: 30,
R2: 30,
R3: 60,
R4: 60,
},
pprice: {
P1: 50,
P2: 70,
P3: 90,
P4: 100,
P5: 100,
},
pdelay: {
P1: 56,
P2: 56,
P3: 56,
P4: 56,
P5: 56,
},
pr: {
P1: { R1: 1 },
P2: { R2: 1, R3: 1 },
P3: { R1: 1, R3: 1, R4: 1 },
P4: { R2: 1, R3: 1, R4: 2 },
P5: { P2: 1, R4: 1 },
},
};
/**
*
* @param {string} p
* @param {[number, number, number]} d
* @return {{ [r: string]: [number, [number, number, number]] }}
*/
function buy_date_of(p, d) {
const r = db.pr[p];
const s = {};
for (const key in r) {
if (key[0] === "R") {
s[key] = [r[key], add_day(d, -db.rdelay[key])];
} else if (key[0] === "P") {
s[key] = [r[key], add_day(d, -db.pdelay[key])];
Object.assign(s, buy_date_of(key, add_day(d, -db.pdelay[key])));
}
}
return s;
}
const order = ["P2", "P4", "P1", "P5", "P3"];
document.addEventListener("DOMContentLoaded", function() {
const datestart_input = E("#datestart");
const efficiency_input = E("#efficiency");
const product_select = E("#product");
const limit_input = E("#limit");
const amount_input = E("#amount");
const ok_button = E("#ok");
const tables = E("#tables");
function generate() {
tables.innerHTML = "";
const datestart = parse_date(datestart_input.value);
const limit = parseInt(limit_input.value);
const efficiency = parseInt(efficiency_input.value);
const amount = parseInt(amount_input.value);
const p = product_select.value;
if (order.indexOf(p) === -1) return;
// for (const p of order) {
const table = A("table");
table.classList.add("result");
tables.appendChild(table);
const thead = A("thead");
table.appendChild(thead);
let tr = A("tr");
thead.append(tr);
const plabel = A("td");
plabel.textContent = p + " 规划";
E("title").textContent = p + ": " + fmt_date(datestart);
tr.appendChild(plabel);
tr = A("tr");
thead.appendChild(tr);
const d0 = buy_date_of(p, datestart);
plabel.colSpan = Object.keys(d0).length + 2;
for (const r in d0) {
const td = A("td");
td.textContent = "购买 " + r + " * " + (d0[r][0] * amount);
tr.appendChild(td);
}
let td = A("td");
td.textContent = p + " 开产日";
tr.appendChild(td);
td = A("td");
td.textContent = p + " 产出 * " + amount;
tr.appendChild(td);
const tbody = A("tbody");
table.appendChild(tbody);
let next_date = datestart;
for (let i = 0; i < limit; ++i) {
const tr = A("tr");
tr.addEventListener("click", () => tr.classList.toggle("highlight"));
tbody.appendChild(tr);
const d = buy_date_of(p, next_date);
for (const r in d) {
const td = A("td");
td.textContent = fmt_date(d[r][1]);
tr.appendChild(td);
}
let td = A("td");
td.textContent = fmt_date(next_date);
tr.appendChild(td);
td = A("td");
td.textContent = fmt_date(add_day(next_date, efficiency /*db.pdelay[p]*/));
tr.appendChild(td);
next_date = add_day(next_date, efficiency /*db.pdelay[p]*/);
}
// }
}
ok_button.addEventListener("click", () => generate());
product_select.addEventListener("change", () => generate());
generate();
});

13
entemu/.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# Fresh build directory
_fresh/
# npm + other dependencies
node_modules/
vendor/

2
entemu/.npmrc Normal file
View file

@ -0,0 +1,2 @@
# for China Mainland users
registry=https://registry.npmmirror.com

5
entemu/.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"denoland.vscode-deno"
]
}

17
entemu/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,17 @@
{
"deno.enable": true,
"deno.lint": true,
"editor.defaultFormatter": "denoland.vscode-deno",
"[typescriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[javascriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[javascript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}

16
entemu/README.md Normal file
View file

@ -0,0 +1,16 @@
# Fresh project
Your new Fresh project is ready to go. You can follow the Fresh "Getting
Started" guide here: https://fresh.deno.dev/docs/getting-started
### Usage
Make sure to install Deno: https://deno.land/manual/getting_started/installation
Then start the project in development mode:
```
deno task dev
```
This will watch the project directory and restart as necessary.

14
entemu/common/date.ts Normal file
View file

@ -0,0 +1,14 @@
export type DateTuple = [number, number, number];
export function datetuple(d: number): DateTuple {
return [d / 12 / 30 | 0, (d / 30 | 0) % 12, d % 30]
}
export function datefmt(d: number): string {
return datetuple(d).map(n => n + 1).join("/");
}
export function datefrom(y: number, m: number, d: number) {
return (y - 1) * 12 * 30 + (m - 1) * 30 + (d - 1);
}

365
entemu/common/plan.ts Normal file
View file

@ -0,0 +1,365 @@
import { datefrom } from "./date.ts";
export interface Plan {
name: string;
times: number;
price: number;
start: number;
amount: number;
period: number;
nextonce: boolean;
role: string;
next: Partial<Plan>[];
}
export const dplan: Plan = {
name: "新策划",
times: 1,
price: 0,
start: 0,
amount: 1,
period: 0,
nextonce: false,
role: "",
next: [],
};
export interface IPlan {
date: number;
plan: Plan;
order: number;
cost: number;
current: number;
}
export function bplan(...plan: Partial<Plan>[]): Plan {
const p = { ...dplan };
for (const p1 of plan) {
Object.assign(p, p1);
}
return p;
}
export function addplan(cal: Record<number, IPlan[]>, p: Plan): number[] {
const e: number[] = [];
let d = p.start;
for (let i = 0; i < p.times; i++) {
if (cal[d] === undefined)
cal[d] = [];
cal[d].push({
date: d,
plan: p,
order: i,
cost: p.price * p.amount,
current: 0,
});
if (!p.nextonce || (p.nextonce && i === 0))
for (const n of p.next) {
e.push(...addplan(cal, bplan(n, {
start: d + (n.start || 0),
amount: (p.amount || 1) * (n.amount || 1),
})));
}
e.push(d);
d += p.period;
}
return e;
}
export function expandplan(d: number, p: Plan): Plan[] {
const l: Plan[] = [];
for (let i = 0; i < p.times; ++i) {
l.push({ ...p, times: 1, start: d + p.period * i });
}
return l;
}
export const plandbpre: Record<string, Partial<Plan>> = {
r1_order: {
name: "采购 R1",
start: -30,
period: 30,
next: [{
name: "收取 R1",
price: 12,
start: 30,
period: 0,
role: "r1",
}],
},
r2_order: {
name: "采购 R2",
start: -30,
period: 30,
next: [{
name: "收取 R2",
price: 12,
start: 30,
period: 0,
role: "r2",
}],
},
r3_order: {
name: "采购 R3",
start: -60,
period: 60,
next: [{
name: "收取 R3",
price: 12,
start: 60,
period: 0,
role: "r3",
}],
},
r4_order: {
name: "采购 R4",
start: -60,
period: 60,
next: [{
name: "收取 R4",
price: 12,
start: 60,
period: 0,
role: "r4",
}],
},
};
export const storage_keys: Record<string, string> = {
// factory: "工厂",
// linea: "自动线",
// optimizea: "自动线技改",
p1: "P1",
p2: "P2",
p3: "P3",
p4: "P4",
p5: "P5",
r1: "R1",
r2: "R2",
r3: "R3",
r4: "R4",
r5: "R5",
};
export const plandb: Record<string, Partial<Plan>> = {
ads: {
name: "广告费",
price: 80,
},
loan: {
name: "贷款",
price: 0,
start: 0,
role: "loan",
},
loanback: {
name: "还贷",
price: 0,
start: 0,
role: "-loan",
},
strategy_ads: {
name: "战略广告",
price: 20,
start: datefrom(1, 12, 30),
period: 360,
times: 5,
},
iso9000: {
name: "ISO9000资质开发",
price: 10,
times: 2,
period: 360,
nextonce: true,
next: [{
name: "ISO9000资质开发完成",
price: 0,
start: datefrom(3, 1, 1),
}],
},
iso14000: {
name: "ISO14000资质开发",
price: 10,
times: 3,
period: 360,
nextonce: true,
next: [{
name: "ISO14000资质开发完成",
price: 0,
start: datefrom(4, 1, 1),
}],
},
market_domestic: {
name: "国内市场资质开发",
price: 10,
times: 2,
period: 360,
nextonce: true,
next: [{
name: "国内市场资质开发完成",
price: 0,
start: datefrom(3, 1, 1),
}],
},
market_asia: {
name: "亚洲市场资质开发",
price: 10,
times: 3,
period: 360,
nextonce: true,
next: [{
name: "亚洲市场资质开发完成",
price: 0,
start: datefrom(4, 1, 1),
}],
},
market_intl: {
name: "国际市场资质开发",
price: 10,
times: 4,
period: 360,
nextonce: true,
next: [{
name: "国际市场资质开发完成",
price: 0,
start: datefrom(5, 1, 1),
}],
},
rent_factory: {
name: "租用厂房",
price: 40,
times: 4,
period: 360,
role: "factory",
},
p1_cert: {
name: "P1 资质开发",
price: 10,
period: 30,
nextonce: true,
next: [{
name: "P1 资质拥有",
start: 30,
price: 0,
period: 0,
role: "p1cert",
}],
},
p2_cert: {
name: "P2 资质开发",
price: 10,
period: 30,
times: 2,
nextonce: true,
next: [{
name: "P2 资质拥有",
start: 60,
price: 0,
period: 0,
role: "p2cert",
}],
},
p4_cert: {
name: "P4 资质开发",
price: 10,
period: 60,
times: 4,
nextonce: true,
next: [{
name: "P4 资质拥有",
start: 4*60,
price: 0,
period: 0,
role: "p4cert",
}],
},
build_line_manual: {
name: "建设手工线",
price: 50,
period: 0,
role: "linem",
},
build_line_auto: {
name: "建设自动线",
times: 3,
price: 50,
period: 30,
role: "linea",
},
optimize_auto: {
name: "自动线技改",
times: 1,
price: 20,
period: 20,
role: "optimizea",
},
p1_produce: {
name: "P1 生产",
times: 30,
price: 9,
period: 0,
role: "-r1",
next: [plandbpre.r1_order, {
name: "收取 P1",
times: 1,
start: 56,
price: 0,
period: 0,
role: "p1",
}],
},
p2_produce: {
name: "P2 生产",
times: 30,
price: 9,
period: 0,
role: "-r2,-r3",
next: [plandbpre.r2_order, plandbpre.r3_order, {
name: "收取 P2",
times: 1,
start: 56,
price: 0,
period: 0,
role: "p2",
}],
},
p4_produce: {
name: "P4 生产",
times: 30,
price: 9,
period: 0,
role: "-r2,-r3,-r4,-r4",
next: [
plandbpre.r2_order,
plandbpre.r3_order,
bplan(plandbpre.r4_order, { amount: 2 }), {
name: "收取 P4",
times: 1,
start: 56,
price: 0,
period: 0,
role: "p4",
}],
},
p2_sell: {
name: "卖出 P2",
price: 0,
role: "-p2",
next: [{
name: "收到 P2 账款",
price: -52,
start: 30,
}],
},
p4_sell: {
name: "卖出 P4",
price: 0,
amount: 1,
role: "-p4",
next: [{
name: "收到 P4 账款",
price: -91,
start: 30,
}],
},
};

44
entemu/deno.json Normal file
View file

@ -0,0 +1,44 @@
{
"tasks": {
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"dev": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
"start": "deno run -A main.ts",
"update": "deno run -A -r jsr:@fresh/update ."
},
"lint": {
"rules": {
"tags": [
"fresh",
"recommended"
]
}
},
"exclude": [
"**/_fresh/*"
],
"imports": {
"fresh": "jsr:@fresh/core@^2.0.0-alpha.25",
"@fresh/plugin-tailwind": "jsr:@fresh/plugin-tailwind@^0.0.1-alpha.7",
"preact": "npm:preact@^10.24.3",
"@preact/signals": "npm:@preact/signals@^1.3.0"
},
"compilerOptions": {
"lib": [
"dom",
"dom.asynciterable",
"dom.iterable",
"deno.ns"
],
"jsx": "precompile",
"jsxImportSource": "preact",
"jsxPrecompileSkipElements": [
"a",
"img",
"source",
"body",
"html",
"head"
]
}
}

229
entemu/deno.lock Normal file
View file

@ -0,0 +1,229 @@
{
"version": "4",
"specifiers": {
"jsr:@fresh/core@^2.0.0-alpha.25": "2.0.0-alpha.25",
"jsr:@luca/esbuild-deno-loader@0.11": "0.11.0",
"jsr:@std/bytes@^1.0.2": "1.0.4",
"jsr:@std/crypto@1": "1.0.3",
"jsr:@std/datetime@~0.225.2": "0.225.2",
"jsr:@std/encoding@1": "1.0.5",
"jsr:@std/encoding@^1.0.5": "1.0.5",
"jsr:@std/fmt@1": "1.0.3",
"jsr:@std/fs@1": "1.0.6",
"jsr:@std/html@1": "1.0.3",
"jsr:@std/jsonc@1": "1.0.1",
"jsr:@std/media-types@1": "1.1.0",
"jsr:@std/path@1": "1.0.8",
"jsr:@std/path@^1.0.6": "1.0.8",
"jsr:@std/path@^1.0.8": "1.0.8",
"jsr:@std/semver@1": "1.0.3",
"npm:@preact/signals@^1.2.3": "1.3.1_preact@10.25.1",
"npm:@preact/signals@^1.3.0": "1.3.1_preact@10.25.1",
"npm:esbuild-wasm@0.23.1": "0.23.1",
"npm:esbuild@0.23.1": "0.23.1",
"npm:preact-render-to-string@^6.5.11": "6.5.11_preact@10.25.1",
"npm:preact@^10.24.1": "10.25.1",
"npm:preact@^10.24.3": "10.25.1"
},
"jsr": {
"@fresh/core@2.0.0-alpha.25": {
"integrity": "1069232989c4bc7f69ad424f6b97cdba1a7631d1307e1c2964aed748cd0cf74b",
"dependencies": [
"jsr:@luca/esbuild-deno-loader",
"jsr:@std/crypto",
"jsr:@std/datetime",
"jsr:@std/encoding@1",
"jsr:@std/fmt",
"jsr:@std/fs",
"jsr:@std/html",
"jsr:@std/jsonc",
"jsr:@std/media-types",
"jsr:@std/path@1",
"jsr:@std/semver",
"npm:@preact/signals@^1.2.3",
"npm:esbuild",
"npm:esbuild-wasm",
"npm:preact-render-to-string",
"npm:preact@^10.24.1",
"npm:preact@^10.24.3"
]
},
"@luca/esbuild-deno-loader@0.11.0": {
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
"dependencies": [
"jsr:@std/bytes",
"jsr:@std/encoding@^1.0.5",
"jsr:@std/path@^1.0.6"
]
},
"@std/bytes@1.0.4": {
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
},
"@std/crypto@1.0.3": {
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
},
"@std/datetime@0.225.2": {
"integrity": "45f0100554a912cd65f48089ef0a33aa1eb6ea21f08090840b539ab582827eaa"
},
"@std/encoding@1.0.5": {
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
},
"@std/fmt@1.0.3": {
"integrity": "97765c16aa32245ff4e2204ecf7d8562496a3cb8592340a80e7e554e0bb9149f"
},
"@std/fs@1.0.6": {
"integrity": "42b56e1e41b75583a21d5a37f6a6a27de9f510bcd36c0c85791d685ca0b85fa2",
"dependencies": [
"jsr:@std/path@^1.0.8"
]
},
"@std/html@1.0.3": {
"integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988"
},
"@std/jsonc@1.0.1": {
"integrity": "6b36956e2a7cbb08ca5ad7fbec72e661e6217c202f348496ea88747636710dda"
},
"@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
},
"@std/path@1.0.8": {
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
},
"@std/semver@1.0.3": {
"integrity": "7c139c6076a080eeaa4252c78b95ca5302818d7eafab0470d34cafd9930c13c8"
}
},
"npm": {
"@esbuild/aix-ppc64@0.23.1": {
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ=="
},
"@esbuild/android-arm64@0.23.1": {
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw=="
},
"@esbuild/android-arm@0.23.1": {
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ=="
},
"@esbuild/android-x64@0.23.1": {
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg=="
},
"@esbuild/darwin-arm64@0.23.1": {
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q=="
},
"@esbuild/darwin-x64@0.23.1": {
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw=="
},
"@esbuild/freebsd-arm64@0.23.1": {
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA=="
},
"@esbuild/freebsd-x64@0.23.1": {
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g=="
},
"@esbuild/linux-arm64@0.23.1": {
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g=="
},
"@esbuild/linux-arm@0.23.1": {
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ=="
},
"@esbuild/linux-ia32@0.23.1": {
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ=="
},
"@esbuild/linux-loong64@0.23.1": {
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw=="
},
"@esbuild/linux-mips64el@0.23.1": {
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q=="
},
"@esbuild/linux-ppc64@0.23.1": {
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw=="
},
"@esbuild/linux-riscv64@0.23.1": {
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA=="
},
"@esbuild/linux-s390x@0.23.1": {
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw=="
},
"@esbuild/linux-x64@0.23.1": {
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ=="
},
"@esbuild/netbsd-x64@0.23.1": {
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA=="
},
"@esbuild/openbsd-arm64@0.23.1": {
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q=="
},
"@esbuild/openbsd-x64@0.23.1": {
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA=="
},
"@esbuild/sunos-x64@0.23.1": {
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA=="
},
"@esbuild/win32-arm64@0.23.1": {
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A=="
},
"@esbuild/win32-ia32@0.23.1": {
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ=="
},
"@esbuild/win32-x64@0.23.1": {
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg=="
},
"@preact/signals-core@1.8.0": {
"integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA=="
},
"@preact/signals@1.3.1_preact@10.25.1": {
"integrity": "sha512-nNvSF2O7RDzxp1Rm7SkA5QhN1a2kN8pGE8J5o6UjgDof0F0Vlg6d6HUUVxxqZ1uJrN9xnH2DpL6rpII3Es0SsQ==",
"dependencies": [
"@preact/signals-core",
"preact"
]
},
"esbuild-wasm@0.23.1": {
"integrity": "sha512-L3vn7ctvBrtScRfoB0zG1eOCiV4xYvpLYWfe6PDZuV+iDFDm4Mt3xeLIDllG8cDHQ8clUouK3XekulE+cxgkgw=="
},
"esbuild@0.23.1": {
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
"dependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
"@esbuild/android-x64",
"@esbuild/darwin-arm64",
"@esbuild/darwin-x64",
"@esbuild/freebsd-arm64",
"@esbuild/freebsd-x64",
"@esbuild/linux-arm",
"@esbuild/linux-arm64",
"@esbuild/linux-ia32",
"@esbuild/linux-loong64",
"@esbuild/linux-mips64el",
"@esbuild/linux-ppc64",
"@esbuild/linux-riscv64",
"@esbuild/linux-s390x",
"@esbuild/linux-x64",
"@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64",
"@esbuild/sunos-x64",
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
"@esbuild/win32-x64"
]
},
"preact-render-to-string@6.5.11_preact@10.25.1": {
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
"dependencies": [
"preact"
]
},
"preact@10.25.1": {
"integrity": "sha512-frxeZV2vhQSohQwJ7FvlqC40ze89+8friponWUFeVEkaCfhC6Eu4V0iND5C9CXz8JLndV07QRDeXzH1+Anz5Og=="
}
},
"workspace": {
"dependencies": [
"jsr:@fresh/core@^2.0.0-alpha.25",
"jsr:@fresh/plugin-tailwind@^0.0.1-alpha.7",
"npm:@preact/signals@^1.3.0",
"npm:preact@^10.24.3"
]
}
}

15
entemu/dev.ts Normal file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env -S deno run -A --watch=static/,routes/
import { Builder } from "fresh/dev";
import { app } from "./main.ts";
const builder = new Builder();
if (Deno.args.includes("build")) {
await builder.build(app);
} else {
await builder.listen(app, {
hostname: Deno.env.get("junkhost") || "127.0.0.1",
port: parseInt(Deno.env.get("junkport") || "11001"),
});
}

161
entemu/islands/Calendar.tsx Normal file
View file

@ -0,0 +1,161 @@
import { useComputed, type Signal } from "@preact/signals";
import { addplan, bplan, IPlan, Plan, storage_keys } from "../common/plan.ts";
import { datefmt, datefrom } from "../common/date.ts";
export interface CalendarProps {
date: Signal<number>;
plans: Signal<Plan[]>;
}
const casht_a = -800;
const casht_b = -950;
const calendar_span = 30;
const calendar_days = new Array(calendar_span).fill(0).map((_, i) => i);
export default function Calendar({ date, plans }: CalendarProps) {
const _calendar = useComputed(() => {
// cal
let e: number[] = [];
const cal: Record<number, IPlan[]> = {};
for (const p of plans.value) {
e = e.concat(addplan(cal, p));
}
e = Array.from(new Set(e)).sort((a, b) => a - b);
let c = 600;
let current = 600;
let alert = -1;
let die = -1;
/*
const cal_ex: Record<number, IPlan[]> = {};
const e_ex: number[] = [];
*/
// cash
for (const d of e) {
for (const p of cal[d]) {
c -= p.cost;
p.current = c;
if (alert === -1 && c < casht_a && d < datefrom(6, 1, 1))
alert = d;
if (die === -1 && c < casht_b && d < datefrom(6, 1, 1))
die = d;
/*
if (c < 0) {
e_ex.push(...(addplan(cal_ex, bplan({
name: "借款",
start: d,
price: -p.cost,
})).concat(addplan(cal_ex, bplan({
name: "还贷",
start: d + 360,
price: p.cost * 1.05,
})))));
}
*/
}
if (d < date.value + 30) {
current = c;
}
}
/*
for (const k in cal_ex) {
if (cal[k])
cal[k].push(...cal_ex[k]);
else
cal[k] = cal_ex[k];
}
e.push(...e_ex);
*/
return {
cal: cal,
e: e,
cash: current,
alert: alert,
die: die,
};
});
const calendar = useComputed(() => _calendar.value.cal);
const event_days = useComputed(() => _calendar.value.e);
const cash = useComputed(() => _calendar.value.cash);
const date_alert = useComputed(() => _calendar.value.alert);
const date_die = useComputed(() => _calendar.value.die);
const storage = useComputed(() => {
const st: Record<string, number> = {};
for (const d of event_days.value) {
if (d >= date.value + 30)
break;
for (const p of calendar.value[d]) {
if (!p.plan.role) continue;
for (let role of p.plan.role.split(",")) {
let amount = p.plan.amount;
// if (!role) continue;
if (role[0] === "-") {
amount = -amount;
role = role.slice(1);
}
if (st[role] === undefined)
st[role] = 0;
st[role] += amount;
}
}
}
return st;
});
let last_date = -1;
return <div class="calendar">
<div class="calendar__calendar">
<div class="calender__cash">{cash.value}</div>
<div class="calendar__head">
<button onClick={() => date.value -= 360}>&lt;&lt;&lt;</button>
<button onClick={() => date.value -= 90}>&lt;&lt;</button>
<button onClick={() => date.value -= 30}>&lt;</button>
<span class="calendar__date">{datefmt(date.value)}</span>
<button onClick={() => date.value += 30}>&gt;</button>
<button onClick={() => date.value += 90}>&gt;&gt;</button>
<button onClick={() => date.value += 360}>&gt;&gt;&gt;</button>
</div>
<div class="calendar__days">
{calendar_days.map(d => <span class={calendar.value[d + date.value] ? "p" : ""}>{d + 1}</span>)}
</div>
</div>
<div class="calendar__alert" hidden>
<p>{date_alert.value === -1 ? "" : "资金周转风险 (现金 < " + casht_a + "): " + datefmt(date_alert.value)}</p>
<p>{date_die.value === -1 ? "" : "资金链断裂 (现金 < " + casht_b + "): " + datefmt(date_die.value)}</p>
</div>
<table class="calendar__storage">
<thead>
<tr><td></td><td></td></tr>
</thead>
<tbody>
{Object.entries(storage.value).filter(st => storage_keys[st[0]] && st[1]).map(st => <tr><td>{storage_keys[st[0]]}</td><td>{st[1]}</td></tr>)}
</tbody>
</table>
<table class="calendar__events" cellSpacing="0">
<thead>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</thead>
<tbody>
{calendar_days.map(d => d + date.value).filter(d => calendar.value[d]).map(d => calendar.value[d].map(p => {
const e = <tr>
<td>{last_date === p.date ? "" : datefmt(p.date)}</td>
<td>{p.plan.name}</td>
<td>{p.plan.amount}</td>
<td>{p.plan.times === 1 ? "1" : "" + (p.order + 1) + " / " + p.plan.times}</td>
<td>{p.cost !== 0 ? "¥" + (-p.cost) : ""}</td>
<td>{p.current}</td>
</tr>
last_date = p.date;
return e;
}))}
</tbody>
</table>
</div>;
}

35
entemu/islands/PlanDB.tsx Normal file
View file

@ -0,0 +1,35 @@
import { useSignal, type Signal } from "@preact/signals";
import { bplan, dplan, Plan, plandb } from "../common/plan.ts";
export interface PlanDBProps {
date: Signal<number>;
plans: Signal<Plan[]>;
cplan: Signal<Plan>;
}
export interface PlanDisplayProps {
plan: Plan;
add?: (plan: Plan) => any;
modify?: (plan: Plan) => any;
del?: (plan: Plan) => any;
}
export function PlanDisplay({ plan, add, modify, del }: PlanDisplayProps) {
const cplan: Signal<Plan> = useSignal({ ...dplan, ...plan });
return <div class="plan">
<pre>{JSON.stringify(cplan.value)}</pre>
</div>;
}
export default function PlanDB({ date, plans, cplan }: PlanDBProps) {
return <div class="plandb">
<div></div>
<div class="plandb_current">
{Object.entries(plans.value).map(([k, v]) => <PlanDisplay plan={v} />)}
</div>
<div></div>
<div class="plandb__presets">
{Object.entries(plandb).map(([k, v]) => <PlanDisplay plan={bplan(v)} />)}
</div>
</div>;
}

View file

@ -0,0 +1,72 @@
import { type Signal, useSignal } from "@preact/signals";
import { bplan, Plan, plandb } from "../common/plan.ts";
import Calendar from "./Calendar.tsx";
import { datefrom } from "../common/date.ts";
export default function Planner() {
const date = useSignal(self.localStorage ? parseInt(localStorage.getItem("entemu.date") || "0") : 0);
date.subscribe(d => self.localStorage ? localStorage.setItem("entemu.date", d.toString()) : void 0);
const plans: Signal<Plan[]> = useSignal([
// base
bplan(plandb.iso9000),
bplan(plandb.iso14000),
bplan(plandb.market_domestic),
bplan(plandb.market_asia),
bplan({ name: "管理费", times: 5*12*30, period: 30, price: 5 }),
bplan({ name: "维修费", start: datefrom(2, 4, 1), times: 4, amount: 4, period: 360, price: 15 }),
// start 1
bplan(plandb.ads),
bplan(plandb.strategy_ads),
bplan(plandb.rent_factory, { times: 4, amount: 1 }),
// bplan(plandb.p1_cert),
bplan(plandb.p2_cert),
bplan(plandb.p4_cert),
bplan(plandb.build_line_auto, { amount: 4 }),
bplan(plandb.optimize_auto, { start: datefrom(1, 4, 1), amount: 4 }),
// bplan(plandb.loan, { price: -500, start: datefrom(1, 3, 1) }),
// bplan(plandb.loanback, { price: 525, start: datefrom(1, 11, 1) }),
// bplan(plandb.loan, { price: -30, start: datefrom(1, 6, 17) }),
// bplan(plandb.loanback, { price: 31.5, start: datefrom(2, 6, 17) }),
// bplan(plandb.loan, { price: -10, start: datefrom(1, 7, 1) }),
// bplan(plandb.loanback, { price: 10.5, start: datefrom(2, 7, 1) }),
// bplan(plandb.loan, { price: -40, start: datefrom(1, 8, 13) }),
// bplan(plandb.loanback, { price: 42, start: datefrom(2, 8, 13) }),
bplan(plandb.loan, { price: -480-360, start: datefrom(1, 12, 30) }),
bplan(plandb.loanback, { price: 480*1.1 + 360*1.05, start: datefrom(3, 1, 1) }),
bplan(plandb, { name: "贴现", price: 40-40*1.1, start: datefrom(2, 1, 1) }),
bplan(plandb.p4_sell, { name: "贱卖 P4", start: datefrom(1, 11, 1), price: -50, role: "-p4" }),
// prod 1
// bplan(plandb.p1_produce, { start: 110, period: 56, amount: 30 }),
bplan(plandb.p2_produce, { start: datefrom(1, 4, 21), period: 56, times: 30, amount: 3 }),
bplan(plandb.p4_produce, { start: datefrom(1, 9, 1), period: 56, times: 30 }),
bplan(plandb.p2_sell, { start: datefrom(1, 7, 15), amount: 3, price: 0, next: [{ name: "收到 P2 账款", price: -60, start: 30, }] }),
bplan(plandb.p2_sell, { start: datefrom(1, 12, 21), amount: 9, price: 0, next: [{ name: "收到 P2 账款", price: -54, start: 30, }] }),
// year 2
bplan(plandb.ad),
bplan(plandb.p2_sell, { start: datefrom(2, 3, 27), amount: 6, next: [{ name: "收到 P2 账款", price: -51, start: 30, }] }),
bplan(plandb.p2_sell, { start: datefrom(2, 7, 19), amount: 6, next: [{ name: "收到 P2 账款", price: -58, start: 30, }] }),
bplan(plandb.p4_sell, { start: datefrom(2, 2, 19), amount: 2, next: [{ name: "收到 P4 账款", price: -92, start: 30, }] }),
bplan(plandb.p4_sell, { start: datefrom(2, 6, 11), amount: 2, next: [{ name: "收到 P4 账款", price: -91, start: 30, }] }),
// year 3
bplan(plandb.p2_sell, { start: datefrom(3, 11, 19), amount: 25, next: [{ name: "收到 P2 账款", price: -58, start: 30, }] }),
bplan(plandb.p4_sell, { start: datefrom(3, 11, 19), amount: 8, next: [{ name: "收到 P4 账款", price: -92, start: 30, }] }),
// year 4
bplan(plandb.p2_sell, { start: datefrom(4, 12, 21), amount: 19, next: [{ name: "收到 P2 账款", price: -58, start: 30, }] }),
bplan(plandb.p4_sell, { start: datefrom(4, 12, 21), amount: 8, next: [{ name: "收到 P4 账款", price: -92, start: 30, }] }),
]);
return <div>
<Calendar date={date} plans={plans} />
</div>;
}

8
entemu/junkrc Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
#;proxy=http://localhost:11001
export deno=/mnt/data/app/deno
export junkhost=127.0.0.1
export junkport=11001
$deno task build
exec $deno run --allow-read --allow-net --allow-env main.ts

33
entemu/main.ts Normal file
View file

@ -0,0 +1,33 @@
import { App, fsRoutes, staticFiles } from "fresh";
import { define, type State } from "./utils.ts";
export const app = new App<State>();
app.use(staticFiles());
// this is the same as the /api/:name route defined via a file. feel free to delete this!
app.get("/api2/:name", (ctx) => {
const name = ctx.params.name;
return new Response(
`Hello, ${name.charAt(0).toUpperCase() + name.slice(1)}!`,
);
});
// this can also be defined via a file. feel free to delete this!
const exampleLoggerMiddleware = define.middleware((ctx) => {
console.log(`${ctx.req.method} ${ctx.req.url}`);
return ctx.next();
});
app.use(exampleLoggerMiddleware);
await fsRoutes(app, {
dir: "./",
loadIsland: (path) => import(`./islands/${path}`),
loadRoute: (path) => import(`./routes/${path}`),
});
if (import.meta.main) {
await app.listen({
hostname: Deno.env.get("junkhost") || "127.0.0.1",
port: parseInt(Deno.env.get("junkport") || "11001"),
});
}

17
entemu/routes/_app.tsx Normal file
View file

@ -0,0 +1,17 @@
import type { PageProps } from "fresh";
export default function App({ Component }: PageProps) {
return (
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>entemu</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<Component />
</body>
</html>
);
}

View file

@ -0,0 +1,10 @@
import { define } from "../../utils.ts";
export const handler = define.handlers({
GET(ctx) {
const name = ctx.params.name;
return new Response(
`Hello, ${name.charAt(0).toUpperCase() + name.slice(1)}!`,
);
},
});

6
entemu/routes/index.tsx Normal file
View file

@ -0,0 +1,6 @@
import Planner from "../islands/Planner.tsx";
import { define } from "../utils.ts";
export default define.page(function Home() {
return <Planner />;
});

BIN
entemu/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

6
entemu/static/logo.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.092 8.845C38.929 20.652 34.092 27 30 30.5c1 3.5-2.986 4.222-4.5 2.5-4.457 1.537-13.512 1.487-20-5C2 24.5 4.73 16.714 14 11.5c8-4.5 16-7 20.092-2.655Z" fill="#FFDB1E"/>
<path d="M14 11.5c6.848-4.497 15.025-6.38 18.368-3.47C37.5 12.5 21.5 22.612 15.5 25c-6.5 2.587-3 8.5-6.5 8.5-3 0-2.5-4-5.183-7.75C2.232 23.535 6.16 16.648 14 11.5Z" fill="#fff" stroke="#FFDB1E"/>
<path d="M28.535 8.772c4.645 1.25-.365 5.695-4.303 8.536-3.732 2.692-6.606 4.21-7.923 4.83-.366.173-1.617-2.252-1.617-1 0 .417-.7 2.238-.934 2.326-1.365.512-4.223 1.29-5.835 1.29-3.491 0-1.923-4.754 3.014-9.122.892-.789 1.478-.645 2.283-.645-.537-.773-.534-.917.403-1.546C17.79 10.64 23 8.77 25.212 8.42c.366.014.82.35.82.629.41-.14 2.095-.388 2.503-.278Z" fill="#FFE600"/>
<path d="M14.297 16.49c.985-.747 1.644-1.01 2.099-2.526.566.121.841-.08 1.29-.701.324.466 1.657.608 2.453.701-.715.451-1.057.852-1.452 2.106-1.464-.611-3.167-.302-4.39.42Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

160
entemu/static/styles.css Normal file
View file

@ -0,0 +1,160 @@
:root {
--fore: #202020;
--back: #fdfdfd;
--back2: #f0f0f0;
--shade: rgba(0, 0, 0, 0.05);
--highlight: yellow;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
button {
color: inherit;
background: none;
border: 1px solid var(--fore);
border-radius: 4px;
/*
font: inherit;
font-size: 1rem;
border: none;
padding: 4px;
background-color: var(--back);
border-radius: 1em;
width: 2.2rem;
*/
}
button, [role="button"] {
cursor: pointer;
}
code {
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
"Liberation Mono",
"Courier New",
monospace;
font-size: 1em;
}
img,
svg {
display: block;
}
img,
video {
max-width: 100%;
height: auto;
}
html {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
color: var(--fore);
background-color: var(--back);
padding-bottom: 4em;
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
"Noto Sans",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji";
}
.calendar__calendar {
width: 18em;
margin: 1em auto;
border-radius: 2em;
padding: 1em;
background-color: var(--back2);
}
.calender__cash {
margin: 4px 0.5em;
}
.calendar__head {
text-align: center;
margin: 0.5em 0;
}
.calendar__days {
display: flex;
flex-wrap: wrap;
width: 16em;
text-align: center;
}
.calendar__date {
display: inline-block;
width: 4em;
text-align: center;
}
.calendar__days > span {
width: 3.2em;
padding: 4px 0;
}
.calendar__days > span.p {
background-color: var(--highlight);
}
.calendar__events {
width: 21cm;
margin: 1em auto;
}
.calendar__alert > * {
color: orange;
font-weight: bold;
width: 21cm;
margin: 0.5em auto;
}
.calendar__storage {
width: 12em;
margin: 1em auto;
border-radius: 2em;
padding: 0.5em;
background-color: var(--back2);
}
.calendar__storage td {
padding: 0 8px;
width: 12em;
}
.calendar__storage td:nth-child(1) {
text-align: end;
}
/* .calendar__events {} */
.calendar__events thead > tr,
.calendar__events tr:nth-of-type(2n) {
background-color: var(--shade);
}
.calendar__events tr:hover {
background-color: rgba(0, 255, 0, 0.2) !important;
}
a:any-link {
color: var(--fore);
}
@media (prefers-color-scheme: dark) {
:root {
--fore: #a0a0a0;
--back: #303030;
--back2: #383838;
--shade: rgba(255, 255, 255, 0.05);
--highlight: rgba(255, 255, 0, 0.15);
}
}

6
entemu/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { createDefine } from "fresh";
// deno-lint-ignore no-empty-interface
export interface State {}
export const define = createDefine<State>();

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module junk
go 1.21.0

26
index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Junk</title>
<link rel="stylesheet" href="junk.css" />
</head>
<body>
<main>
<h1>Historical Interests</h1>
<dl>
<dt><a href="/entemu/">entemu</a></dt>
<dd>Planning for a sandbox enterprise game, made in one week. Ugly code. Change plans by changing the code.</dd>
</dl>
<dl>
<dt><a href="/entemu-legacy/">entemu-legacy</a></dt>
<dd>Legacy version of entemu, made overnight. Ugly code. Have been used together with entemu.</dd>
</dl>
</main>
<footer>
<p><a href="https://git.yozora.tech/maki/junk">This service itself</a> is junk, too.</p>
<p>Everything here are licensed under <a href="/LICENSE">WTFPL, version 2</a>.</p>
</footer>
</body>
</html>

23
junk.css Normal file
View file

@ -0,0 +1,23 @@
:root {
--fore: #202020;
--back: #fdfdfd;
}
body {
margin: 1em auto;
width: 21cm;
line-height: 1.5;
background-color: var(--back);
color: var(--fore);
}
a:any-link {
color: var(--fore);
}
@media (prefers-color-scheme: dark) {
:root {
--fore: #a0a0a0;
--back: #303030;
}
}

166
main.go Normal file
View file

@ -0,0 +1,166 @@
package main
import (
"bufio"
"net/http"
"net/url"
"os"
"os/exec"
"os/signal"
"strings"
"syscall"
"time"
)
const junkaddr = "[::]:8012"
const proxybufsize = 4 << 10
type JunkConf struct {
Proxy string
Cmd *exec.Cmd
}
func main() {
status := cmain(len(os.Args), os.Args)
os.Setenv("status", status)
if status != "" {
os.Exit(1)
}
}
func proxy(w http.ResponseWriter, r *http.Request, proxy_path string) {
var err error
r.RequestURI = ""
r.URL, err = url.Parse(proxy_path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
resp, err := http.DefaultClient.Do(r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
for k, v := range resp.Header {
for _, x := range v {
w.Header().Add(k, x)
}
}
w.WriteHeader(resp.StatusCode)
buf := make([]byte, proxybufsize)
for {
n, err := resp.Body.Read(buf)
w.Write(buf[:n])
if err != nil {
break
}
}
}
func cmain(argc int, argv []string) string {
fileserver := http.FileServer(http.Dir(""))
cmdmap := map[string]JunkConf{}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
p1 := r.URL.Path
if p1 == "/" {
fileserver.ServeHTTP(w, r)
return
}
p := strings.Split(p1, "/")[1:]
seg := p[0]
if ref := r.Header.Get("Referer"); ref != "" {
rurl, err := url.Parse(ref)
rp := strings.Split(rurl.Path, "/")[1:]
if rurl.Path != "/" {
rseg := rp[0]
if err == nil && seg != rseg && (len(p) < 2 || p[1] != rseg) {
p1 = "/" + rseg + r.URL.Path
w.Header().Set("Location", p1)
w.WriteHeader(http.StatusFound)
return
}
}
}
if conf, ok := cmdmap[seg]; ok {
proxy_path := conf.Proxy + "/" + strings.Join(p[1:], "/")
proxy(w, r, proxy_path)
return
}
if f, err := os.Open(seg + "/junkrc"); err == nil {
// info, err := f.Stat()
s := bufio.NewScanner(f)
conf := JunkConf{}
for s.Scan() {
line := s.Text()
if len(line) > 2 && line[0:2] == "#;" {
note := line[2:]
i := strings.IndexByte(note, '=')
if i == -1 {
continue
}
key, value := note[:i], note[i+1:]
switch key {
case "proxy":
conf.Proxy = value
}
}
}
f.Close()
println("exec:", seg+"/junkrc")
cmd := exec.Command("./junkrc")
cmd.Dir = seg
// cmd.Stdout = os.Stdout
conf.Cmd = cmd
err = cmd.Start()
if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(err.Error()))
return
}
waits := 0
for {
_, err := http.Head(conf.Proxy)
if err != nil {
if cmd.ProcessState != nil {
cmd.Wait()
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("process exited early: " + cmd.ProcessState.String()))
return
} else {
waits++
time.Sleep(500 * time.Millisecond)
if waits > 10 {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("starting process: timeout"))
cmd.Process.Kill()
cmd.Wait()
return
}
continue
}
}
cmdmap[seg] = conf
break
}
proxy_path := conf.Proxy + "/" + strings.Join(p[1:], "/")
proxy(w, r, proxy_path)
return
}
fileserver.ServeHTTP(w, r)
})
lock := make(chan os.Signal, 1)
signal.Notify(lock, os.Interrupt, syscall.SIGTERM)
go func() {
println("started:", "http://"+junkaddr+"/")
http.ListenAndServe(junkaddr, nil)
}()
<-lock
for k, v := range cmdmap {
println("killing:", k)
v.Cmd.Process.Kill()
v.Cmd.Wait()
}
return ""
}