Files
web2app/frontend/src/pages/Upload.tsx
shumengya b69dfc4813 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>
2026-05-29 13:30:16 +08:00

154 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
</>
);
}