chore: wave-0 baseline (Tauri2+React shell, platform detect, fake catalog)
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AgentDock</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "agentdock-desktop",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.0",
|
||||
"@vitejs/plugin-react": "^4.5.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "agentdock-desktop"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "AgentDock 桌面壳(Tauri 2)"
|
||||
|
||||
[lib]
|
||||
name = "agentdock_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
agentdock-platform = { path = "../../../crates/agentdock-platform" }
|
||||
agentdock-adapter = { path = "../../../crates/agentdock-adapter" }
|
||||
agentdock-store = { path = "../../../crates/agentdock-store" }
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "默认能力:主窗口(Wave 0 仅调用自有 invoke 命令)",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 887 B |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,11 @@
|
||||
//! IPC 命令:假目录(架构 §2 adapters/,IPC 契约 listCatalog)
|
||||
|
||||
use agentdock_adapter::catalog::{self, CatalogEntry};
|
||||
|
||||
use crate::adapters_dir;
|
||||
|
||||
/// 读取 adapters/catalog.yaml 与 tools/*.yaml,返回 14 个目录条目
|
||||
#[tauri::command(rename = "listCatalog")]
|
||||
pub fn list_catalog() -> Result<Vec<CatalogEntry>, String> {
|
||||
catalog::load_catalog(adapters_dir()).map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//! IPC 命令:本机环境检测(架构 §5,IPC 契约 detectEnv)
|
||||
|
||||
use agentdock_platform::detect;
|
||||
use agentdock_platform::model::PlatformEnv;
|
||||
|
||||
/// 返回本机真实环境快照(Windows/Linux 双平台)
|
||||
#[tauri::command(rename = "detectEnv")]
|
||||
pub fn detect_env() -> PlatformEnv {
|
||||
detect::detect_env()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod catalog;
|
||||
pub mod env;
|
||||
@@ -0,0 +1,42 @@
|
||||
mod commands;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 解析适配器目录。
|
||||
/// 优先级:环境变量 AGENTDOCK_ADAPTERS_DIR > CWD 相对路径(tauri dev 时 CWD=apps/desktop)
|
||||
/// > CARGO_MANIFEST_DIR 相对路径(仓库根)。
|
||||
fn adapters_dir() -> PathBuf {
|
||||
if let Ok(dir) = std::env::var("AGENTDOCK_ADAPTERS_DIR") {
|
||||
return PathBuf::from(dir);
|
||||
}
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let cargo_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let candidates = [
|
||||
cwd.join("../../adapters"),
|
||||
cargo_dir.join("../../../adapters"),
|
||||
];
|
||||
for c in candidates {
|
||||
if c.join("catalog.yaml").exists() {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
cargo_dir.join("../../../adapters")
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.setup(|_app| {
|
||||
// 自测标记:`tauri dev` 启动后日志出现该行即代表壳已就绪
|
||||
println!("[agentdock] window-ready");
|
||||
// Wave 0:初始化 SQLite 空库(架构 §2 状态存储层)
|
||||
let _ = agentdock_store::Store::open_default();
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::env::detect_env,
|
||||
commands::catalog::list_catalog,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
agentdock_desktop_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "AgentDock",
|
||||
"version": "0.1.0",
|
||||
"identifier": "cn.agentdock.desktop",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "AgentDock",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"minWidth": 960,
|
||||
"minHeight": 600,
|
||||
"center": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Sidebar, type PageKey, PAGE_META } from "./components/Sidebar";
|
||||
import { Header } from "./components/Header";
|
||||
import { OverviewPage } from "./pages/Overview";
|
||||
import { CatalogPage } from "./pages/Catalog";
|
||||
import { MyCliPage } from "./pages/MyCli";
|
||||
import { ConfigCenterPage } from "./pages/ConfigCenter";
|
||||
import { BackupPage } from "./pages/Backup";
|
||||
import { SettingsPage } from "./pages/Settings";
|
||||
import { useEnv } from "./hooks/useEnv";
|
||||
import type { PlatformEnv, RuntimeInfo } from "./ipc/types";
|
||||
|
||||
/** 平台相关运行时集合(Windows 不看 apt,Linux 不看 winget) */
|
||||
const WIN_RUNTIMES = ["node", "npm", "python", "uv", "git", "winget"] as const;
|
||||
const LINUX_RUNTIMES = ["node", "npm", "python", "uv", "git", "apt"] as const;
|
||||
|
||||
function envWarningCount(env: PlatformEnv | null): number {
|
||||
if (!env) return 0;
|
||||
const keys = env.os === "windows" ? WIN_RUNTIMES : LINUX_RUNTIMES;
|
||||
let n = 0;
|
||||
for (const k of keys) {
|
||||
const info = env.runtimes[k] as RuntimeInfo | null | undefined;
|
||||
if (info && info.status !== "installed") n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [page, setPage] = useState<PageKey>("overview");
|
||||
const { env } = useEnv();
|
||||
const warningCount = envWarningCount(env);
|
||||
|
||||
const content = useMemo(() => {
|
||||
switch (page) {
|
||||
case "overview":
|
||||
return <OverviewPage />;
|
||||
case "catalog":
|
||||
return <CatalogPage />;
|
||||
case "my-cli":
|
||||
return <MyCliPage />;
|
||||
case "config":
|
||||
return <ConfigCenterPage />;
|
||||
case "backup":
|
||||
return <BackupPage />;
|
||||
case "settings":
|
||||
return <SettingsPage />;
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div className="bg-grid grid-drift" aria-hidden="true" />
|
||||
<Sidebar current={page} onNavigate={setPage} />
|
||||
<div className="app-main">
|
||||
<Header title={PAGE_META[page].title} warningCount={warningCount} />
|
||||
<main className="app-content">
|
||||
{/* key=page 触发 §4.2 页面切换动效:translateY(8px)+淡入 */}
|
||||
<div key={page} className="page-enter">
|
||||
{content}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/** CLI 单色字母头像(视觉规范 §7:双字母单色,--ad-bg-2 底 + 1px --ad-border) */
|
||||
export function CliMonogram({
|
||||
name,
|
||||
size = 32,
|
||||
}: {
|
||||
name: string;
|
||||
size?: number;
|
||||
}) {
|
||||
const chars = name.replace(/\s+/g, "").slice(0, 2).toUpperCase();
|
||||
return (
|
||||
<span
|
||||
className="monogram"
|
||||
style={{ width: size, height: size, fontSize: Math.round(size * 0.42) }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{chars}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ChevronDown, Search } from "lucide-react";
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
/** 环境警告条数(0 时整颗 chip 不渲染,规范 §3.0) */
|
||||
warningCount?: number;
|
||||
}
|
||||
|
||||
/** 顶栏(视觉规范 §3.0):页面标题 + 搜索框 + 环境警告 chip */
|
||||
export function Header({ title, warningCount = 0 }: HeaderProps) {
|
||||
return (
|
||||
<header className="header">
|
||||
<h1 className="header-title">{title}</h1>
|
||||
<div className="header-actions">
|
||||
<div className="search-box">
|
||||
<Search size={16} strokeWidth={1.5} className="search-icon" aria-hidden="true" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="搜索 CLI…"
|
||||
aria-label="搜索 CLI"
|
||||
// 搜索功能属于后续波次,Wave 0 仅为占位
|
||||
/>
|
||||
</div>
|
||||
{warningCount > 0 && (
|
||||
<button type="button" className="env-chip">
|
||||
<span className="status-dot warn breathe" aria-hidden="true" />
|
||||
<span>{warningCount} 项环境警告</span>
|
||||
<ChevronDown size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export type KpiTone = "primary" | "attention" | "accent" | "warning";
|
||||
|
||||
const TONE_COLOR: Record<KpiTone, string> = {
|
||||
primary: "var(--ad-primary)",
|
||||
attention: "var(--ad-attention)",
|
||||
accent: "var(--ad-accent)",
|
||||
warning: "var(--ad-warning)",
|
||||
};
|
||||
|
||||
interface KpiCardProps {
|
||||
label: string;
|
||||
value: number;
|
||||
tone: KpiTone;
|
||||
/** 副标题:对象点名句式(视觉规范 §3.1.1) */
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* KPI 卡(视觉规范 §3.1.1):
|
||||
* - 径向渐变玻璃板 + 上缘高光 + --ad-shadow-panel
|
||||
* - 顶部色条贯通整宽;数值为 0 时色条降灰、数字回 --ad-text-1
|
||||
* - 数字随状态色贯穿
|
||||
*/
|
||||
export function KpiCard({ label, value, tone, subtitle }: KpiCardProps) {
|
||||
const color = value > 0 ? TONE_COLOR[tone] : "var(--ad-border)";
|
||||
const valueColor = value > 0 ? TONE_COLOR[tone] : "var(--ad-text-1)";
|
||||
return (
|
||||
<div className="kpi-card">
|
||||
<div className="kpi-bar" style={{ background: color }} aria-hidden="true" />
|
||||
<div className="kpi-body">
|
||||
<div className="kpi-value" style={{ color: valueColor }}>
|
||||
{value}
|
||||
</div>
|
||||
<div className="kpi-label">{label}</div>
|
||||
<div className="kpi-subtitle">{subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/** 内联等宽 chip(视觉规范 §2.2:文件名/版本号/命令专用) */
|
||||
export function MonoChip({ children }: { children: ReactNode }) {
|
||||
return <span className="mono-chip">{children}</span>;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
Archive,
|
||||
Gauge,
|
||||
Library,
|
||||
Settings,
|
||||
SlidersHorizontal,
|
||||
SquareTerminal,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
export type PageKey =
|
||||
| "overview"
|
||||
| "catalog"
|
||||
| "my-cli"
|
||||
| "config"
|
||||
| "backup"
|
||||
| "settings";
|
||||
|
||||
export interface PageMeta {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const PAGE_META: Record<PageKey, PageMeta> = {
|
||||
overview: { title: "总览" },
|
||||
catalog: { title: "CLI 目录" },
|
||||
"my-cli": { title: "我的 CLI" },
|
||||
config: { title: "配置中心" },
|
||||
backup: { title: "备份与迁移" },
|
||||
settings: { title: "设置" },
|
||||
};
|
||||
|
||||
interface NavItem {
|
||||
key: PageKey;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ key: "overview", label: "总览", icon: Gauge },
|
||||
{ key: "catalog", label: "CLI 目录", icon: Library },
|
||||
{ key: "my-cli", label: "我的 CLI", icon: SquareTerminal },
|
||||
{ key: "config", label: "配置中心", icon: SlidersHorizontal },
|
||||
{ key: "backup", label: "备份与迁移", icon: Archive },
|
||||
{ key: "settings", label: "设置", icon: Settings },
|
||||
];
|
||||
|
||||
const ENGINE_VERSION = "v0.1.0";
|
||||
|
||||
interface SidebarProps {
|
||||
current: PageKey;
|
||||
onNavigate: (page: PageKey) => void;
|
||||
}
|
||||
|
||||
/** 左侧竖导航 + 底部引擎状态区(视觉规范 §3.0) */
|
||||
export function Sidebar({ current, onNavigate }: SidebarProps) {
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<nav className="sidebar-nav" aria-label="主导航">
|
||||
{NAV_ITEMS.map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={current === key ? "nav-item active" : "nav-item"}
|
||||
onClick={() => onNavigate(key)}
|
||||
aria-current={current === key ? "page" : undefined}
|
||||
>
|
||||
<Icon size={20} strokeWidth={1.5} className="nav-icon" aria-hidden="true" />
|
||||
<span className="nav-label">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className="sidebar-engine">
|
||||
<span className="status-dot ok" aria-hidden="true" />
|
||||
<span className="engine-text">本地引擎 · 运行中</span>
|
||||
<span className="mono-chip engine-version">{ENGINE_VERSION}</span>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export type BadgeKind = "installed" | "update" | "auth" | "uninstalled";
|
||||
|
||||
const KIND_CLASS: Record<BadgeKind, string> = {
|
||||
installed: "badge badge-installed",
|
||||
update: "badge badge-update",
|
||||
auth: "badge badge-auth",
|
||||
uninstalled: "badge badge-uninstalled",
|
||||
};
|
||||
|
||||
interface StatusBadgeProps {
|
||||
kind: BadgeKind;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/** 状态角标(视觉规范 §3.1.4 / §3.2:已安装=青、可更新=品红+弱辉光、待授权=紫、未安装=灰) */
|
||||
export function StatusBadge({ kind, label }: StatusBadgeProps) {
|
||||
return <span className={KIND_CLASS[kind]}>{label}</span>;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { listCatalog } from "../ipc";
|
||||
import type { CatalogEntry } from "../ipc/types";
|
||||
|
||||
export interface UseCatalogResult {
|
||||
entries: CatalogEntry[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/** 加载适配器目录(listCatalog) */
|
||||
export function useCatalog(): UseCatalogResult {
|
||||
const [entries, setEntries] = useState<CatalogEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
listCatalog()
|
||||
.then((list) => {
|
||||
if (!cancelled) {
|
||||
setEntries(list);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled) {
|
||||
setError(String(err));
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { entries, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { detectEnv } from "../ipc";
|
||||
import type { PlatformEnv } from "../ipc/types";
|
||||
|
||||
export interface UseEnvResult {
|
||||
env: PlatformEnv | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/** 加载本机环境检测结果(一次性的只读快照) */
|
||||
export function useEnv(): UseEnvResult {
|
||||
const [env, setEnv] = useState<PlatformEnv | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
detectEnv()
|
||||
.then((e) => {
|
||||
if (!cancelled) {
|
||||
setEnv(e);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!cancelled) {
|
||||
setError(String(err));
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { env, loading, error };
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 类型化 invoke 封装(唯一前端 → Rust 入口,架构 §2「界面层禁直接拼命令」)
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { CatalogEntry, PlatformEnv } from "./types";
|
||||
|
||||
/** 是否运行在 Tauri 环境(否则走 mock,便于纯浏览器联调 UI) */
|
||||
function isTauri(): boolean {
|
||||
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
|
||||
}
|
||||
|
||||
export async function detectEnv(): Promise<PlatformEnv> {
|
||||
if (!isTauri()) {
|
||||
return mockPlatformEnv();
|
||||
}
|
||||
return invoke<PlatformEnv>("detectEnv");
|
||||
}
|
||||
|
||||
export async function listCatalog(): Promise<CatalogEntry[]> {
|
||||
if (!isTauri()) {
|
||||
return mockCatalog();
|
||||
}
|
||||
return invoke<CatalogEntry[]>("listCatalog");
|
||||
}
|
||||
|
||||
// ---- 浏览器联调用 mock(仅当不在 Tauri 内时生效,不影响真实数据) ----
|
||||
const mockCatalog = (): CatalogEntry[] => [
|
||||
{ id: "codex", name: "Codex CLI", name_zh: "Codex CLI", vendor: "OpenAI", status: "available" },
|
||||
{ id: "claude-code", name: "Claude Code", name_zh: "Claude Code", vendor: "Anthropic", status: "available" },
|
||||
{ id: "gemini", name: "Gemini CLI", name_zh: "Gemini CLI", vendor: "Google", status: "available" },
|
||||
{ id: "copilot", name: "Copilot CLI", name_zh: "Copilot CLI", vendor: "GitHub", status: "available" },
|
||||
{ id: "kimi", name: "Kimi CLI", name_zh: "Kimi CLI", vendor: "月之暗面", status: "available" },
|
||||
{ id: "qwen", name: "Qwen Code", name_zh: "Qwen Code", vendor: "阿里巴巴", status: "available" },
|
||||
{ id: "codebuddy", name: "CodeBuddy Code", name_zh: "CodeBuddy Code", vendor: "腾讯", status: "available" },
|
||||
{ id: "opencode", name: "OpenCode", name_zh: "OpenCode", vendor: "opencode.ai", status: "available" },
|
||||
{ id: "crush", name: "Crush", name_zh: "Crush", vendor: "Charm", status: "available" },
|
||||
{ id: "goose", name: "Goose", name_zh: "Goose", vendor: "Linux 基金会(AAIF)", status: "available" },
|
||||
{ id: "aider", name: "Aider", name_zh: "Aider", vendor: "aider.chat", status: "available" },
|
||||
{ id: "cursor", name: "Cursor CLI", name_zh: "Cursor CLI", vendor: "Cursor", status: "available" },
|
||||
{ id: "cline", name: "Cline", name_zh: "Cline", vendor: "Cline", status: "available" },
|
||||
{ id: "warp", name: "Warp Agent CLI", name_zh: "Warp Agent CLI", vendor: "Warp", status: "available" },
|
||||
];
|
||||
|
||||
const mockPlatformEnv = (): PlatformEnv => ({
|
||||
os: "windows",
|
||||
os_version: "Windows 11 24H2 (Build 26100)",
|
||||
arch: "x86_64",
|
||||
shells: { powershell_version: "5.1.26100.1", pwsh_version: null, bash_available: true },
|
||||
runtimes: {
|
||||
node: { status: "installed", version: "24.18.0", path: "C:\\Program Files\\nodejs\\node.exe" },
|
||||
npm: { status: "installed", version: "11.16.0", path: "C:\\Program Files\\nodejs\\npm.cmd" },
|
||||
python: { status: "installed", version: "3.14.6", path: "C:\\Python314\\python.exe" },
|
||||
uv: { status: "not_in_path", version: null, path: null },
|
||||
git: { status: "installed", version: "2.47.1", path: "C:\\Program Files\\Git\\cmd\\git.exe" },
|
||||
winget: { status: "installed", version: "1.8.1911", path: "C:\\Users\\MoBai\\AppData\\Local\\Microsoft\\WindowsApps\\winget.exe" },
|
||||
apt: { status: "not_in_path", version: null, path: null },
|
||||
},
|
||||
path_entries: ["C:\\Windows\\system32", "C:\\Windows", "C:\\Program Files\\nodejs"],
|
||||
capabilities: { keyring: "ok", can_elevate: false },
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
// IPC 契约类型(对齐架构 §5 PlatformEnv / listCatalog)
|
||||
// 字段命名与 Rust 侧 serde 输出一致(snake_case)。
|
||||
|
||||
export interface PlatformEnv {
|
||||
os: string;
|
||||
os_version: string;
|
||||
arch: string;
|
||||
shells: Shells;
|
||||
runtimes: Runtimes;
|
||||
path_entries: string[];
|
||||
capabilities: Capabilities;
|
||||
}
|
||||
|
||||
export interface Shells {
|
||||
powershell_version: string | null;
|
||||
pwsh_version: string | null;
|
||||
bash_available: boolean;
|
||||
}
|
||||
|
||||
export interface Runtimes {
|
||||
node: RuntimeInfo | null;
|
||||
npm: RuntimeInfo | null;
|
||||
python: RuntimeInfo | null;
|
||||
uv: RuntimeInfo | null;
|
||||
git: RuntimeInfo | null;
|
||||
winget: RuntimeInfo | null;
|
||||
apt: RuntimeInfo | null;
|
||||
}
|
||||
|
||||
export type RuntimeStatus =
|
||||
| "installed"
|
||||
| "not_installed"
|
||||
| "not_in_path"
|
||||
| "exec_failed"
|
||||
| "version_unparseable";
|
||||
|
||||
export interface RuntimeInfo {
|
||||
status: RuntimeStatus;
|
||||
version: string | null;
|
||||
path: string | null;
|
||||
}
|
||||
|
||||
export interface Capabilities {
|
||||
keyring: string;
|
||||
can_elevate: boolean;
|
||||
}
|
||||
|
||||
export interface CatalogEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
name_zh: string;
|
||||
vendor: string;
|
||||
status: string;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
// 设计 Token(视觉规范 v1.2,唯一品味依据)
|
||||
import "./tokens/color.css";
|
||||
import "./tokens/typography.css";
|
||||
import "./tokens/space.css";
|
||||
import "./tokens/motion.css";
|
||||
import "./tokens/effects.css";
|
||||
import "./tokens/tiers.css";
|
||||
import "./styles/global.css";
|
||||
|
||||
/** 性能档位自动判定(架构 §8.3 / 视觉规范 §5) */
|
||||
function applyFxTier(): void {
|
||||
const html = document.documentElement;
|
||||
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const cores = navigator.hardwareConcurrency ?? 8;
|
||||
if (reduce || cores <= 4) {
|
||||
html.dataset.fxTier = "low";
|
||||
}
|
||||
}
|
||||
|
||||
applyFxTier();
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Archive } from "lucide-react";
|
||||
|
||||
/** 备份与迁移(PRD §7):创建备份、查看内容、恢复。Wave 0 占位。 */
|
||||
export function BackupPage() {
|
||||
return (
|
||||
<div className="placeholder-page">
|
||||
<div className="placeholder-icon">
|
||||
<Archive size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||
</div>
|
||||
<h2 className="placeholder-title">备份与迁移</h2>
|
||||
<p className="placeholder-desc">
|
||||
备份功能在 Wave 4 落地。备份将明确标注是否包含敏感信息,密钥永不进备份。
|
||||
</p>
|
||||
<button type="button" className="btn btn-secondary" disabled>
|
||||
创建备份
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { useCatalog } from "../hooks/useCatalog";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
import { CliMonogram } from "../components/CliMonogram";
|
||||
|
||||
type StatusFilter = "all" | "installed" | "uninstalled" | "update";
|
||||
type PlatformFilter = "all" | "windows" | "linux";
|
||||
|
||||
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "installed", label: "已安装" },
|
||||
{ key: "uninstalled", label: "未安装" },
|
||||
{ key: "update", label: "可更新" },
|
||||
];
|
||||
|
||||
const PLATFORM_FILTERS: { key: PlatformFilter; label: string }[] = [
|
||||
{ key: "all", label: "全部平台" },
|
||||
{ key: "windows", label: "Windows" },
|
||||
{ key: "linux", label: "Linux" },
|
||||
];
|
||||
|
||||
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml) */
|
||||
export function CatalogPage() {
|
||||
const { entries, loading, error } = useCatalog();
|
||||
const [query, setQuery] = useState("");
|
||||
const [status, setStatus] = useState<StatusFilter>("all");
|
||||
const [platform, setPlatform] = useState<PlatformFilter>("all");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return entries.filter((e) => {
|
||||
if (q) {
|
||||
const haystack = `${e.name_zh} ${e.name} ${e.vendor} ${e.id}`.toLowerCase();
|
||||
if (!haystack.includes(q)) return false;
|
||||
}
|
||||
// Wave 0:14 个均为未安装;已安装/可更新筛选结果为空(诚实占位)
|
||||
if (status === "installed") return false;
|
||||
if (status === "update") return false;
|
||||
// 平台:全部 14 个均支持 Windows + Ubuntu(调研底稿结论),筛选不改变集合
|
||||
if (platform === "windows" || platform === "linux") return true;
|
||||
return true;
|
||||
});
|
||||
}, [entries, query, status, platform]);
|
||||
|
||||
return (
|
||||
<div className="catalog">
|
||||
<div className="catalog-toolbar">
|
||||
<div className="search-box">
|
||||
<Search size={16} strokeWidth={1.5} className="search-icon" aria-hidden="true" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="搜索 CLI…"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
aria-label="搜索 CLI"
|
||||
/>
|
||||
</div>
|
||||
<div className="filter-group" role="group" aria-label="状态筛选">
|
||||
{STATUS_FILTERS.map((f) => (
|
||||
<button
|
||||
key={f.key}
|
||||
type="button"
|
||||
className={status === f.key ? "pill active" : "pill"}
|
||||
onClick={() => setStatus(f.key)}
|
||||
>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="filter-group" role="group" aria-label="平台筛选">
|
||||
{PLATFORM_FILTERS.map((f) => (
|
||||
<button
|
||||
key={f.key}
|
||||
type="button"
|
||||
className={platform === f.key ? "pill active" : "pill"}
|
||||
onClick={() => setPlatform(f.key)}
|
||||
>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="panel-empty">目录加载失败:{error}</p>}
|
||||
{loading && !error && <p className="panel-empty">目录加载中…</p>}
|
||||
|
||||
<div className="catalog-grid">
|
||||
{filtered.map((entry) => (
|
||||
<article className="catalog-card" key={entry.id}>
|
||||
<div className="catalog-card-top">
|
||||
<CliMonogram name={entry.name} size={40} />
|
||||
<div className="catalog-card-head">
|
||||
<div className="catalog-card-name">{entry.name_zh}</div>
|
||||
<div className="catalog-card-vendor">{entry.vendor}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="catalog-card-desc">官方渠道安装 · 适配器随 Wave 1 落地</p>
|
||||
<div className="catalog-card-bottom">
|
||||
<StatusBadge kind="uninstalled" label="未安装" />
|
||||
<span className="catalog-platforms">Windows · Linux</span>
|
||||
<MonoChip>v1</MonoChip>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{!loading && filtered.length === 0 && (
|
||||
<p className="panel-empty catalog-empty">没有符合条件的 CLI</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SlidersHorizontal } from "lucide-react";
|
||||
|
||||
/** 配置中心(PRD §7):按 CLI 展示中文配置表单。Wave 0 占位。 */
|
||||
export function ConfigCenterPage() {
|
||||
return (
|
||||
<div className="placeholder-page">
|
||||
<div className="placeholder-icon">
|
||||
<SlidersHorizontal size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||
</div>
|
||||
<h2 className="placeholder-title">配置中心</h2>
|
||||
<p className="placeholder-desc">
|
||||
中文配置表单在 Wave 2 落地。敏感字段将使用密码输入与系统密钥库。
|
||||
</p>
|
||||
<button type="button" className="btn btn-secondary" disabled>
|
||||
选择 CLI 开始配置
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { SquareTerminal } from "lucide-react";
|
||||
|
||||
/** 我的 CLI(PRD §7):版本 / 路径 / 授权状态。Wave 0 尚无已安装 CLI,展示空态。 */
|
||||
export function MyCliPage() {
|
||||
return (
|
||||
<div className="placeholder-page">
|
||||
<div className="placeholder-icon">
|
||||
<SquareTerminal size={40} strokeWidth={1.5} aria-hidden="true" />
|
||||
</div>
|
||||
<h2 className="placeholder-title">暂无已安装 CLI</h2>
|
||||
<p className="placeholder-desc">
|
||||
安装功能在 Wave 1 开放。届时这里会展示每个 CLI 的版本、路径与授权状态。
|
||||
</p>
|
||||
<button type="button" className="btn btn-primary" disabled>
|
||||
去 CLI 目录看看
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { useState } from "react";
|
||||
import { Activity, Archive, ChevronRight, Lock, Plus } from "lucide-react";
|
||||
import { useEnv } from "../hooks/useEnv";
|
||||
import { useCatalog } from "../hooks/useCatalog";
|
||||
import { KpiCard } from "../components/KpiCard";
|
||||
import { MonoChip } from "../components/MonoChip";
|
||||
import { StatusBadge } from "../components/StatusBadge";
|
||||
import { CliMonogram } from "../components/CliMonogram";
|
||||
import type { PlatformEnv, RuntimeInfo } from "../ipc/types";
|
||||
|
||||
/** 平台相关运行时集合(Windows 不看 apt,Linux 不看 winget) */
|
||||
const WINDOWS_RUNTIMES = ["node", "npm", "python", "uv", "git", "winget"] as const;
|
||||
const LINUX_RUNTIMES = ["node", "npm", "python", "uv", "git", "apt"] as const;
|
||||
|
||||
function runtimeProblems(env: PlatformEnv): { name: string; info: RuntimeInfo }[] {
|
||||
const keys = env.os === "windows" ? WINDOWS_RUNTIMES : LINUX_RUNTIMES;
|
||||
const problems: { name: string; info: RuntimeInfo }[] = [];
|
||||
for (const key of keys) {
|
||||
const info = env.runtimes[key];
|
||||
if (info && info.status !== "installed") {
|
||||
problems.push({ name: key, info });
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
function runtimeStatusText(status: string): string {
|
||||
switch (status) {
|
||||
case "not_in_path":
|
||||
return "不在 PATH";
|
||||
case "not_installed":
|
||||
return "未安装";
|
||||
case "exec_failed":
|
||||
return "执行失败";
|
||||
case "version_unparseable":
|
||||
return "版本未知";
|
||||
default:
|
||||
return "未知";
|
||||
}
|
||||
}
|
||||
|
||||
function RuntimeItem({ name, info }: { name: string; info: RuntimeInfo }) {
|
||||
const dotClass =
|
||||
info.status === "installed"
|
||||
? "status-dot ok"
|
||||
: info.status === "exec_failed"
|
||||
? "status-dot error"
|
||||
: "status-dot warn";
|
||||
return (
|
||||
<div className="runtime-item">
|
||||
<span className={dotClass} aria-hidden="true" />
|
||||
<span className="runtime-name">{name}</span>
|
||||
{info.version ? (
|
||||
<MonoChip>{info.version}</MonoChip>
|
||||
) : (
|
||||
<span className="runtime-missing">{runtimeStatusText(info.status)}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EnvPanel() {
|
||||
const { env, loading, error } = useEnv();
|
||||
const [showAllPath, setShowAllPath] = useState(false);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="panel env-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">本机环境</h2>
|
||||
</div>
|
||||
<p className="panel-empty">环境检测失败:{error}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const pathPreview = env ? env.path_entries.slice(0, 8) : [];
|
||||
|
||||
return (
|
||||
<section className="panel env-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">本机环境</h2>
|
||||
{loading && <span className="panel-hint">检测中…</span>}
|
||||
</div>
|
||||
{env && (
|
||||
<>
|
||||
<div className="env-grid">
|
||||
<div className="env-basic">
|
||||
<div className="env-row">
|
||||
<span className="env-key">系统</span>
|
||||
<span className="env-val">{env.os_version}</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">架构</span>
|
||||
<span className="env-val">{env.arch}</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">Shell</span>
|
||||
<span className="env-val">
|
||||
{env.shells.powershell_version ? (
|
||||
<>PowerShell <MonoChip>{env.shells.powershell_version}</MonoChip></>
|
||||
) : null}
|
||||
{env.shells.pwsh_version ? (
|
||||
<> · pwsh <MonoChip>{env.shells.pwsh_version}</MonoChip></>
|
||||
) : null}
|
||||
{env.shells.bash_available ? " · Bash ✓" : ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">密钥库</span>
|
||||
<span className="env-val">
|
||||
{env.capabilities.keyring === "ok" ? "Credential Manager ✓" : "密钥库缺失"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="env-row">
|
||||
<span className="env-key">提权</span>
|
||||
<span className="env-val">{env.capabilities.can_elevate ? "可用" : "不可用"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="runtime-grid">
|
||||
{(["node", "npm", "python", "uv", "git", "winget", "apt"] as const).map(
|
||||
(key) => {
|
||||
const info = env.runtimes[key];
|
||||
if (!info) return null;
|
||||
return <RuntimeItem key={key} name={key} info={info} />;
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="path-block">
|
||||
<div className="path-head">
|
||||
<span className="path-title">
|
||||
PATH({env.path_entries.length} 条)
|
||||
<MonoChip>{env.path_entries.length}</MonoChip>
|
||||
</span>
|
||||
{env.path_entries.length > pathPreview.length && (
|
||||
<button
|
||||
type="button"
|
||||
className="link-btn"
|
||||
onClick={() => setShowAllPath((v) => !v)}
|
||||
>
|
||||
{showAllPath ? "收起" : "展开全部"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{(showAllPath ? env.path_entries : pathPreview).map((entry, idx) => (
|
||||
<div key={idx} className="path-entry">
|
||||
{entry}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/** 总览页(视觉规范 §3.1 + PRD §7):KPI + 最近诊断/快速操作 + CLI 状态网格 + 本机环境 */
|
||||
export function OverviewPage() {
|
||||
const { env } = useEnv();
|
||||
const { entries } = useCatalog();
|
||||
|
||||
const warningCount = env ? runtimeProblems(env).length : 0;
|
||||
const warningNames = env ? runtimeProblems(env).map((p) => p.name) : [];
|
||||
|
||||
return (
|
||||
<div className="overview">
|
||||
{/* ① KPI 一排 4 张 */}
|
||||
<section className="kpi-grid" aria-label="概览指标">
|
||||
<KpiCard label="已安装" value={0} tone="primary" subtitle="暂无已安装 CLI" />
|
||||
<KpiCard label="待授权" value={0} tone="attention" subtitle="暂无待授权项" />
|
||||
<KpiCard label="可更新" value={0} tone="accent" subtitle="暂无可用更新" />
|
||||
<KpiCard
|
||||
label="环境异常"
|
||||
value={warningCount}
|
||||
tone="warning"
|
||||
subtitle={warningCount > 0 ? warningNames.join(" · ") : "环境正常"}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{/* ② 中段:左 2/3 最近诊断 + 右 1/3 快速操作 */}
|
||||
<section className="overview-mid">
|
||||
<div className="panel diag-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">最近诊断</h2>
|
||||
<button type="button" className="link-btn">
|
||||
查看全部 <ChevronRight size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<ul className="diag-list">
|
||||
<li className="diag-row">
|
||||
<span className="status-dot ok" aria-hidden="true" />
|
||||
<span className="diag-cli">检测引擎</span>
|
||||
<span className="diag-text">诊断功能将在 Wave 1 接入</span>
|
||||
<span className="diag-time">刚刚</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="panel quick-card">
|
||||
<h2 className="panel-title">快速操作</h2>
|
||||
<button type="button" className="btn btn-primary" disabled>
|
||||
<Plus size={16} strokeWidth={1.5} aria-hidden="true" /> 添加 CLI
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" disabled>
|
||||
<Activity size={16} strokeWidth={1.5} aria-hidden="true" /> 立即诊断
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" disabled>
|
||||
<Archive size={16} strokeWidth={1.5} aria-hidden="true" /> 创建备份
|
||||
</button>
|
||||
<div className="recent-installs">
|
||||
<div className="group-label">最近安装</div>
|
||||
<p className="group-empty">暂无记录</p>
|
||||
</div>
|
||||
<p className="safety-note">
|
||||
<Lock size={12} strokeWidth={1.5} aria-hidden="true" />
|
||||
所有操作在本地完成,密钥只存系统保险柜,不上传。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ③ 下段:CLI 状态网格通栏 */}
|
||||
<section className="panel cli-status-panel">
|
||||
<div className="panel-head">
|
||||
<h2 className="panel-title">CLI 状态</h2>
|
||||
<button type="button" className="link-btn">
|
||||
管理全部 {entries.length} 个
|
||||
<ChevronRight size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="cli-status-grid">
|
||||
{entries.map((entry) => (
|
||||
<div className="cli-block" key={entry.id}>
|
||||
<div className="cli-block-top">
|
||||
<CliMonogram name={entry.name} size={32} />
|
||||
<span className="cli-block-name">{entry.name_zh}</span>
|
||||
<StatusBadge kind="uninstalled" label="未安装" />
|
||||
</div>
|
||||
<div className="cli-block-version">
|
||||
<MonoChip>—</MonoChip>
|
||||
</div>
|
||||
<div className="cli-block-auth">
|
||||
<span className="status-dot unknown" aria-hidden="true" />
|
||||
<span>未检测 · 可一键安装</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ④ 本机环境(真实检测结果,架构 §5) */}
|
||||
<EnvPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Settings } from "lucide-react";
|
||||
|
||||
/** 设置(PRD §7):下载源策略 / 自动检查更新 / 语言 / 日志与隐私。Wave 0 静态占位。 */
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<h2 className="panel-title">常规</h2>
|
||||
<div className="settings-group">
|
||||
<div className="setting-row">
|
||||
<div className="setting-info">
|
||||
<div className="setting-label">下载源策略</div>
|
||||
<div className="setting-hint">选择 CLI 安装时的首选下载渠道(Wave 1 生效)</div>
|
||||
</div>
|
||||
<span className="setting-value">官方渠道</span>
|
||||
</div>
|
||||
<div className="setting-row">
|
||||
<div className="setting-info">
|
||||
<div className="setting-label">自动检查更新</div>
|
||||
<div className="setting-hint">启动时检查 AgentDock 自身与 CLI 的更新(Wave 4 生效)</div>
|
||||
</div>
|
||||
<span className="setting-value">开</span>
|
||||
</div>
|
||||
<div className="setting-row">
|
||||
<div className="setting-info">
|
||||
<div className="setting-label">语言</div>
|
||||
<div className="setting-hint">第一版仅提供简体中文</div>
|
||||
</div>
|
||||
<span className="setting-value">简体中文</span>
|
||||
</div>
|
||||
<div className="setting-row">
|
||||
<div className="setting-info">
|
||||
<div className="setting-label">日志与隐私</div>
|
||||
<div className="setting-hint">日志本地保存,敏感值自动脱敏(Wave 1 生效)</div>
|
||||
</div>
|
||||
<span className="setting-value">默认</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-page-foot">
|
||||
<Settings size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||
AgentDock v0.1.0 · 本地运行 · 不上传任何数据
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* ============================================================
|
||||
* 设计 Token · 配色(color)
|
||||
* 来源:视觉规范 v1.2 §2.1(施工唯一品味依据)
|
||||
* 禁止在此文件之外出现任何魔法色值;改动需先征得总管同意。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
/* ---- 底色(深空阶梯,由深到浅)---- */
|
||||
--ad-bg-0: #070B14; /* 应用窗口底色 */
|
||||
--ad-bg-1: #0D1420; /* 面板、卡片底色 */
|
||||
--ad-bg-2: #131C2E; /* 浮层、弹窗、下拉、mono chip 底 */
|
||||
--ad-bg-3: #1A2438; /* 悬浮态、按下态的填充 */
|
||||
--ad-border: #22304A; /* 常规 1px 分隔线 */
|
||||
--ad-border-bright: #35507A; /* 悬浮/选中的边框 */
|
||||
|
||||
/* ---- 霓虹强调色(全系统只有这两个霓虹色)---- */
|
||||
--ad-primary: #00E5FF; /* 主操作、链接、聚焦、选中态(青) */
|
||||
--ad-primary-hover: #4DEFFF; /* 主操作悬浮 */
|
||||
--ad-primary-pressed: #00B4CC; /* 主操作按下 */
|
||||
--ad-primary-dim: rgba(0, 229, 255, 0.12); /* 选中项底、标签底 */
|
||||
--ad-accent: #FF4DDB; /* 可更新标记、重要提醒(品红,克制使用) */
|
||||
--ad-accent-dim: rgba(255, 77, 219, 0.12); /* 品红标签底 */
|
||||
|
||||
/* ---- 状态色(诊断四级 + 通用语义,六色语义表 v1.2 定稿)---- */
|
||||
--ad-success: #2EE6A8; /* 正常 / 已安装 / 已授权 / 已自动修复 */
|
||||
--ad-attention: #A78BFA; /* 需要留意:提示级 + 待授权 / 未授权(紫) */
|
||||
--ad-warning: #FFB020; /* 警告 / 需要你注意的异常 */
|
||||
--ad-danger: #FF4D5E; /* 错误 / 需要你处理 / 高危操作 */
|
||||
--ad-success-dim: rgba(46, 230, 168, 0.12); /* 状态色 12% 透明底色 */
|
||||
--ad-attention-dim: rgba(167, 139, 250, 0.12); /* 状态色 12% 透明底色 */
|
||||
--ad-warning-dim: rgba(255, 176, 32, 0.12); /* 状态色 12% 透明底色 */
|
||||
--ad-danger-dim: rgba(255, 77, 94, 0.12); /* 状态色 12% 透明底色 */
|
||||
|
||||
/* ---- 文字 ---- */
|
||||
--ad-text-1: #E8EEF7; /* 主文案 */
|
||||
--ad-text-2: #9AA8BD; /* 次要说明(承载信息的最低允许色,对比度 ≥ 4.5:1) */
|
||||
--ad-text-3: #5E6E87; /* 仅占位、禁用、纯装饰(禁止承载信息) */
|
||||
|
||||
/* ---- 侧边栏选中态(v1.2 §3.0:左缘生长光晕,无独立指示条)---- */
|
||||
--ad-nav-selected-bg: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 229, 255, 0.30),
|
||||
rgba(0, 229, 255, 0.12) 45%,
|
||||
rgba(0, 229, 255, 0.05)
|
||||
);
|
||||
--ad-nav-selected-border: inset 0 0 0 1px rgba(0, 229, 255, 0.38);
|
||||
|
||||
/* ---- 组件表面(§3.0 / §3.1 明示值)---- */
|
||||
--ad-surface-card: radial-gradient(120% 100% at 50% 0%, #16233A 0%, #0D1420 60%); /* KPI 卡悬浮玻璃板 */
|
||||
--ad-card-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06); /* 卡片上缘高光 */
|
||||
--ad-warning-chip-border: rgba(255, 176, 32, 0.5); /* 环境警告 chip 描边 */
|
||||
|
||||
/* ---- 状态角标描边(§3.1.4 / §3.2 明示值)---- */
|
||||
--ad-primary-border: rgba(0, 229, 255, 0.4);
|
||||
--ad-accent-border: rgba(255, 77, 219, 0.5);
|
||||
--ad-attention-border: rgba(167, 139, 250, 0.5);
|
||||
--ad-success-border: rgba(46, 230, 168, 0.4);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* ============================================================
|
||||
* 设计 Token · 阴影与辉光(effects)
|
||||
* 来源:视觉规范 v1.2 §2.4 / §2.6 / §3.0
|
||||
* 辉光只给:主按钮、选中导航项、聚焦输入框、需处理状态指示灯、
|
||||
* 可更新角标、环境警告 chip。列表每行都发光 = 全部不发光,禁止。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
/* ---- 阴影 ---- */
|
||||
--ad-shadow-panel: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
--ad-shadow-pop: 0 12px 40px rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* ---- 辉光 ---- */
|
||||
--ad-glow-primary: 0 0 12px rgba(0, 229, 255, 0.35), 0 0 32px rgba(0, 229, 255, 0.12);
|
||||
--ad-glow-accent: 0 0 12px rgba(255, 77, 219, 0.35);
|
||||
--ad-glow-warning: 0 0 10px rgba(255, 176, 32, 0.28); /* 仅警告 chip / 警告状态灯 */
|
||||
--ad-glow-danger: 0 0 12px rgba(255, 77, 94, 0.35);
|
||||
|
||||
/* ---- 主按钮(§2.6,v1.2 hover 双层扩散辉光)---- */
|
||||
--ad-btn-primary-bg: linear-gradient(180deg, #33D9EE 0%, #06BCD9 55%, #049EBB 100%);
|
||||
--ad-btn-primary-text: #03252E;
|
||||
--ad-btn-primary-inner: inset 0 1px 0 rgba(255, 255, 255, 0.30),
|
||||
inset 0 -1px 0 rgba(2, 20, 26, 0.35);
|
||||
--ad-btn-primary-glow: 0 0 10px rgba(0, 229, 255, 0.22);
|
||||
--ad-btn-primary-glow-hover: 0 0 14px rgba(0, 229, 255, 0.30),
|
||||
0 0 36px rgba(0, 229, 255, 0.12);
|
||||
|
||||
/* 品红更新按钮(全应用唯一允许品红做按钮的场景) */
|
||||
--ad-btn-accent-bg: linear-gradient(180deg, #F26AD9 0%, #D633B8 55%, #B5209C 100%);
|
||||
--ad-btn-accent-glow-hover: 0 0 14px rgba(255, 77, 219, 0.30),
|
||||
0 0 36px rgba(255, 77, 219, 0.12);
|
||||
|
||||
/* ---- 背景网格(§3.0:要么可见要么不做)---- */
|
||||
--ad-grid-color: rgba(0, 229, 255, 0.05); /* 透明度固定 5%,施工员不许再调 */
|
||||
--ad-grid-size: 40px;
|
||||
--ad-grid-mask: radial-gradient(120% 90% at 50% 40%, #000 30%, transparent 75%);
|
||||
|
||||
/* ---- 弹窗遮罩(§3.6)---- */
|
||||
--ad-modal-mask: rgba(4, 7, 13, 0.7);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* ============================================================
|
||||
* 设计 Token · 动效(motion)
|
||||
* 来源:视觉规范 v1.2 §2.5 / §4
|
||||
* 通则:只动 transform 与 opacity;辉光可用 box-shadow 过渡但仅限单个聚焦元素。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
/* ---- 缓动曲线 ---- */
|
||||
--ad-ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* 进场、浮现,主力曲线 */
|
||||
--ad-ease-inout: cubic-bezier(0.65, 0, 0.35, 1); /* 展开收起、位移、呼吸 */
|
||||
--ad-ease-linear: linear; /* 仅环境循环动画 */
|
||||
|
||||
/* ---- 时长 ---- */
|
||||
--ad-dur-micro: 120ms; /* 按下、勾选 */
|
||||
--ad-dur-fast: 200ms; /* 悬浮、淡入淡出 */
|
||||
--ad-dur-med: 320ms; /* 面板展开、页面切换 */
|
||||
--ad-dur-slow: 600ms; /* 仅入场编排,一次性 */
|
||||
|
||||
/* ---- 状态点呼吸(§4.5)---- */
|
||||
--ad-breathe-duration: 2400ms;
|
||||
--ad-breathe-min-opacity: 0.4; /* 透明度下限 40%,再低像灭了 */
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* ============================================================
|
||||
* 设计 Token · 间距 / 圆角(space)
|
||||
* 来源:视觉规范 v1.2 §2.3
|
||||
* 赛博感靠小圆角 + 发光描边,不做大圆角。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
/* ---- 间距(4px 基数)---- */
|
||||
--ad-space-1: 4px;
|
||||
--ad-space-2: 8px;
|
||||
--ad-space-3: 12px;
|
||||
--ad-space-4: 16px;
|
||||
--ad-space-5: 20px;
|
||||
--ad-space-6: 24px;
|
||||
--ad-space-8: 32px;
|
||||
--ad-space-10: 40px;
|
||||
|
||||
/* ---- 圆角 ---- */
|
||||
--ad-radius-s: 4px; /* 标签、小按钮、chip */
|
||||
--ad-radius-m: 8px; /* 卡片、输入框、弹窗 */
|
||||
--ad-radius-l: 12px; /* 主面板 */
|
||||
|
||||
/* ---- 描边 ---- */
|
||||
--ad-border-width: 1px; /* 描边统一 1px;发光描边 = 1px 实色 + 外辉光,不加粗 */
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/* ============================================================
|
||||
* 设计 Token · 性能档位(tiers)
|
||||
* 来源:视觉规范 v1.2 §5(性能降级策略)/ 架构 §8.3
|
||||
*
|
||||
* 三档:高 / 中 / 低,默认 balanced(自动降档由 JS 判定后设置
|
||||
* data-fx-tier 属性,见 src/main.tsx)。
|
||||
* - prefers-reduced-motion 强制 low
|
||||
* - low:仅 L0 + 必要过渡,无循环动画、无辉光、无切角
|
||||
* ============================================================ */
|
||||
:root {
|
||||
--ad-fx-tier: balanced;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--ad-fx-tier: low;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 低档:移除辉光、循环动画、过渡、切角 ---- */
|
||||
[data-fx-tier="low"] *,
|
||||
[data-fx-tier="low"] *::before,
|
||||
[data-fx-tier="low"] *::after {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
[data-fx-tier="low"] .clip-notch {
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
/* ---- 中档:只保留单层辉光(半径减半),循环动画停用 ---- */
|
||||
[data-fx-tier="balanced"] .fx-multilayer-glow {
|
||||
box-shadow: 0 0 6px rgba(0, 229, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 背景网格漂移(§4.8,60s 一周,仅高档/中档) */
|
||||
[data-fx-tier="low"] .grid-drift {
|
||||
animation: none;
|
||||
}
|
||||
.grid-drift {
|
||||
animation: grid-drift 60s linear infinite;
|
||||
}
|
||||
@keyframes grid-drift {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 40px 40px; }
|
||||
}
|
||||
|
||||
/* 状态点呼吸(§4.5:仅警告/错误/授权可能过期呼吸;低档停用) */
|
||||
[data-fx-tier="low"] .status-dot-breathe {
|
||||
animation: none;
|
||||
}
|
||||
.status-dot-breathe {
|
||||
animation: ad-breathe var(--ad-dur-slow) var(--ad-ease-inout) infinite;
|
||||
}
|
||||
@keyframes ad-breathe {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: var(--ad-breathe-min-opacity); }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* ============================================================
|
||||
* 设计 Token · 字体(typography)
|
||||
* 来源:视觉规范 v1.2 §2.2
|
||||
* 不加任何外联 Web 字体(离线可用、安装包不膨胀)。
|
||||
* ============================================================ */
|
||||
:root {
|
||||
/* ---- 字体栈 ---- */
|
||||
--ad-font-ui: "PingFang SC", "Microsoft YaHei UI", "Microsoft YaHei",
|
||||
"Noto Sans CJK SC", "Source Han Sans SC", system-ui, sans-serif;
|
||||
--ad-font-mono: "Cascadia Code", "JetBrains Mono", Consolas,
|
||||
"Noto Sans Mono CJK SC", monospace;
|
||||
|
||||
/* ---- 字阶(字号 / 行高 / 字重)---- */
|
||||
--ad-text-xs-size: 12px; --ad-text-xs-height: 18px; --ad-text-xs-weight: 400;
|
||||
--ad-text-s-size: 13px; --ad-text-s-height: 20px; --ad-text-s-weight: 400;
|
||||
--ad-text-m-size: 14px; --ad-text-m-height: 22px; --ad-text-m-weight: 400;
|
||||
--ad-text-l-size: 16px; --ad-text-l-height: 24px; --ad-text-l-weight: 500;
|
||||
--ad-text-xl-size: 20px; --ad-text-xl-height: 28px; --ad-text-xl-weight: 600;
|
||||
--ad-text-num-size: 28px; --ad-text-num-height: 36px; --ad-text-num-weight: 600;
|
||||
|
||||
/* ---- Mono chip(§2.2 对比度三件套之二)----
|
||||
* 正文/列表中内联出现的文件名、版本号、命令一律套此样式:
|
||||
* 12px 等宽、--ad-bg-2 底、1px --ad-border、--ad-radius-s、内边距 1px 6px、文字 --ad-text-1 */
|
||||
--ad-mono-chip-font: var(--ad-font-mono);
|
||||
--ad-mono-chip-size: 12px;
|
||||
--ad-mono-chip-bg: var(--ad-bg-2);
|
||||
--ad-mono-chip-border: 1px solid var(--ad-border);
|
||||
--ad-mono-chip-radius: var(--ad-radius-s);
|
||||
--ad-mono-chip-padding: 1px 6px;
|
||||
--ad-mono-chip-color: var(--ad-text-1);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
|
||||
// Tauri 开发约定:固定端口 1420,避免占用冲突
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 不监听 src-tauri,避免 Rust 改动触发前端重载
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
});
|
||||