init: Wardrobe 衣物管理系统 - 项目首页、OTA 升级、Android CI/CD

- 响应式项目首页 (Landing, 含 R2 APK 下载) + 路由 /home 调整
- OTA 升级: tauri-plugin-hotswap (lib.rs/main.rs 拆分, 客户端热替换)
- OTA 服务端 (check/bundle/manifest-web) + build-ota/genkey/upload 脚本
- 密钥/凭据统一存配置中心 (signing + ota + cloudflare 分组)
- Android CI (android-* 分支) + OTA CI (手动触发), 产物上传 R2
- 修复 tsc/构建阻塞问题
This commit is contained in:
jif2.zhang
2026-09-11 15:46:42 +08:00
commit 38b5e3190a
62 changed files with 17963 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { sendCode, register, login } from "../api";
import { useAppStore } from "../stores/app";
import { useAuthStore } from "../stores/auth";
export default function Login() {
const navigate = useNavigate();
const { account, code, nickname, isRegister, loading, setAccount, setCode, setNickname, setMode, setLoading } = useAuthStore();
const setUser = useAppStore((s) => s.setUser);
const [status, setStatus] = useState("");
const isEmail = account.includes("@");
const canSendCode = account.length >= 6;
const handleSendCode = async () => {
setLoading(true);
setStatus("");
try {
const result = await sendCode(account);
setStatus(result);
} catch (e) {
setStatus(String(e));
} finally {
setLoading(false);
}
};
const handleSubmit = async () => {
setLoading(true);
setStatus("");
try {
const email = isEmail ? account : null;
const phone = isEmail ? null : account;
const result = isRegister
? await register(email, phone, code, nickname || "时尚达人")
: await login(email, phone, code);
if (result.success && result.user) {
setUser(result.user, result.token);
navigate("/home", { replace: true });
} else {
setStatus(result.message);
}
} catch (e) {
setStatus(String(e));
} finally {
setLoading(false);
}
};
return (
<div style={{ height: "100%", display: "flex", flexDirection: "column", justifyContent: "center", padding: "0 28px", maxWidth: 420, margin: "0 auto" }}>
<div style={{ textAlign: "center", marginBottom: 40 }}>
<div style={{ fontSize: 56, marginBottom: 8 }}>👕</div>
<h1 style={{ fontSize: 32, letterSpacing: -0.5 }}>Wardrobe</h1>
<p style={{ color: "var(--text-secondary)", fontSize: 14, marginTop: 4 }}>你的 AI 穿搭私人管家</p>
</div>
<div className="card" style={{ padding: 24 }}>
<div style={{ display: "flex", gap: 8, marginBottom: 20 }}>
<button
style={{ flex: 1, padding: "10px 0", borderRadius: "var(--radius-sm)", fontWeight: 600, fontSize: 14, background: isRegister ? "transparent" : "var(--text-primary)", color: isRegister ? "var(--text-secondary)" : "#fff" }}
onClick={() => setMode("login")}
>
登录
</button>
<button
style={{ flex: 1, padding: "10px 0", borderRadius: "var(--radius-sm)", fontWeight: 600, fontSize: 14, background: isRegister ? "var(--text-primary)" : "transparent", color: isRegister ? "#fff" : "var(--text-secondary)" }}
onClick={() => setMode("register")}
>
注册
</button>
</div>
<div className="input-group">
<label>{isEmail ? "邮箱" : "手机号"}</label>
<input className="input" placeholder={isEmail ? "you@example.com" : "13800001234"} value={account} onChange={(e) => setAccount(e.target.value.trim())} />
</div>
{isRegister && (
<div className="input-group">
<label>昵称</label>
<input className="input" placeholder="你的昵称" value={nickname} onChange={(e) => setNickname(e.target.value)} />
</div>
)}
<div className="input-group">
<label>验证码</label>
<div className="code-row">
<input className="input" placeholder="验证码" maxLength={4} inputMode="numeric" value={code} onChange={(e) => setCode(e.target.value)} />
<button className="code-btn" disabled={!canSendCode || loading} onClick={handleSendCode}>
获取验证码
</button>
</div>
</div>
{status && <p style={{ fontSize: 13, color: status.includes("成功") ? "var(--success)" : "var(--danger)", marginBottom: 12 }}>{status}</p>}
<button className="btn btn-primary" disabled={loading || !account || code.length < 4} onClick={handleSubmit}>
{loading ? "处理中..." : isRegister ? "注册" : "登录"}
</button>
</div>
</div>
);
}