Several optimizations

This commit is contained in:
AmintaCCCP
2025-07-28 19:55:17 +08:00
parent 400a980360
commit 9fc8cb44c3
11 changed files with 1982 additions and 0 deletions

3
.bolt/config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

5
.bolt/prompt Normal file
View File

@@ -0,0 +1,5 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.

204
.github/workflows/build-desktop.yml vendored Normal file
View File

@@ -0,0 +1,204 @@
name: Build Desktop App
on:
push:
branches: [ main, master ]
tags: [ 'v*' ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build web app
run: npm run build
- name: Install Electron dependencies
run: |
npm install --save-dev electron electron-builder
- name: Create Electron main process
run: |
mkdir -p electron
cat > electron/main.js << 'EOF'
const { app, BrowserWindow, Menu } = require('electron');
const path = require('path');
const isDev = process.env.NODE_ENV === 'development';
function createWindow() {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
webSecurity: true
},
icon: path.join(__dirname, '../dist/vite.svg'),
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
show: false
});
// Load the app
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// Handle window closed
mainWindow.on('closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
return mainWindow;
}
// App event handlers
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// Security: Prevent new window creation
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', (event, navigationUrl) => {
event.preventDefault();
require('electron').shell.openExternal(navigationUrl);
});
});
EOF
- name: Create Electron package.json
run: |
cat > electron/package.json << 'EOF'
{
"name": "github-stars-manager-desktop",
"version": "1.0.0",
"description": "GitHub Stars Manager Desktop App",
"main": "main.js",
"author": "GitHub Stars Manager",
"license": "MIT"
}
EOF
- name: Update main package.json for Electron
run: |
npm pkg set main="electron/main.js"
npm pkg set homepage="./"
npm pkg set scripts.electron="electron ."
npm pkg set scripts.electron-dev="NODE_ENV=development electron ."
npm pkg set scripts.dist="electron-builder"
npm pkg set build.appId="com.github-stars-manager.app"
npm pkg set build.productName="GitHub Stars Manager"
npm pkg set build.directories.output="release"
npm pkg set build.files[0]="dist/**/*"
npm pkg set build.files[1]="electron/**/*"
npm pkg set build.files[2]="node_modules/**/*"
npm pkg set build.files[3]="package.json"
- name: Configure platform-specific build settings
shell: bash
run: |
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
npm pkg set build.win.target="nsis"
npm pkg set build.win.icon="dist/vite.svg"
npm pkg set build.nsis.oneClick=false
npm pkg set build.nsis.allowToChangeInstallationDirectory=true
elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
npm pkg set build.mac.target="dmg"
npm pkg set build.mac.icon="dist/vite.svg"
npm pkg set build.mac.category="public.app-category.productivity"
else
npm pkg set build.linux.target="AppImage"
npm pkg set build.linux.icon="dist/vite.svg"
npm pkg set build.linux.category="Office"
fi
- name: Build Electron app
run: npm run dist
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts (Windows)
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: windows-app
path: release/*.exe
- name: Upload artifacts (macOS)
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: macos-app
path: release/*.dmg
- name: Upload artifacts (Linux)
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: linux-app
path: release/*.AppImage
release:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
windows-app/*
macos-app/*
linux-app/*
draft: false
prerelease: false
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

42
.github/workflows/build-web.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Build and Deploy Web App
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build web app
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: web-build
path: dist/
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
cname: your-domain.com # 可选:如果你有自定义域名

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

86
electron-builder.yml Normal file
View File

@@ -0,0 +1,86 @@
# Electron Builder配置文件
appId: com.github-stars-manager.app
productName: GitHub Stars Manager
copyright: Copyright © 2024 GitHub Stars Manager
directories:
output: release
buildResources: build
files:
- dist/**/*
- electron/**/*
- node_modules/**/*
- package.json
extraMetadata:
main: electron/main.js
# Windows配置
win:
target:
- target: nsis
arch:
- x64
- ia32
icon: dist/vite.svg
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
createStartMenuShortcut: true
shortcutName: GitHub Stars Manager
# macOS配置
mac:
target:
- target: dmg
arch:
- x64
- arm64
icon: dist/vite.svg
category: public.app-category.productivity
hardenedRuntime: true
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.inherit.plist
dmg:
title: GitHub Stars Manager
icon: dist/vite.svg
window:
width: 540
height: 380
contents:
- x: 410
y: 230
type: link
path: /Applications
- x: 130
y: 230
type: file
# Linux配置
linux:
target:
- target: AppImage
arch:
- x64
- target: deb
arch:
- x64
- target: rpm
arch:
- x64
icon: dist/vite.svg
category: Office
synopsis: AI-powered GitHub starred repositories management tool
description: >
GitHub Stars Manager is an intelligent tool for organizing and managing
your GitHub starred repositories with AI-powered analysis and release tracking.
# 发布配置
publish:
provider: github
owner: your-username
repo: your-repo-name

