dev-manager-tauri/src/components/dashboard/RepoRegistryModal.tsx
lanrtop bf697b5e28 feat(ui): 功能区重构阶段二——洞察页 + 全局/项目层级归位(N3/N4/N5)
N3 洞察页三 tab(飞轮/健康中心/全局治理):
   - FlywheelPanel(版本队列/阻塞/停转归档/Git质量/梦核)整体迁出 BlueprintModal
     ——跨项目内容不再寄生单项目弹窗,BlueprintModal 减重 414 行
   - CONVENTIONS 规则来源 + 应用梦核建议迁至全局治理 tab(影响所有项目的操作归全局层)
   - BlueprintModal 侧板只留单项目内容(模块详情/余票/可派发/项目治理)
N4 Gitea 层级归位:实例管理(全局配置)拆为 GiteaInstancesSection 挂设置页;
   项目绑定+webhook 留治理面板,无实例时提示去设置页添加;共用 queryKey 缓存
N5 仓库注册表并入 项目→管理→仓库 tab(RegistryTab 复用),Sidebar 底部入口
   与弹窗死壳移除;总览页更名对齐"跨项目行动项"定位

至此 ui-restructure N1-N5 全部完成:三条公理(名实一致/层级对齐/频率分层)落地。
零后端变更,typecheck 全绿。
2026-07-02 14:51:43 +09:00

