feat: migrate to Cloudflare Workers with icon and version upload

Replace Express/SQLite and web/ with frontend/, worker/, and wrangler deploy. Add optional app icon upload, date-based app_version, and build-app workflow input.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 13:30:05 +08:00
parent 0921e5f7a1
commit b69dfc4813
55 changed files with 3285 additions and 3411 deletions

View File

@@ -0,0 +1,153 @@
import { FormEvent, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { createBuild } from "../api/client";
import { getDefaultAppVersion } from "../lib/version";
export default function Upload() {
const navigate = useNavigate();
const [appNameZh, setAppNameZh] = useState("");
const [appNameEn, setAppNameEn] = useState("");
const [appVersion, setAppVersion] = useState(() => getDefaultAppVersion());
const [identifier, setIdentifier] = useState("");
const [file, setFile] = useState<File | null>(null);
const [icon, setIcon] = useState<File | null>(null);
const [iconPreview, setIconPreview] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!icon) {
setIconPreview(null);
return;
}
const url = URL.createObjectURL(icon);
setIconPreview(url);
return () => URL.revokeObjectURL(url);
}, [icon]);
async function onSubmit(event: FormEvent) {
event.preventDefault();
if (!file) {
setError("请选择 zip 文件");
return;
}
setLoading(true);
setError(null);
try {
const formData = new FormData();
formData.append("file", file);
formData.append("appNameZh", appNameZh);
formData.append("appNameEn", appNameEn);
formData.append("appVersion", appVersion.trim());
if (identifier.trim()) {
formData.append("identifier", identifier.trim());
}
if (icon) {
formData.append("icon", icon);
}
const result = await createBuild(formData);
navigate(`/jobs/${result.id}`);
} catch (err) {
setError(err instanceof Error ? err.message : "上传失败");
} finally {
setLoading(false);
}
}
return (
<>
<h1 className="page-title"></h1>
<p className="page-lead">
index.html zip Windows Android
</p>
<section className="doc-section">
<h2>使</h2>
<p className="prose">
index.html 50MB
PNG / JPG / ICO zip
2026.5.29 Bundle ID
</p>
</section>
<section className="doc-section">
<h2></h2>
<form className="form" onSubmit={onSubmit}>
<label>
<input
type="text"
value={appNameZh}
onChange={(e) => setAppNameZh(e.target.value)}
placeholder="我的应用"
required
/>
</label>
<label>
<input
type="text"
value={appNameEn}
onChange={(e) => setAppNameEn(e.target.value)}
placeholder="My App"
required
pattern="[a-zA-Z][a-zA-Z0-9 _.-]*"
title="以字母开头,仅含英文字母、数字、空格、下划线和连字符"
/>
</label>
<label>
<input
type="text"
value={appVersion}
onChange={(e) => setAppVersion(e.target.value)}
placeholder={getDefaultAppVersion()}
required
pattern="\d{4}\.\d{1,2}\.\d{1,2}"
title="格式YYYY.M.D例如 2026.5.29"
/>
</label>
<label>
Bundle ID
<input
type="text"
value={identifier}
onChange={(e) => setIdentifier(e.target.value)}
placeholder="com.example.myapp"
/>
</label>
<label>
<input
type="file"
accept=".png,.jpg,.jpeg,.ico,image/png,image/jpeg,image/x-icon"
onChange={(e) => setIcon(e.target.files?.[0] ?? null)}
/>
</label>
{iconPreview ? (
<div className="icon-preview">
<img src={iconPreview} alt="图标预览" width={64} height={64} />
<span className="muted">{icon?.name}</span>
</div>
) : null}
<label>
zip
<input
type="file"
accept=".zip,application/zip"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
required
/>
</label>
{error ? <p className="error-text">{error}</p> : null}
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? "上传并触发构建…" : "开始构建"}
</button>
</form>
</section>
</>
);
}