97
public/icon.svg Normal file
View File

@@ -0,0 +1,97 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<!-- Background gradient -->
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
<linearGradient id="starGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ffd700;stop-opacity:1" />
<stop offset="100%" style="stop-color:#ffb347;stop-opacity:1" />
</linearGradient>
<linearGradient id="githubGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f0f0f0;stop-opacity:1" />
</linearGradient>
<!-- Shadow filter -->
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="2" dy="4" stdDeviation="3" flood-color="#000000" flood-opacity="0.3"/>
</filter>
</defs>
<!-- Background circle -->
<circle cx="256" cy="256" r="240" fill="url(#bgGradient)" stroke="#4a5568" stroke-width="4"/>
<!-- GitHub logo background circle -->
<circle cx="256" cy="200" r="80" fill="url(#githubGradient)" filter="url(#shadow)"/>
<!-- GitHub cat icon (simplified) -->
<g transform="translate(256, 200)">
<!-- Cat body -->
<ellipse cx="0" cy="10" rx="35" ry="45" fill="#24292e"/>
<!-- Cat head -->
<circle cx="0" cy="-20" r="30" fill="#24292e"/>
<!-- Cat ears -->
<polygon points="-20,-35 -10,-50 -5,-35" fill="#24292e"/>
<polygon points="20,-35 10,-50 5,-35" fill="#24292e"/>
<!-- Cat eyes -->
<circle cx="-10" cy="-25" r="4" fill="#ffffff"/>
<circle cx="10" cy="-25" r="4" fill="#ffffff"/>
<circle cx="-10" cy="-25" r="2" fill="#24292e"/>
<circle cx="10" cy="-25" r="2" fill="#24292e"/>
<!-- Cat nose -->
<polygon points="0,-15 -3,-10 3,-10" fill="#ffffff"/>
<!-- Cat whiskers -->
<line x1="-25" y1="-20" x2="-35" y2="-18" stroke="#ffffff" stroke-width="2"/>
<line x1="-25" y1="-15" x2="-35" y2="-15" stroke="#ffffff" stroke-width="2"/>
<line x1="25" y1="-20" x2="35" y2="-18" stroke="#ffffff" stroke-width="2"/>
<line x1="25" y1="-15" x2="35" y2="-15" stroke="#ffffff" stroke-width="2"/>
</g>
<!-- Stars around GitHub logo -->
<g fill="url(#starGradient)" filter="url(#shadow)">
<!-- Main star -->
<g transform="translate(180, 140)">
<polygon points="0,-20 6,-6 20,-6 10,2 16,16 0,8 -16,16 -10,2 -20,-6 -6,-6" />
</g>
<!-- Secondary stars -->
<g transform="translate(330, 160) scale(0.7)">
<polygon points="0,-20 6,-6 20,-6 10,2 16,16 0,8 -16,16 -10,2 -20,-6 -6,-6" />
</g>
<g transform="translate(200, 280) scale(0.5)">
<polygon points="0,-20 6,-6 20,-6 10,2 16,16 0,8 -16,16 -10,2 -20,-6 -6,-6" />
</g>
<g transform="translate(320, 280) scale(0.6)">
<polygon points="0,-20 6,-6 20,-6 10,2 16,16 0,8 -16,16 -10,2 -20,-6 -6,-6" />
</g>
</g>
<!-- Management/Organization symbols -->
<g transform="translate(256, 350)" fill="#ffffff" filter="url(#shadow)">
<!-- Folder icon -->
<rect x="-30" y="-10" width="60" height="40" rx="4" fill="#4299e1" opacity="0.9"/>
<rect x="-25" y="-15" width="20" height="8" rx="2" fill="#4299e1" opacity="0.9"/>
<!-- Files inside folder -->
<rect x="-20" y="-2" width="15" height="2" fill="#ffffff" opacity="0.8"/>
<rect x="-20" y="5" width="25" height="2" fill="#ffffff" opacity="0.8"/>
<rect x="-20" y="12" width="20" height="2" fill="#ffffff" opacity="0.8"/>
</g>
<!-- AI/Smart features indicator -->
<g transform="translate(380, 320)" fill="#10b981" filter="url(#shadow)">
<circle cx="0" cy="0" r="15" fill="#10b981"/>
<!-- Brain/AI symbol -->
<path d="M-8,-5 Q-8,-10 -3,-10 Q2,-10 2,-5 Q7,-5 7,0 Q7,5 2,5 Q-3,5 -3,0 Q-8,0 -8,-5" fill="#ffffff"/>
<circle cx="-3" cy="-3" r="1.5" fill="#10b981"/>
<circle cx="2" cy="-3" r="1.5" fill="#10b981"/>
<circle cx="-1" cy="2" r="1" fill="#10b981"/>
</g>
<!-- App title -->
<text x="256" y="450" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="bold" fill="#ffffff" filter="url(#shadow)">
GitHub Stars
</text>
<text x="256" y="475" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" fill="#e2e8f0">
Manager
</text>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

