generated from kevisual/vite-react-template
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:
77
src/pages/config/gitea/page.tsx
Normal file
77
src/pages/config/gitea/page.tsx
Normal 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;
|
||||
50
src/pages/config/gitea/store/index.ts
Normal file
50
src/pages/config/gitea/store/index.ts
Normal 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 });
|
||||
},
|
||||
}));
|
||||
13
src/pages/config/gitea/store/schema.ts
Normal file
13
src/pages/config/gitea/store/schema.ts
Normal 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',
|
||||
};
|
||||
16
src/pages/config/hooks/check.ts
Normal file
16
src/pages/config/hooks/check.ts
Normal 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
187
src/pages/config/page.tsx
Normal 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;
|
||||
51
src/pages/config/store/index.ts
Normal file
51
src/pages/config/store/index.ts
Normal 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,
|
||||
}
|
||||
)
|
||||
);
|
||||
23
src/pages/config/store/schema.ts
Normal file
23
src/pages/config/store/schema.ts
Normal 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: ''
|
||||
};
|
||||
Reference in New Issue
Block a user