feat: optimize ui
This commit is contained in:
@@ -331,7 +331,7 @@ function App() {
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex h-[calc(100vh-4rem)] gap-4 p-4">
|
||||
<main className="flex h-[calc(100vh-4rem)] gap-4 p-4 overflow-hidden">
|
||||
<div className="w-3/5">
|
||||
<Providers />
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@ function App() {
|
||||
<div className="h-3/5">
|
||||
<Router />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Transformers />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
||||
// Validate the received data to ensure it has the expected structure
|
||||
const validConfig = {
|
||||
LOG: typeof data.LOG === 'boolean' ? data.LOG : false,
|
||||
LOG_LEVEL: typeof data.LOG_LEVEL === 'string' ? data.LOG_LEVEL : 'info',
|
||||
LOG_LEVEL: typeof data.LOG_LEVEL === 'string' ? data.LOG_LEVEL : 'debug',
|
||||
CLAUDE_PATH: typeof data.CLAUDE_PATH === 'string' ? data.CLAUDE_PATH : '',
|
||||
HOST: typeof data.HOST === 'string' ? data.HOST : '127.0.0.1',
|
||||
PORT: typeof data.PORT === 'number' ? data.PORT : 3456,
|
||||
@@ -115,7 +115,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
||||
// Set default empty config when fetch fails
|
||||
setConfig({
|
||||
LOG: false,
|
||||
LOG_LEVEL: 'info',
|
||||
LOG_LEVEL: 'debug',
|
||||
CLAUDE_PATH: '',
|
||||
HOST: '127.0.0.1',
|
||||
PORT: 3456,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { X, Trash2, Plus, Eye, EyeOff } from "lucide-react";
|
||||
import { X, Trash2, Plus, Eye, EyeOff, Search, XCircle } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import { ComboInput } from "@/components/ui/combo-input";
|
||||
@@ -38,6 +38,7 @@ export function Providers() {
|
||||
const [showApiKey, setShowApiKey] = useState<Record<number, boolean>>({});
|
||||
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
|
||||
const [nameError, setNameError] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const comboInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -487,15 +488,57 @@ export function Providers() {
|
||||
|
||||
const editingProvider = editingProviderData || (editingProviderIndex !== null ? validProviders[editingProviderIndex] : null);
|
||||
|
||||
// Filter providers based on search term
|
||||
const filteredProviders = validProviders.filter(provider => {
|
||||
if (!searchTerm) return true;
|
||||
const term = searchTerm.toLowerCase();
|
||||
// Check provider name and URL
|
||||
if (
|
||||
(provider.name && provider.name.toLowerCase().includes(term)) ||
|
||||
(provider.api_base_url && provider.api_base_url.toLowerCase().includes(term))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Check models
|
||||
if (provider.models && Array.isArray(provider.models)) {
|
||||
return provider.models.some(model =>
|
||||
model && model.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="flex h-full flex-col rounded-lg border shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b p-4">
|
||||
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({validProviders.length})</span></CardTitle>
|
||||
<Button onClick={handleAddProvider}>{t("providers.add")}</Button>
|
||||
<CardHeader className="flex flex-col border-b p-4 gap-3">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({filteredProviders.length}/{validProviders.length})</span></CardTitle>
|
||||
<Button onClick={handleAddProvider}>{t("providers.add")}</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
|
||||
<Input
|
||||
placeholder={t("providers.search")}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
{searchTerm && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSearchTerm("")}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow overflow-y-auto p-4">
|
||||
<ProviderList
|
||||
providers={validProviders}
|
||||
providers={filteredProviders}
|
||||
onEdit={handleEditProvider}
|
||||
onRemove={setDeletingProviderIndex}
|
||||
/>
|
||||
|
||||
@@ -58,12 +58,12 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent data-testid="settings-dialog">
|
||||
<DialogHeader>
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange} >
|
||||
<DialogContent data-testid="settings-dialog" className="max-h-[80vh] flex flex-col p-0">
|
||||
<DialogHeader className="p-4 pb-0">
|
||||
<DialogTitle>{t("toplevel.title")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-4 p-4 px-8 overflow-y-auto flex-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="log"
|
||||
@@ -213,7 +213,7 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="p-4 pt-0">
|
||||
<Button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -96,6 +97,207 @@ const ANSI_COLORS: Record<string, string> = {
|
||||
bg_bright_purple: "bg-purple-400",
|
||||
};
|
||||
|
||||
|
||||
// 图标搜索输入组件
|
||||
interface IconData {
|
||||
className: string;
|
||||
unicode: string;
|
||||
char: string;
|
||||
}
|
||||
|
||||
interface IconSearchInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
fontFamily: string;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
const IconSearchInput = React.memo(({ value, onChange, fontFamily, t }: IconSearchInputProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState(value);
|
||||
const [icons, setIcons] = useState<IconData[]>([]);
|
||||
const [filteredIcons, setFilteredIcons] = useState<IconData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// 加载Nerdfonts图标数据
|
||||
const loadIcons = useCallback(async () => {
|
||||
if (icons.length > 0) return; // 已经加载过了
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch('https://www.nerdfonts.com/assets/css/combo.css');
|
||||
const cssText = await response.text();
|
||||
|
||||
// 解析CSS中的图标类名和Unicode
|
||||
const iconRegex = /\.nf-([a-zA-Z0-9_-]+):before\s*\{\s*content:\s*"\\([0-9a-fA-F]+)";?\s*\}/g;
|
||||
const iconData: IconData[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = iconRegex.exec(cssText)) !== null) {
|
||||
const className = `nf-${match[1]}`;
|
||||
const unicode = match[2];
|
||||
const char = String.fromCharCode(parseInt(unicode, 16));
|
||||
iconData.push({ className, unicode, char });
|
||||
}
|
||||
|
||||
setIcons(iconData);
|
||||
setFilteredIcons(iconData.slice(0, 200));
|
||||
} catch (error) {
|
||||
console.error('Failed to load icons:', error);
|
||||
setIcons([]);
|
||||
setFilteredIcons([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [icons.length]);
|
||||
|
||||
// 模糊搜索图标
|
||||
useEffect(() => {
|
||||
if (searchTerm.trim() === '') {
|
||||
setFilteredIcons(icons.slice(0, 100)); // 显示前100个图标
|
||||
return;
|
||||
}
|
||||
|
||||
const term = searchTerm.toLowerCase();
|
||||
let filtered = icons;
|
||||
|
||||
// 如果输入的是特殊字符(可能是粘贴的图标),则搜索对应图标
|
||||
if (term.length === 1 || /[\u{2000}-\u{2FFFF}]/u.test(searchTerm)) {
|
||||
const pastedIcon = icons.find(icon => icon.char === searchTerm);
|
||||
if (pastedIcon) {
|
||||
filtered = [pastedIcon];
|
||||
} else {
|
||||
// 搜索包含该字符的图标
|
||||
filtered = icons.filter(icon => icon.char === searchTerm);
|
||||
}
|
||||
} else {
|
||||
// 模糊搜索:类名、简化后的名称匹配
|
||||
filtered = icons.filter(icon => {
|
||||
const className = icon.className.toLowerCase();
|
||||
const simpleClassName = className.replace(/[_-]/g, '');
|
||||
const simpleTerm = term.replace(/[_-]/g, '');
|
||||
|
||||
return (
|
||||
className.includes(term) ||
|
||||
simpleClassName.includes(simpleTerm) ||
|
||||
// 关键词匹配
|
||||
term.split(' ').every(keyword =>
|
||||
className.includes(keyword) || simpleClassName.includes(keyword)
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setFilteredIcons(filtered.slice(0, 120)); // 显示更多结果
|
||||
}, [searchTerm, icons]);
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setSearchTerm(newValue);
|
||||
onChange(newValue);
|
||||
|
||||
// 始终打开下拉框,让用户搜索或确认粘贴的内容
|
||||
setIsOpen(true);
|
||||
if (icons.length === 0) {
|
||||
loadIcons();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理粘贴事件
|
||||
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const pastedText = e.clipboardData.getData('text');
|
||||
|
||||
// 如果是单个字符(可能是图标),直接接受并打开下拉框显示相应图标
|
||||
if (pastedText && pastedText.length === 1) {
|
||||
setTimeout(() => {
|
||||
setIsOpen(true);
|
||||
}, 10);
|
||||
}
|
||||
};
|
||||
|
||||
// 选择图标
|
||||
const handleIconSelect = (iconChar: string) => {
|
||||
setSearchTerm(iconChar);
|
||||
onChange(iconChar);
|
||||
setIsOpen(false);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
// 处理焦点事件
|
||||
const handleFocus = () => {
|
||||
setIsOpen(true);
|
||||
if (icons.length === 0) {
|
||||
loadIcons();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 处理失去焦点(延迟关闭以便点击图标)
|
||||
const handleBlur = () => {
|
||||
setTimeout(() => setIsOpen(false), 200);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={searchTerm}
|
||||
onChange={handleInputChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
placeholder={t("statusline.icon_placeholder")}
|
||||
style={{ fontFamily: fontFamily + ', monospace' }}
|
||||
className="text-lg pr-2"
|
||||
/>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 mt-1 w-full max-h-72 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<svg className="animate-spin h-6 w-6 text-primary" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" opacity="0.1"/>
|
||||
<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-5 gap-2 p-2 max-h-72 overflow-y-auto">
|
||||
{filteredIcons.map((icon) => (
|
||||
<div
|
||||
key={icon.className}
|
||||
className="flex items-center justify-center p-3 text-2xl cursor-pointer hover:bg-secondary rounded transition-colors"
|
||||
onClick={() => handleIconSelect(icon.char)}
|
||||
onMouseDown={(e) => e.preventDefault()} // 防止失去焦点
|
||||
title={`${icon.char} - ${icon.className}`}
|
||||
style={{ fontFamily: fontFamily + ', monospace' }}
|
||||
>
|
||||
{icon.char}
|
||||
</div>
|
||||
))}
|
||||
{filteredIcons.length === 0 && (
|
||||
<div className="col-span-5 flex flex-col items-center justify-center p-8 text-muted-foreground">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="mb-2">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.35-4.35" />
|
||||
</svg>
|
||||
<div className="text-sm">
|
||||
{searchTerm ? `${t("statusline.no_icons_found")} "${searchTerm}"` : t("statusline.no_icons_available")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// 变量替换函数
|
||||
function replaceVariables(
|
||||
text: string,
|
||||
@@ -500,9 +702,57 @@ export function StatusLineConfigDialog({
|
||||
? currentModules[selectedModuleIndex]
|
||||
: null;
|
||||
|
||||
// 删除选中模块的函数
|
||||
const deleteSelectedModule = useCallback(() => {
|
||||
if (selectedModuleIndex === null) return;
|
||||
|
||||
const currentTheme =
|
||||
statusLineConfig.currentStyle as keyof StatusLineConfig;
|
||||
const themeConfig = statusLineConfig[currentTheme];
|
||||
const modules =
|
||||
themeConfig &&
|
||||
typeof themeConfig === "object" &&
|
||||
"modules" in themeConfig
|
||||
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
|
||||
: [];
|
||||
|
||||
if (selectedModuleIndex >= 0 && selectedModuleIndex < modules.length) {
|
||||
modules.splice(selectedModuleIndex, 1);
|
||||
|
||||
setStatusLineConfig((prev) => ({
|
||||
...prev,
|
||||
[currentTheme]: { modules },
|
||||
}));
|
||||
|
||||
setSelectedModuleIndex(null);
|
||||
}
|
||||
}, [selectedModuleIndex, statusLineConfig]);
|
||||
|
||||
// 字体样式
|
||||
const fontStyle = fontFamily ? { fontFamily } : {};
|
||||
|
||||
// 键盘事件监听器,支持删除选中的模块
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// 检查是否选中了模块
|
||||
if (selectedModuleIndex === null) return;
|
||||
|
||||
// 检查是否按下了删除键 (Delete 或 Backspace)
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
deleteSelectedModule();
|
||||
}
|
||||
};
|
||||
|
||||
// 添加事件监听器
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [selectedModuleIndex, deleteSelectedModule]);
|
||||
|
||||
// 当字体或主题变化时强制重新渲染
|
||||
const fontKey = `${fontFamily}-${statusLineConfig.currentStyle}`;
|
||||
|
||||
@@ -558,30 +808,30 @@ export function StatusLineConfigDialog({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="theme-style" className="text-sm font-medium">
|
||||
主题样式
|
||||
{t("statusline.theme")}
|
||||
</Label>
|
||||
<Combobox
|
||||
options={[
|
||||
{ label: "默认", value: "default" },
|
||||
{ label: "Powerline", value: "powerline" },
|
||||
{ label: t("statusline.theme_default"), value: "default" },
|
||||
{ label: t("statusline.theme_powerline"), value: "powerline" },
|
||||
]}
|
||||
value={statusLineConfig.currentStyle}
|
||||
onChange={handleThemeChange}
|
||||
data-testid="theme-selector"
|
||||
placeholder="选择主题样式"
|
||||
placeholder={t("statusline.theme_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="font-family" className="text-sm font-medium">
|
||||
字体
|
||||
{t("statusline.module_icon")}
|
||||
</Label>
|
||||
<Combobox
|
||||
options={NERD_FONTS}
|
||||
value={fontFamily}
|
||||
onChange={(value) => setFontFamily(value)}
|
||||
data-testid="font-family-selector"
|
||||
placeholder="选择字体"
|
||||
placeholder={t("statusline.font_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,9 +840,9 @@ export function StatusLineConfigDialog({
|
||||
{/* 三栏布局:组件列表 | 预览区域 | 属性配置 */}
|
||||
<div className="grid grid-cols-5 gap-6 overflow-hidden flex-1">
|
||||
{/* 左侧:支持的组件 */}
|
||||
<div className="border rounded-lg p-4 flex flex-col overflow-hidden col-span-1">
|
||||
<h3 className="text-sm font-medium mb-3">组件</h3>
|
||||
<div className="space-y-2 overflow-y-auto flex-1">
|
||||
<div className="border rounded-lg flex flex-col overflow-hidden col-span-1">
|
||||
<h3 className="text-sm font-medium p-4 pb-0 mb-3">{t("statusline.components")}</h3>
|
||||
<div className="space-y-2 overflow-y-auto px-4 pb-4 flex-1">
|
||||
{MODULE_TYPES_OPTIONS.map((moduleType) => (
|
||||
<div
|
||||
key={moduleType.value}
|
||||
@@ -610,7 +860,7 @@ export function StatusLineConfigDialog({
|
||||
|
||||
{/* 中间:预览区域 */}
|
||||
<div className="border rounded-lg p-4 flex flex-col col-span-3">
|
||||
<h3 className="text-sm font-medium mb-3">预览</h3>
|
||||
<h3 className="text-sm font-medium mb-3">{t("statusline.preview")}</h3>
|
||||
<div
|
||||
key={fontKey}
|
||||
className={`rounded bg-black/90 text-white font-mono text-sm overflow-x-auto flex items-center border border-border p-3 py-5 shadow-inner overflow-hidden ${
|
||||
@@ -793,7 +1043,7 @@ export function StatusLineConfigDialog({
|
||||
<path d="M8 12h8" />
|
||||
</svg>
|
||||
<span className="text-gray-500 text-sm">
|
||||
拖拽组件到此处进行配置
|
||||
{t("statusline.drag_hint")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -801,45 +1051,31 @@ export function StatusLineConfigDialog({
|
||||
</div>
|
||||
|
||||
{/* 右侧:属性配置 */}
|
||||
<div className="border rounded-lg p-4 flex flex-col overflow-hidden col-span-1">
|
||||
<h3 className="text-sm font-medium mb-3">属性</h3>
|
||||
<div className="overflow-y-auto flex-1">
|
||||
<div className="border rounded-lg flex flex-col overflow-hidden col-span-1">
|
||||
<h3 className="text-sm font-medium p-4 pb-0 mb-3">{t("statusline.properties")}</h3>
|
||||
<div className="overflow-y-auto px-4 pb-4 flex-1">
|
||||
{selectedModule && selectedModuleIndex !== null ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("statusline.module_type")}</Label>
|
||||
<Combobox
|
||||
options={MODULE_TYPES_OPTIONS}
|
||||
value={selectedModule.type}
|
||||
onChange={(value) =>
|
||||
handleModuleChange(selectedModuleIndex, "type", value)
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择模块类型以确定显示的信息
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="module-icon">
|
||||
{t("statusline.module_icon")}
|
||||
</Label>
|
||||
<Input
|
||||
<IconSearchInput
|
||||
key={fontKey}
|
||||
id="module-icon"
|
||||
value={selectedModule.icon || ""}
|
||||
onChange={(e) =>
|
||||
onChange={(value) =>
|
||||
handleModuleChange(
|
||||
selectedModuleIndex,
|
||||
"icon",
|
||||
e.target.value
|
||||
value
|
||||
)
|
||||
}
|
||||
placeholder="例如: "
|
||||
style={fontStyle}
|
||||
fontFamily={fontFamily}
|
||||
t={t}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
输入图标字符或表情符号(可选)
|
||||
{t("statusline.icon_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -857,10 +1093,10 @@ export function StatusLineConfigDialog({
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="例如: {{workDirName}}"
|
||||
placeholder={t("statusline.text_placeholder")}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<p>输入显示文本,可使用变量:</p>
|
||||
<p>{t("statusline.module_text_description")}</p>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
@@ -909,7 +1145,7 @@ export function StatusLineConfigDialog({
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择文字颜色
|
||||
{t("statusline.module_color_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -926,7 +1162,7 @@ export function StatusLineConfigDialog({
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择背景颜色(可选)
|
||||
{t("statusline.module_background_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -934,7 +1170,7 @@ export function StatusLineConfigDialog({
|
||||
{selectedModule.type === "script" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="module-script-path">
|
||||
脚本路径
|
||||
{t("statusline.module_script_path")}
|
||||
</Label>
|
||||
<Input
|
||||
id="module-script-path"
|
||||
@@ -946,10 +1182,10 @@ export function StatusLineConfigDialog({
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
placeholder="例如: /path/to/your/script.js"
|
||||
placeholder={t("statusline.script_placeholder")}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
输入Node.js脚本文件的绝对路径
|
||||
{t("statusline.module_script_path_description")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -958,36 +1194,15 @@ export function StatusLineConfigDialog({
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const currentTheme =
|
||||
statusLineConfig.currentStyle as keyof StatusLineConfig;
|
||||
const themeConfig = statusLineConfig[currentTheme];
|
||||
const modules =
|
||||
themeConfig &&
|
||||
typeof themeConfig === "object" &&
|
||||
"modules" in themeConfig
|
||||
? [
|
||||
...((themeConfig as StatusLineThemeConfig)
|
||||
.modules || []),
|
||||
]
|
||||
: [];
|
||||
modules.splice(selectedModuleIndex, 1);
|
||||
|
||||
setStatusLineConfig((prev) => ({
|
||||
...prev,
|
||||
[currentTheme]: { modules },
|
||||
}));
|
||||
|
||||
setSelectedModuleIndex(null);
|
||||
}}
|
||||
onClick={deleteSelectedModule}
|
||||
>
|
||||
删除组件
|
||||
{t("statusline.delete_module")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full min-h-[200px]">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
选择一个组件进行配置
|
||||
{t("statusline.select_hint")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -119,4 +119,38 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* 美化滚动条 - WebKit浏览器 (Chrome, Safari, Edge) */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-muted-foreground/30;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-muted-foreground/50;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: oklch(0.556 0 0) oklch(0.97 0 0);
|
||||
}
|
||||
|
||||
.dark * {
|
||||
scrollbar-color: oklch(0.708 0 0) oklch(0.269 0 0);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,8 @@
|
||||
"select_template": "Select a template...",
|
||||
"api_key_required": "API Key is required",
|
||||
"name_required": "Name is required",
|
||||
"name_duplicate": "A provider with this name already exists"
|
||||
"name_duplicate": "A provider with this name already exists",
|
||||
"search": "Search providers..."
|
||||
|
||||
},
|
||||
"router": {
|
||||
@@ -128,9 +129,17 @@
|
||||
"module_text": "Text",
|
||||
"module_color": "Color",
|
||||
"module_background": "Background",
|
||||
"add_module": "Add Module",
|
||||
"module_text_description": "Enter display text, variables can be used:",
|
||||
"module_color_description": "Select text color",
|
||||
"module_background_description": "Select background color (optional)",
|
||||
"module_script_path": "Script Path",
|
||||
"module_script_path_description": "Enter the absolute path of the Node.js script file",
|
||||
"add_module": "Add Module",
|
||||
"remove_module": "Remove Module",
|
||||
"delete_module": "Delete Module",
|
||||
"preview": "Preview",
|
||||
"components": "Components",
|
||||
"properties": "Properties",
|
||||
"workDir": "Working Directory",
|
||||
"gitBranch": "Git Branch",
|
||||
"model": "Model",
|
||||
@@ -153,6 +162,16 @@
|
||||
"color_bright_magenta": "Bright Magenta",
|
||||
"color_bright_cyan": "Bright Cyan",
|
||||
"color_bright_white": "Bright White",
|
||||
"font_placeholder": "Select Font",
|
||||
"theme_placeholder": "Select Theme Style",
|
||||
"icon_placeholder": "Paste icon or search by name...",
|
||||
"icon_description": "Enter icon character, paste icon, or search icons (optional)",
|
||||
"text_placeholder": "e.g.: {{workDirName}}",
|
||||
"script_placeholder": "e.g.: /path/to/your/script.js",
|
||||
"drag_hint": "Drag components here to configure",
|
||||
"select_hint": "Select a component to configure",
|
||||
"no_icons_found": "No icons found",
|
||||
"no_icons_available": "No icons available",
|
||||
"import_export": "Import/Export",
|
||||
"import": "Import Config",
|
||||
"export": "Export Config",
|
||||
|
||||
@@ -93,7 +93,8 @@
|
||||
"select_template": "选择一个模板...",
|
||||
"api_key_required": "API 密钥为必填项",
|
||||
"name_required": "名称为必填项",
|
||||
"name_duplicate": "已存在同名供应商"
|
||||
"name_duplicate": "已存在同名供应商",
|
||||
"search": "搜索供应商..."
|
||||
|
||||
},
|
||||
"router": {
|
||||
@@ -128,9 +129,17 @@
|
||||
"module_text": "文本",
|
||||
"module_color": "颜色",
|
||||
"module_background": "背景",
|
||||
"add_module": "添加模块",
|
||||
"module_text_description": "输入显示文本,可使用变量:",
|
||||
"module_color_description": "选择文字颜色",
|
||||
"module_background_description": "选择背景颜色(可选)",
|
||||
"module_script_path": "脚本路径",
|
||||
"module_script_path_description": "输入Node.js脚本文件的绝对路径",
|
||||
"add_module": "添加模块",
|
||||
"remove_module": "移除模块",
|
||||
"delete_module": "删除组件",
|
||||
"preview": "预览",
|
||||
"components": "组件",
|
||||
"properties": "属性",
|
||||
"workDir": "工作目录",
|
||||
"gitBranch": "Git分支",
|
||||
"model": "模型",
|
||||
@@ -153,6 +162,16 @@
|
||||
"color_bright_magenta": "亮品红",
|
||||
"color_bright_cyan": "亮青色",
|
||||
"color_bright_white": "亮白色",
|
||||
"font_placeholder": "选择字体",
|
||||
"theme_placeholder": "选择主题样式",
|
||||
"icon_placeholder": "粘贴图标或输入名称搜索...",
|
||||
"icon_description": "输入图标字符、粘贴图标或搜索图标(可选)",
|
||||
"text_placeholder": "例如: {{workDirName}}",
|
||||
"script_placeholder": "例如: /path/to/your/script.js",
|
||||
"drag_hint": "拖拽组件到此处进行配置",
|
||||
"select_hint": "选择一个组件进行配置",
|
||||
"no_icons_found": "未找到图标",
|
||||
"no_icons_available": "暂无可用图标",
|
||||
"import_export": "导入/导出",
|
||||
"import": "导入配置",
|
||||
"export": "导出配置",
|
||||
|
||||
Reference in New Issue
Block a user