feat: 添加远端配置的保存和加载功能,优化仓库卡片显示主题和可见性

This commit is contained in:
2026-02-25 01:26:01 +08:00
parent 7ec6428643
commit 635e6a8a1b
5 changed files with 153 additions and 96 deletions

View File

@@ -99,5 +99,5 @@ export const useLayoutStore = create<LayoutStore>((set, get) => ({
setLoginPageConfig: (config) => set((state) => ({ setLoginPageConfig: (config) => set((state) => ({
loginPageConfig: { ...state.loginPageConfig, ...config }, loginPageConfig: { ...state.loginPageConfig, ...config },
})), })),
links: [{ title: '首页', href: '/', key: 'home' }], links: [{ title: '', href: '/', key: 'home' }],
})); }));

View File

@@ -8,10 +8,13 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import { configSchema } from './store/schema'; import { configSchema } from './store/schema';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useLayoutStore } from '../auth/store';
import { useShallow } from 'zustand/shallow';
import { queryLogin } from '@/modules/query';
export const ConfigPage = () => { export const ConfigPage = () => {
const { config, setConfig, resetConfig } = useConfigStore(); const { config, setConfig, resetConfig } = useConfigStore();
const layoutStore = useLayoutStore(useShallow(state => ({ me: state.me })))
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const result = configSchema.safeParse(config); const result = configSchema.safeParse(config);
@@ -28,6 +31,42 @@ export const ConfigPage = () => {
const handleChange = (field: keyof typeof config, value: string | boolean) => { const handleChange = (field: keyof typeof config, value: string | boolean) => {
setConfig({ [field]: value }); setConfig({ [field]: value });
}; };
const saveToRemote = async () => {
const _config = config;
const res = await queryLogin.post({
path: 'config',
key: 'update',
data: {
key: 'cnb_center_config.json',
data: _config,
}
});
if (res.code === 200) {
toast.success('保存到远端成功')
} else {
toast.error('保存到远端失败')
}
}
const loadFromRemote = async () => {
const res = await queryLogin.post({
path: 'config',
key: 'get',
data: {
key: 'cnb_center_config.json',
}
})
if (res.code === 404) {
toast.error('远端配置不存在')
return;
}
if (res.code === 200) {
const config = res.data?.data as typeof config;
setConfig(config);
toast.success('获取远端配置成功')
}
}
return ( return (
<TooltipProvider> <TooltipProvider>
@@ -51,9 +90,9 @@ export const ConfigPage = () => {
<TooltipContent> <TooltipContent>
<p> <p>
访 CNB API 访 CNB API
<a <a
href="https://cnb.cool/profile/token" href="https://cnb.cool/profile/token"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="underline ml-1 hover:text-blue-400" className="underline ml-1 hover:text-blue-400"
> >
@@ -82,9 +121,9 @@ export const ConfigPage = () => {
<TooltipContent> <TooltipContent>
<p> <p>
Cookie Cookie
<a <a
href="https://cnb.cool/kevisual/cnb-live-extension" href="https://cnb.cool/kevisual/cnb-live-extension"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="underline ml-1 hover:text-blue-400" className="underline ml-1 hover:text-blue-400"
> >
@@ -103,83 +142,92 @@ export const ConfigPage = () => {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cors-url"></Label> <Label htmlFor="cors-url"></Label>
<Input <Input
id="cors-url" id="cors-url"
type="url" type="url"
value={config.CNB_CORS_URL} value={config.CNB_CORS_URL}
onChange={(e) => handleChange('CNB_CORS_URL', e.target.value)} onChange={(e) => handleChange('CNB_CORS_URL', e.target.value)}
placeholder="https://cors.example.com" 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> </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"> <div className="flex items-center space-x-2">
<Button type="submit"></Button> <Checkbox
<Button type="button" variant="outline" onClick={resetConfig}> id="enable-cors"
checked={config.ENABLE_CORS}
</Button> onCheckedChange={(checked) => handleChange('ENABLE_CORS', checked === true)}
</div> />
</form> <Label htmlFor="enable-cors" className="cursor-pointer">
</CardContent>
</Card> </Label>
</div> </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>
{layoutStore.me && <>
<Button type="button" variant="outline" onClick={loadFromRemote}>
</Button>
<Button type="button" variant="outline" onClick={saveToRemote}>
</Button>
</>
}
</div>
</form>
</CardContent>
</Card>
</div>
</TooltipProvider> </TooltipProvider>
); );
}; };

View File

@@ -246,15 +246,19 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
</div> </div>
</div> </div>
{repo.topics && ( <div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2"> {repo.topics && (<>
{repo.topics.split(',').map((topic: string, idx: number) => ( {
<Badge key={idx} variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors"> repo.topics.split(',').map((topic: string, idx: number) => (
{topic.trim()} <Badge key={idx} variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">
</Badge> {topic.trim()}
))} </Badge>
</div> ))
)} }
</>
)}
<Badge variant="outline" className="text-xs border-neutral-300 text-neutral-700 hover:bg-neutral-100 transition-colors">{repo.visibility_level}</Badge>
</div>
{repo.site && ( {repo.site && (
<a <a

View File

@@ -7,13 +7,14 @@ import { WorkspaceDetailDialog } from './modules/WorkspaceDetailDialog'
import { SyncRepoDialog } from './modules/SyncRepoDialog' import { SyncRepoDialog } from './modules/SyncRepoDialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Plus, RefreshCw, Search } from 'lucide-react' import { Plus, RefreshCw, Search, Settings } from 'lucide-react'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { useNavigate } from '@tanstack/react-router'
export const App = () => { export const App = () => {
const { list, refresh, loading, editRepo, setEditRepo, workspaceList, showEditDialog, setShowEditDialog, showCreateDialog, setShowCreateDialog, startWorkspace, deleteItem, setSelectedSyncRepo, setSyncDialogOpen } = useRepoStore() const { list, refresh, loading, editRepo, setEditRepo, workspaceList, showEditDialog, setShowEditDialog, showCreateDialog, setShowCreateDialog, startWorkspace, deleteItem, setSelectedSyncRepo, setSyncDialogOpen } = useRepoStore()
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
refresh({ showTips: false }) refresh({ showTips: false })
}, []) }, [])
@@ -73,7 +74,10 @@ export const App = () => {
<div className="container mx-auto p-6 max-w-7xl flex-1"> <div className="container mx-auto p-6 max-w-7xl flex-1">
<div className="mb-8 flex items-center justify-between"> <div className="mb-8 flex items-center justify-between">
<div> <div>
<h1 className="text-4xl font-bold text-neutral-900 mb-2"></h1> <h1 className="text-4xl font-bold text-neutral-900 mb-2 flex gap-1 items-center">
<Settings className="inline-block h-5 w-5 ml-2 text-neutral-400 hover:text-neutral-600 cursor-pointer" onClick={() => navigate({ to: '/config' })} />
</h1>
<p className="text-neutral-600"> {list.length} </p> <p className="text-neutral-600"> {list.length} </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -20,7 +20,8 @@ interface Data {
name: string; name: string;
freeze: boolean; freeze: boolean;
status: number; status: number;
visibility_level: string; // Public, Private
visibility_level: string;
flags: string; flags: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;