180
scripts/build-desktop.js Normal file
View File

@@ -0,0 +1,180 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
console.log('🚀 开始构建桌面应用...');
// 1. 构建Web应用
console.log('📦 构建Web应用...');
execSync('npm run build', { stdio: 'inherit' });
// 2. 创建Electron目录和文件
console.log('⚡ 设置Electron环境...');
const electronDir = path.join(__dirname, '../electron');
if (!fs.existsSync(electronDir)) {
fs.mkdirSync(electronDir, { recursive: true });
}
// 3. 创建主进程文件
const mainJs = `
const { app, BrowserWindow, Menu, shell } = require('electron');
const path = require('path');
const isDev = process.env.NODE_ENV === 'development';
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
webSecurity: true
},
icon: path.join(__dirname, '../dist/vite.svg'),
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
show: false
});
// 加载应用
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
mainWindow.once('ready-to-show', () => {
mainWindow.show();
// 设置应用菜单
if (process.platform === 'darwin') {
const template = [
{
label: 'GitHub Stars Manager',
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
},
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectall' }
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
},
{
label: 'Window',
submenu: [
{ role: 'minimize' },
{ role: 'close' }
]
}
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
});
// 处理外部链接
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// 安全设置
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', (event, navigationUrl) => {
event.preventDefault();
shell.openExternal(navigationUrl);
});
});
`;
fs.writeFileSync(path.join(electronDir, 'main.js'), mainJs);
// 4. 创建Electron package.json
const electronPackageJson = {
name: 'github-stars-manager-desktop',
version: '1.0.0',
description: 'GitHub Stars Manager Desktop App',
main: 'main.js',
author: 'GitHub Stars Manager',
license: 'MIT'
};
fs.writeFileSync(
path.join(electronDir, 'package.json'),
JSON.stringify(electronPackageJson, null, 2)
);
// 5. 安装Electron依赖
console.log('📥 安装Electron依赖...');
try {
execSync('npm install --save-dev electron electron-builder', { stdio: 'inherit' });
} catch (error) {
console.error('安装依赖失败:', error.message);
process.exit(1);
}
// 6. 构建应用
console.log('🔨 构建桌面应用...');
try {
execSync('npx electron-builder', { stdio: 'inherit' });
console.log('✅ 桌面应用构建完成!');
console.log('📁 构建文件位于 release/ 目录');
} catch (error) {
console.error('构建失败:', error.message);
process.exit(1);
}

File diff suppressed because it is too large Load Diff

53
src/components/Modal.tsx Normal file
View File