214 lines
8.8 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 { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
getGitRepos,
updateGitRepoDeploy,
deleteGitRepo,
type GitRepo,
type UpdateGitRepoDeployInput,
} from "../../lib/commands";
import { useUIStore } from "../../store/ui";
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
function DeployForm({
repo,
onSaved,
onCancel,
}: {
repo: GitRepo;
onSaved: () => void;
onCancel: () => void;
}) {
const showToast = useUIStore((s) => s.showToast);
const qc = useQueryClient();
const [form, setForm] = useState<UpdateGitRepoDeployInput>({
staging_url: repo.staging_url ?? "",
prod_url: repo.prod_url ?? "",
server_note: repo.server_note ?? "",
upstream_url: repo.upstream_url ?? "",
tech_stack: repo.tech_stack ?? "",
status: repo.status ?? "active",
priority: repo.priority ?? "mid",
});
const [saving, setSaving] = useState(false);
const set = (k: keyof UpdateGitRepoDeployInput) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
setForm((f) => ({ ...f, [k]: e.target.value }));
const handleSave = async () => {
setSaving(true);
try {
await updateGitRepoDeploy(repo.id, form);
qc.invalidateQueries({ queryKey: ["git-repos"] });
showToast("已保存 ✓");
onSaved();
} catch (e) {
showToast(`❌ ${e}`);
} finally {
setSaving(false);
}
};
const inputCls = "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200";
return (
<div className="border-t border-blue-100 px-4 py-4 space-y-3 bg-blue-50/30">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">生产环境 URL</label>
<input className={inputCls} value={form.prod_url ?? ""} onChange={set("prod_url")} placeholder="https://example.com" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">预发 / 测试 URL</label>
<input className={inputCls} value={form.staging_url ?? ""} onChange={set("staging_url")} placeholder="https://staging.example.com" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">公司上游仓库</label>
<input className={inputCls} value={form.upstream_url ?? ""} onChange={set("upstream_url")} placeholder="https://github.com/company/repo.git" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">技术栈</label>
<input className={inputCls} value={form.tech_stack ?? ""} onChange={set("tech_stack")} placeholder="React / Rust / Node" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">状态</label>
<select className={inputCls} value={form.status ?? "active"} onChange={set("status")}>
<option value="active">进行中</option>
<option value="paused">暂停</option>
<option value="archived">归档</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">优先级</label>
<select className={inputCls} value={form.priority ?? "mid"} onChange={set("priority")}>
<option value="high">高</option>
<option value="mid">中</option>
<option value="low">低</option>
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">服务器备注</label>
<textarea
className={`${inputCls} resize-none`}
rows={2}
value={form.server_note ?? ""}
onChange={set("server_note")}
placeholder="服务器地址、SSH Key 路径…"
/>
</div>
<div className="flex justify-end gap-2">
<button onClick={onCancel} className="px-3 py-1.5 text-xs border border-gray-200 rounded-lg text-gray-500 hover:bg-gray-50">取消</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{saving ? "保存中…" : "保存"}
</button>
</div>
</div>
);
}
// ── 已注册仓库列表 ────────────────────────────────────────────────────────────
// 同时被 ManagePage「仓库」tab 复用(ui-restructure N5)
export function RegistryTab() {
const showToast = useUIStore((s) => s.showToast);
const qc = useQueryClient();
const [expandedId, setExpandedId] = useState<string | null>(null);
const { data: repos = [], isLoading } = useQuery({
queryKey: ["git-repos"],
queryFn: getGitRepos,
});
const handleDelete = async (id: string, name: string) => {
if (!confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`)) return;
try {
await deleteGitRepo(id);
qc.invalidateQueries({ queryKey: ["git-repos"] });
qc.invalidateQueries({ queryKey: ["projects"] });
showToast("已删除");
} catch (e) {
showToast(`❌ ${e}`);
}
};
if (isLoading) {
return <div className="flex-1 flex items-center justify-center text-gray-400 text-sm">加载中…</div>;
}
if (repos.length === 0) {
return (
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 py-16 gap-2">
<p className="text-2xl">📦</p>
<p className="text-sm">还没有注册仓库——在项目编辑中填写仓库地址或从仓库挂载后自动注册</p>
</div>
);
}
return (
<div className="flex-1 overflow-y-auto min-h-0">
{repos.map((repo) => {
const isExpanded = expandedId === repo.id;
return (
<div key={repo.id} className="border-b border-gray-100 last:border-0">
<div className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-gray-800">{repo.name}</span>
{repo.tech_stack && <span className="text-xs text-gray-400">{repo.tech_stack}</span>}
{repo.default_branch && (
<span className="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-mono">{repo.default_branch}</span>
)}
{repo.status && repo.status !== "active" && (
<span className={`text-xs px-1.5 py-0.5 rounded ${repo.status === "archived" ? "bg-gray-100 text-gray-400" : "bg-yellow-50 text-yellow-600"}`}>
{repo.status === "archived" ? "已归档" : "暂停"}
</span>
)}
</div>
<p className="text-xs text-gray-400 truncate mt-0.5 font-mono">{repo.repo_url}</p>
{(repo.prod_url || repo.staging_url) && (
<div className="flex gap-3 mt-1">
{repo.prod_url && <span className="text-xs text-green-600 truncate">生产: {repo.prod_url}</span>}
{repo.staging_url && <span className="text-xs text-blue-500 truncate">预发: {repo.staging_url}</span>}
</div>
)}
</div>
<div className="flex gap-1.5 shrink-0">
<button
onClick={() => setExpandedId(isExpanded ? null : repo.id)}
className={`px-2.5 py-1.5 rounded-lg text-xs border transition-colors ${
isExpanded
? "border-blue-300 bg-blue-50 text-blue-700"
: "border-gray-200 text-gray-500 hover:bg-gray-50"
}`}
>
{isExpanded ? "收起" : "部署信息"}
</button>
<button
onClick={() => handleDelete(repo.id, repo.name)}
className="px-2.5 py-1.5 rounded-lg text-xs border border-gray-200 text-red-400 hover:bg-red-50 hover:border-red-200 transition-colors"
>
删除
</button>
</div>
</div>
{isExpanded && (
<DeployForm
repo={repo}
onSaved={() => setExpandedId(null)}
onCancel={() => setExpandedId(null)}
/>
)}
</div>
);
})}
</div>
);
}