feat: add repository management dialogs and store functionality

- Implement CreateRepoDialog for creating new repositories with form validation.
- Implement EditRepoDialog for editing existing repository details.
- Implement SyncRepoDialog for syncing repositories with Gitea, including repository creation if necessary.
- Implement WorkspaceDetailDialog for managing workspace links and actions.
- Enhance the repo store with new state management for repository actions, including creating, editing, and syncing repositories.
- Add build configuration utilities for repository synchronization.
- Create a new page for repository management, integrating all dialogs and functionalities.
- Add login route for authentication.
This commit is contained in:
2026-02-25 01:02:55 +08:00
parent f4643464ba
commit 7ec6428643
32 changed files with 3303 additions and 71 deletions

View File

@@ -0,0 +1,77 @@
import { useGiteaConfigStore } from './store';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { giteaConfigSchema } from './store/schema';
import { toast } from 'sonner';
export const GiteaConfigPage = () => {
const { config, setConfig, resetConfig } = useGiteaConfigStore();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const result = giteaConfigSchema.safeParse(config);
if (result.success) {
toast.success('Gitea 配置已保存');
setTimeout(() => {
location.reload();
}, 400);
} else {
console.error('验证错误:', result.error.format());
toast.error('配置验证失败');
}
};
const handleChange = (field: keyof typeof config, value: string) => {
setConfig({ [field]: value });
};
return (
<div className="container mx-auto max-w-2xl py-8">
<Card>
<CardHeader>
<CardTitle>Gitea </CardTitle>
<CardDescription>
Gitea API
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="gitea-url">Gitea </Label>
<Input
id="gitea-url"
type="url"
value={config.GITEA_URL}
onChange={(e) => handleChange('GITEA_URL', e.target.value)}
placeholder="https://git.xiongxiao.me"
/>
</div>
<div className="space-y-2">
<Label htmlFor="gitea-token">Gitea Token</Label>
<Input
id="gitea-token"
type="password"
value={config.GITEA_TOKEN}
onChange={(e) => handleChange('GITEA_TOKEN', e.target.value)}
placeholder="请输入您的 Gitea Access Token"
/>
</div>
<div className="flex gap-4">
<Button type="submit"></Button>
<Button type="button" variant="outline" onClick={resetConfig}>
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
};
export default GiteaConfigPage;

View File

@@ -0,0 +1,50 @@
import { create } from 'zustand';
import type { GiteaConfig } from './schema';
type GiteaConfigState = {
config: GiteaConfig;
setConfig: (config: Partial<GiteaConfig>) => void;
resetConfig: () => void;
};
const DEFAULT_CONFIG: GiteaConfig = {
GITEA_TOKEN: '',
GITEA_URL: 'https://git.xiongxiao.me',
};
const loadInitialConfig = (): GiteaConfig => {
try {
const token = localStorage.getItem('GITEA_TOKEN') || '';
const url = localStorage.getItem('GITEA_URL') || DEFAULT_CONFIG.GITEA_URL;
return {
GITEA_TOKEN: token,
GITEA_URL: url,
};
} catch {
// Ignore parse errors
}
return DEFAULT_CONFIG;
};
const saveConfig = (config: GiteaConfig) => {
try {
localStorage.setItem('GITEA_TOKEN', config.GITEA_TOKEN);
localStorage.setItem('GITEA_URL', config.GITEA_URL);
} catch (error) {
console.error('Failed to save config:', error);
}
};
export const useGiteaConfigStore = create<GiteaConfigState>()((set) => ({
config: loadInitialConfig(),
setConfig: (newConfig) =>
set((state) => {
const updatedConfig = { ...state.config, ...newConfig };
saveConfig(updatedConfig);
return { config: updatedConfig };
}),
resetConfig: () => {
saveConfig(DEFAULT_CONFIG);
return set({ config: DEFAULT_CONFIG });
},
}));

View File

@@ -0,0 +1,13 @@
import { z } from 'zod';
export const giteaConfigSchema = z.object({
GITEA_TOKEN: z.string().min(1, 'Gitea Token is required'),
GITEA_URL: z.url('Must be a valid URL'),
});
export type GiteaConfig = z.infer<typeof giteaConfigSchema>;
export const defaultGiteaConfig: GiteaConfig = {
GITEA_TOKEN: '',
GITEA_URL: 'https://git.xiongxiao.me',
};

View File

@@ -0,0 +1,16 @@
import { useEffect } from "react";
import { useConfigStore } from "../store";
import { useNavigate } from "@tanstack/react-router";
export const useCheckConfig = () => {
const navigate = useNavigate();
useEffect(() => {
const config = useConfigStore.getState().config;
if (!config.CNB_API_KEY) {
navigate({
to: '/config'
})
}
}, [])
}

187
src/pages/config/page.tsx Normal file
View File