@@ -0,0 +1,53 @@
import React from 'react';
import { X } from 'lucide-react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
maxWidth?: string;
}
export const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
maxWidth = 'max-w-md'
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 overflow-y-auto">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
onClick={onClose}
/>
{/* Modal */}
<div className="flex min-h-full items-center justify-center p-4">
<div className={`relative w-full ${maxWidth} bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700`}>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{title}
</h3>
<button
onClick={onClose}
className="p-2 rounded-lg text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6">
{children}
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,273 @@
import React, { useState, useEffect } from 'react';
import { Save, X, Plus } from 'lucide-react';
import { Modal } from './Modal';
import { Repository } from '../types';
import { useAppStore, getAllCategories } from '../store/useAppStore';
interface RepositoryEditModalProps {
isOpen: boolean;
onClose: () => void;
repository: Repository | null;
}
export const RepositoryEditModal: React.FC<RepositoryEditModalProps> = ({
isOpen,
onClose,
repository
}) => {
const { updateRepository, language, customCategories, repositories } = useAppStore();
const [formData, setFormData] = useState({
description: '',
tags: [] as string[],
category: ''
});
const [newTag, setNewTag] = useState('');
const allCategories = getAllCategories(customCategories, language);
// 获取仓库当前所属的分类
const getCurrentCategory = (repo: Repository) => {
// 如果有自定义分类,直接返回
if (repo.custom_category) {
return repo.custom_category;
}
// 否则根据AI标签或其他信息推断当前分类
for (const category of allCategories) {
if (category.id === 'all') continue;
// 检查AI标签匹配
if (repo.ai_tags && repo.ai_tags.length > 0) {
const hasMatch = repo.ai_tags.some(tag =>
category.keywords.some(keyword =>
tag.toLowerCase().includes(keyword.toLowerCase()) ||
keyword.toLowerCase().includes(tag.toLowerCase())
)
);
if (hasMatch) {
return category.name;
}
}
// 检查传统匹配方式
const repoText = [
repo.name,
repo.description || '',
repo.language || '',
...(repo.topics || []),
repo.ai_summary || ''
].join(' ').toLowerCase();
const hasKeywordMatch = category.keywords.some(keyword =>
repoText.includes(keyword.toLowerCase())
);
if (hasKeywordMatch) {
return category.name;
}
}
return '';
};
useEffect(() => {
if (repository && isOpen) {
const currentCategory = getCurrentCategory(repository);
setFormData({
description: repository.custom_description || repository.description || '',
tags: repository.custom_tags || repository.ai_tags || repository.topics || [],
category: currentCategory
});
}
}, [repository, isOpen]);
const handleSave = () => {
if (!repository) return;
const updatedRepo = {
...repository,
custom_description: formData.description !== repository.description ? formData.description : undefined,
custom_tags: formData.tags.length > 0 ? formData.tags : undefined,
custom_category: formData.category ? formData.category : undefined,
last_edited: new Date().toISOString()
};
updateRepository(updatedRepo);
onClose();
};
const handleClose = () => {
setFormData({
description: '',
tags: [],
category: ''
});
setNewTag('');
onClose();
};
const handleAddTag = () => {
if (newTag.trim() && !formData.tags.includes(newTag.trim())) {
setFormData(prev => ({
...prev,
tags: [...prev.tags, newTag.trim()]
}));
setNewTag('');
}
};
const handleRemoveTag = (tagToRemove: string) => {
setFormData(prev => ({
...prev,
tags: prev.tags.filter(tag => tag !== tagToRemove)
}));
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddTag();
}
};
const t = (zh: string, en: string) => language === 'zh' ? zh : en;
if (!repository) return null;
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
title={t('编辑仓库信息', 'Edit Repository Info')}
maxWidth="max-w-2xl"
>
<div className="space-y-6">
{/* Repository Info */}
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div className="flex items-center space-x-3 mb-2">
<img
src={repository.owner.avatar_url}
alt={repository.owner.login}
className="w-8 h-8 rounded-full"
/>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white">
{repository.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{repository.owner.login}
</p>
</div>
</div>
{repository.description && (
<p className="text-sm text-gray-600 dark:text-gray-300">
{t('原始描述:', 'Original description:')} {repository.description}
</p>
)}
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('自定义描述', 'Custom Description')}
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
rows={3}
placeholder={t('输入自定义描述...', 'Enter custom description...')}
/>
</div>
{/* Category */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('分类', 'Category')}
</label>
<select
value={formData.category}
onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">{t('选择分类...', 'Select category...')}</option>
{allCategories.filter(cat => cat.id !== 'all').map(category => (
<option key={category.id} value={category.name}>
{category.icon} {category.name}
</option>
))}
</select>
{formData.category && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t('当前分类:', 'Current category:')} {formData.category}
</p>
)}
</div>
{/* Tags */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t('自定义标签', 'Custom Tags')}
</label>
{/* Existing Tags */}
{formData.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-3">
{formData.tags.map((tag, index) => (
<span
key={index}
className="inline-flex items-center px-3 py-1 bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-full text-sm"
>
{tag}
<button
onClick={() => handleRemoveTag(tag)}
className="ml-2 text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-200"
>
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
)}
{/* Add New Tag */}
<div className="flex space-x-2">
<input
type="text"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
onKeyPress={handleKeyPress}
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder={t('添加标签...', 'Add tag...')}
/>
<button
onClick={handleAddTag}
disabled={!newTag.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-600">
<button
onClick={handleClose}
className="flex items-center space-x-2 px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
<X className="w-4 h-4" />
<span>{t('取消', 'Cancel')}</span>
</button>
<button
onClick={handleSave}
className="flex items-center space-x-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Save className="w-4 h-4" />
<span>{t('保存', 'Save')}</span>
</button>
</div>
</div>
</Modal>
);
};