@@ -0,0 +1,187 @@
import { useConfigStore } from './store';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Info } from 'lucide-react';
import { configSchema } from './store/schema';
import { toast } from 'sonner';
export const ConfigPage = () => {
const { config, setConfig, resetConfig } = useConfigStore();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const result = configSchema.safeParse(config);
if (result.success) {
toast.success('配置已保存')
setTimeout(() => {
location.reload()
}, 400)
} else {
console.error('验证错误:', result.error.format());
}
};
const handleChange = (field: keyof typeof config, value: string | boolean) => {
setConfig({ [field]: value });
};
return (
<TooltipProvider>
<div className="container mx-auto max-w-2xl py-8">
<Card>
<CardHeader>
<CardTitle>CNB </CardTitle>
<CardDescription>
CNB API
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor="api-key">API </Label>
<Tooltip>
<TooltipTrigger>
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
访 CNB API
<a
href="https://cnb.cool/profile/token"
target="_blank"
rel="noopener noreferrer"
className="underline ml-1 hover:text-blue-400"
>
</a>
</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="api-key"
type="text"
value={config.CNB_API_KEY}
onChange={(e) => handleChange('CNB_API_KEY', e.target.value)}
placeholder="请输入您的 CNB API 密钥"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor="cookie">Cookie</Label>
<Tooltip>
<TooltipTrigger>
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cookie
<a
href="https://cnb.cool/kevisual/cnb-live-extension"
target="_blank"
rel="noopener noreferrer"
className="underline ml-1 hover:text-blue-400"
>
</a>
</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="cookie"
type="text"
value={config.CNB_COOKIE}
onChange={(e) => handleChange('CNB_COOKIE', e.target.value)}
placeholder="请输入您的 CNB Cookie"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cors-url"></Label>
<Input
id="cors-url"
type="url"
value={config.CNB_CORS_URL}
onChange={(e) => handleChange('CNB_CORS_URL', e.target.value)}
placeholder="https://cors.example.com"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-cors"
checked={config.ENABLE_CORS}
onCheckedChange={(checked) => handleChange('ENABLE_CORS', checked === true)}
/>
<Label htmlFor="enable-cors" className="cursor-pointer">
</Label>
</div>
<div className="space-y-2">
<Label htmlFor="ai-base-url">AI </Label>
<Input
id="ai-base-url"
type="url"
value={config.AI_BASE_URL}
onChange={(e) => handleChange('AI_BASE_URL', e.target.value)}
placeholder="请输入 AI 基础地址"
/>
</div>
<div className="space-y-2">
<Label htmlFor="ai-model">AI </Label>
<Input
id="ai-model"
type="text"
value={config.AI_MODEL}
onChange={(e) => handleChange('AI_MODEL', e.target.value)}
placeholder="请输入 AI 模型名称"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor="ai-api-key">AI </Label>
<Tooltip>
<TooltipTrigger>
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
使 CNB AI API
</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="ai-api-key"
type="password"
value={config.AI_API_KEY}
onChange={(e) => handleChange('AI_API_KEY', e.target.value)}
placeholder="请输入您的 AI API 密钥"
/>
</div>
<div className="flex gap-4">
<Button type="submit"></Button>
<Button type="button" variant="outline" onClick={resetConfig}>
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
</TooltipProvider>
);
};
export default ConfigPage;

View File

@@ -0,0 +1,51 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { Config, defaultConfig } from './schema';
type ConfigState = {
config: Config;
setConfig: (config: Partial<Config>) => void;
resetConfig: () => void;
};
const STORAGE_KEY = 'cnb-config';
const DEFAULT_CONFIG = {
CNB_API_KEY: '',
CNB_COOKIE: '',
CNB_CORS_URL: 'https://cors.kevisual.cn',
ENABLE_CORS: true,
AI_BASE_URL: 'https://api.cnb.cool/kevisual/cnb-ai/-/ai/',
AI_MODEL: 'CNB-Models',
AI_API_KEY: ''
}
const loadInitialConfig = (): Config => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
}
} catch {
// Ignore parse errors
}
return DEFAULT_CONFIG;
};
export const useConfigStore = create<ConfigState>()(
persist(
(set) => ({
config: loadInitialConfig(),
setConfig: (newConfig) =>
set((state) => ({
config: { ...state.config, ...newConfig },
})),
resetConfig: () =>
set({
config: DEFAULT_CONFIG,
}),
}),
{
name: STORAGE_KEY,
}
)
);

View File

@@ -0,0 +1,23 @@
import { z } from 'zod';
export const configSchema = z.object({
CNB_API_KEY: z.string().min(1, 'API Key is required'),
CNB_COOKIE: z.string().min(1, 'Cookie is required'),
CNB_CORS_URL: z.url('Must be a valid URL'),
ENABLE_CORS: z.boolean(),
AI_BASE_URL: z.url('Must be a valid URL'),
AI_MODEL: z.string().min(1, 'AI Model is required'),
AI_API_KEY: z.string().min(1, 'AI API Key is required'),
});
export type Config = z.infer<typeof configSchema>;
export const defaultConfig: Config = {
CNB_API_KEY: '',
CNB_COOKIE: '',
CNB_CORS_URL: 'https://cors.kevisual.cn',
ENABLE_CORS: true,
AI_BASE_URL: '',
AI_MODEL: '',
AI_API_KEY: ''
};