feat: update statusline config ui

This commit is contained in:
musistudio
2025-08-16 19:01:15 +08:00
parent 19d0f3b8f5
commit d2969e4332
4 changed files with 513 additions and 309 deletions

View File

@@ -14,23 +14,42 @@ import { Combobox } from "@/components/ui/combobox";
import { ColorPicker } from "@/components/ui/color-picker";
import { Badge } from "@/components/ui/badge";
import { useConfig } from "./ConfigProvider";
import { validateStatusLineConfig, formatValidationError, createDefaultStatusLineConfig } from "@/utils/statusline";
import type { StatusLineConfig, StatusLineModuleConfig, StatusLineThemeConfig } from "@/types";
import {
validateStatusLineConfig,
formatValidationError,
createDefaultStatusLineConfig,
} from "@/utils/statusline";
import type {
StatusLineConfig,
StatusLineModuleConfig,
StatusLineThemeConfig,
} from "@/types";
const DEFAULT_MODULE: StatusLineModuleConfig = {
type: "workDir",
icon: "󰉋",
text: "{{workDirName}}",
color: "bright_blue"
color: "bright_blue",
};
// Nerd Font选项
const NERD_FONTS = [
{ label: "Hack Nerd Font Mono", value: "Hack Nerd Font Mono" },
{ label: "FiraCode Nerd Font Mono", value: "FiraCode Nerd Font Mono" },
{
label: "JetBrainsMono Nerd Font Mono",
value: "JetBrainsMono Nerd Font Mono",
},
{ label: "Monaspace Nerd Font Mono", value: "Monaspace Nerd Font Mono" },
{ label: "UbuntuMono Nerd Font", value: "UbuntuMono Nerd Font" },
];
// 模块类型选项
const MODULE_TYPES = [
{ label: "workDir", value: "workDir" },
{ label: "gitBranch", value: "gitBranch" },
{ label: "model", value: "model" },
{ label: "usage", value: "usage" }
{ label: "usage", value: "usage" },
];
// ANSI颜色代码映射
@@ -77,71 +96,143 @@ const ANSI_COLORS: Record<string, string> = {
};
// 变量替换函数
function replaceVariables(text: string, variables: Record<string, string>): string {
function replaceVariables(
text: string,
variables: Record<string, string>
): string {
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
return variables[varName] || match;
});
}
// 渲染单个模块预览
function renderModulePreview(module: StatusLineModuleConfig, isPowerline: boolean = false): React.ReactNode {
function renderModulePreview(
module: StatusLineModuleConfig,
isPowerline: boolean = false
): React.ReactNode {
// 模拟变量数据
const variables = {
workDirName: "project",
gitBranch: "main",
model: "Claude Sonnet 4",
inputTokens: "1.2k",
outputTokens: "2.5k"
outputTokens: "2.5k",
};
const text = replaceVariables(module.text, variables);
const icon = module.icon || "";
// 如果text为空且不是usage类型则跳过该模块
if (!text && module.type !== "usage") {
return null;
}
// 检查是否为十六进制颜色值
const isHexColor = (color: string) => /^#[0-9A-F]{6}$/i.test(color);
// 如果是Powerline样式添加背景色和分隔符
if (isPowerline) {
const bgColorClass = module.background ? ANSI_COLORS[module.background] || "" : "";
const textColorClass = module.color ? ANSI_COLORS[module.color] || "text-white" : "text-white";
// 处理背景色 - 支持ANSI颜色和十六进制颜色
let bgColorStyle = {};
let bgColorClass = "";
let separatorDataBg = "";
if (module.background) {
if (isHexColor(module.background)) {
bgColorStyle = { backgroundColor: module.background };
// 对于十六进制颜色我们直接使用颜色值作为data属性
separatorDataBg = module.background;
} else {
bgColorClass = ANSI_COLORS[module.background] || "";
separatorDataBg = module.background;
}
}
// 处理文字颜色 - 支持ANSI颜色和十六进制颜色
let textColorStyle = {};
let textColorClass = "";
if (module.color) {
if (isHexColor(module.color)) {
textColorStyle = { color: module.color };
} else {
textColorClass = ANSI_COLORS[module.color] || "text-white";
}
} else {
textColorClass = "text-white";
}
return (
<div className={`powerline-module ${bgColorClass} ${textColorClass}`}>
<div
className={`powerline-module px-4 ${bgColorClass} ${textColorClass}`}
style={{ ...bgColorStyle, ...textColorStyle }}
>
<div className="powerline-module-content">
{icon && <span>{icon}</span>}
<span>{text}</span>
</div>
<div
<div
className="powerline-separator"
data-current-bg={module.background || ""}
data-current-bg={separatorDataBg}
/>
</div>
);
}
// 处理默认样式下的颜色
let textStyle = {};
let textClass = "";
if (module.color) {
if (isHexColor(module.color)) {
textStyle = { color: module.color };
} else {
textClass = ANSI_COLORS[module.color] || "";
}
}
return (
<>
{icon && <span>{icon}</span>}
<span>{text}</span>
{icon && (
<span style={textStyle} className={textClass}>
{icon}
</span>
)}
<span style={textStyle} className={textClass}>
{text}
</span>
</>
);
}
interface StatusLineConfigDialogProps {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
}
export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfigDialogProps) {
export function StatusLineConfigDialog({
isOpen,
onOpenChange,
}: StatusLineConfigDialogProps) {
const { t } = useTranslation();
const { config, setConfig } = useConfig();
const [statusLineConfig, setStatusLineConfig] = useState<StatusLineConfig>(
config?.StatusLine || createDefaultStatusLineConfig()
);
// 字体状态
const [fontFamily, setFontFamily] = useState<string>(
config?.StatusLine?.fontFamily || "Hack Nerd Font Mono"
);
const [selectedModuleIndex, setSelectedModuleIndex] = useState<number | null>(
null
);
const [hexBackgroundColors, setHexBackgroundColors] = useState<Set<string>>(
new Set()
);
// 添加Powerline分隔符样式
useEffect(() => {
const styleElement = document.createElement('style');
const styleElement = document.createElement("style");
styleElement.innerHTML = `
.powerline-module {
display: inline-flex;
@@ -208,44 +299,90 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
.powerline-separator[data-current-bg="bg_bright_purple"] { border-left-color: #c084fc; }
`;
document.head.appendChild(styleElement);
// 清理函数
return () => {
document.head.removeChild(styleElement);
};
}, []);
const [statusLineConfig, setStatusLineConfig] = useState<StatusLineConfig>(
config?.StatusLine || createDefaultStatusLineConfig()
);
const [selectedModuleIndex, setSelectedModuleIndex] = useState<number | null>(null);
// 动态更新十六进制背景颜色的样式
useEffect(() => {
// 收集所有模块中使用的十六进制背景颜色
const hexColors = new Set<string>();
Object.keys(statusLineConfig).forEach((key) => {
const themeConfig = statusLineConfig[key as keyof StatusLineConfig];
if (
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
) {
const modules = (themeConfig as StatusLineThemeConfig).modules || [];
modules.forEach((module) => {
if (module.background && /^#[0-9A-F]{6}$/i.test(module.background)) {
hexColors.add(module.background);
}
});
}
});
setHexBackgroundColors(hexColors);
// 创建动态样式元素
const styleElement = document.createElement("style");
styleElement.id = "hex-powerline-styles";
// 生成十六进制颜色的CSS规则
let cssRules = "";
hexColors.forEach((color) => {
// 将十六进制颜色转换为RGB值
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
cssRules += `.powerline-separator[data-current-bg="${color}"] { border-left-color: rgb(${r}, ${g}, ${b}); }\n`;
});
styleElement.innerHTML = cssRules;
document.head.appendChild(styleElement);
// 清理函数
return () => {
const existingStyle = document.getElementById("hex-powerline-styles");
if (existingStyle) {
document.head.removeChild(existingStyle);
}
};
}, [statusLineConfig]);
// 模块类型选项
const MODULE_TYPES_OPTIONS = MODULE_TYPES.map(item => ({
...item,
label: t(`statusline.${item.label}`)
const MODULE_TYPES_OPTIONS = MODULE_TYPES.map((item) => ({
...item,
label: t(`statusline.${item.label}`),
}));
const handleThemeChange = (value: string) => {
setStatusLineConfig(prev => ({ ...prev, currentStyle: value }));
setStatusLineConfig((prev) => ({ ...prev, currentStyle: value }));
};
const handleModuleChange = (index: number, field: keyof StatusLineModuleConfig, value: string) => {
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const handleModuleChange = (
index: number,
field: keyof StatusLineModuleConfig,
value: string
) => {
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
const modules =
themeConfig && typeof themeConfig === "object" && "modules" in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
if (modules[index]) {
modules[index] = { ...modules[index], [field]: value };
}
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
};
@@ -254,67 +391,92 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
const handleSave = () => {
// 验证配置
const validationResult = validateStatusLineConfig(statusLineConfig);
if (!validationResult.isValid) {
// 格式化错误信息
const errorMessages = validationResult.errors.map(error =>
const errorMessages = validationResult.errors.map((error) =>
formatValidationError(error, t)
);
setValidationErrors(errorMessages);
return;
}
// 清除之前的错误
setValidationErrors([]);
if (config) {
setConfig({
...config,
StatusLine: statusLineConfig
StatusLine: {
...statusLineConfig,
fontFamily,
},
});
onOpenChange(false);
}
};
// 创建自定义Alert组件
const CustomAlert = ({
title,
description,
variant = "default"
}: {
title: string;
description: React.ReactNode;
variant?: "default" | "destructive";
const CustomAlert = ({
title,
description,
variant = "default",
}: {
title: string;
description: React.ReactNode;
variant?: "default" | "destructive";
}) => {
const isError = variant === "destructive";
return (
<div className={`rounded-lg border p-4 ${
isError
? "bg-red-50 border-red-200 text-red-800"
: "bg-blue-50 border-blue-200 text-blue-800"
}`}>
<div
className={`rounded-lg border p-4 ${
isError
? "bg-red-50 border-red-200 text-red-800"
: "bg-blue-50 border-blue-200 text-blue-800"
}`}
>
<div className="flex">
<div className="flex-shrink-0">
{isError ? (
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
<svg
className="h-5 w-5 text-red-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
) : (
<svg className="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
<svg
className="h-5 w-5 text-blue-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
)}
</div>
<div className="ml-3">
<h3 className={`text-sm font-medium ${
isError ? "text-red-800" : "text-blue-800"
}`}>
<h3
className={`text-sm font-medium ${
isError ? "text-red-800" : "text-blue-800"
}`}
>
{title}
</h3>
<div className={`mt-2 text-sm ${
isError ? "text-red-700" : "text-blue-700"
}`}>
<div
className={`mt-2 text-sm ${
isError ? "text-red-700" : "text-blue-700"
}`}
>
{description}
</div>
</div>
@@ -323,27 +485,54 @@ export function StatusLineConfigDialog({ isOpen, onOpenChange }: StatusLineConfi
);
};
const currentThemeKey = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentThemeConfig = statusLineConfig[currentThemeKey];
const currentModules = currentThemeConfig && typeof currentThemeConfig === 'object' && 'modules' in currentThemeConfig
? ((currentThemeConfig as StatusLineThemeConfig).modules || [])
: [];
const selectedModule = selectedModuleIndex !== null && currentModules.length > selectedModuleIndex ? currentModules[selectedModuleIndex] : null;
const currentThemeKey =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentThemeConfig = statusLineConfig[currentThemeKey];
const currentModules =
currentThemeConfig &&
typeof currentThemeConfig === "object" &&
"modules" in currentThemeConfig
? (currentThemeConfig as StatusLineThemeConfig).modules || []
: [];
const selectedModule =
selectedModuleIndex !== null && currentModules.length > selectedModuleIndex
? currentModules[selectedModuleIndex]
: null;
// 字体样式
const fontStyle = fontFamily ? { fontFamily } : {};
// 当字体或主题变化时强制重新渲染
const fontKey = `${fontFamily}-${statusLineConfig.currentStyle}`;
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl h-[90vh] overflow-hidden sm:max-w-5xl md:max-w-6xl lg:max-w-7xl animate-in fade-in-90 slide-in-from-bottom-10 duration-300 flex flex-col">
<DialogHeader data-testid="statusline-config-dialog-header" className="border-b pb-4">
<DialogHeader
data-testid="statusline-config-dialog-header"
className="border-b pb-4"
>
<DialogTitle className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="mr-2">
<path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M14 3v4a2 2 0 0 0 2 2h4"/>
<path d="M3 12h18"/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2"
>
<path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M14 3v4a2 2 0 0 0 2 2h4" />
<path d="M3 12h18" />
</svg>
{t("statusline.title")}
</DialogTitle>
</DialogHeader>
{/* 错误显示区域 */}
{validationErrors.length > 0 && (
<div className="px-6">
@@ -360,20 +549,20 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
/>
</div>
)}
<div className="flex flex-col gap-6 flex-1 overflow-hidden">
{/* 配置面板 */}
<div className="space-y-6">
{/* 主题样式选择 */}
<div className="flex items-center justify-between">
<Label htmlFor="theme-style" className="text-sm font-medium">
</Label>
<div className="w-1/2">
{/* 主题样式和字体选择 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="theme-style" className="text-sm font-medium">
</Label>
<Combobox
options={[
{ label: "默认", value: "default" },
{ label: "Powerline", value: "powerline" }
{ label: "Powerline", value: "powerline" },
]}
value={statusLineConfig.currentStyle}
onChange={handleThemeChange}
@@ -381,11 +570,22 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
placeholder="选择主题样式"
/>
</div>
<div className="space-y-2">
<Label htmlFor="font-family" className="text-sm font-medium">
</Label>
<Combobox
options={NERD_FONTS}
value={fontFamily}
onChange={(value) => setFontFamily(value)}
data-testid="font-family-selector"
placeholder="选择字体"
/>
</div>
</div>
</div>
</div>
{/* 三栏布局:组件列表 | 预览区域 | 属性配置 */}
<div className="grid grid-cols-5 gap-6 overflow-hidden flex-1">
{/* 左侧:支持的组件 */}
@@ -393,7 +593,7 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<h3 className="text-sm font-medium mb-3"></h3>
<div className="space-y-2 overflow-y-auto flex-1">
{MODULE_TYPES_OPTIONS.map((moduleType) => (
<div
<div
key={moduleType.value}
className="flex items-center gap-2 p-2 border rounded cursor-move hover:bg-secondary"
draggable
@@ -406,13 +606,19 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
))}
</div>
</div>
{/* 中间:预览区域 */}
<div className="border rounded-lg p-4 flex flex-col col-span-3">
<h3 className="text-sm font-medium mb-3"></h3>
<div
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 ${statusLineConfig.currentStyle === 'powerline' ? 'gap-0 h-8 p-0 items-center overflow-visible relative' : 'h-5 overflow-hidden'}`}
<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 ${
statusLineConfig.currentStyle === "powerline"
? "gap-0 h-8 p-0 items-center relative"
: "h-5"
}`}
data-testid="statusline-preview"
style={fontStyle}
onDragOver={(e) => {
e.preventDefault();
}}
@@ -421,36 +627,63 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
const moduleType = e.dataTransfer.getData("moduleType");
if (moduleType) {
// 添加新模块
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [
...((themeConfig as StatusLineThemeConfig)
.modules || []),
]
: [];
// 根据模块类型设置默认值
let newModule: StatusLineModuleConfig;
switch (moduleType) {
case "workDir":
newModule = { type: "workDir", icon: "󰉋", text: "{{workDirName}}", color: "bright_blue" };
newModule = {
type: "workDir",
icon: "󰉋",
text: "{{workDirName}}",
color: "bright_blue",
};
break;
case "gitBranch":
newModule = { type: "gitBranch", icon: "🌿", text: "{{gitBranch}}", color: "bright_green" };
newModule = {
type: "gitBranch",
icon: "🌿",
text: "{{gitBranch}}",
color: "bright_green",
};
break;
case "model":
newModule = { type: "model", icon: "🤖", text: "{{model}}", color: "bright_yellow" };
newModule = {
type: "model",
icon: "🤖",
text: "{{model}}",
color: "bright_yellow",
};
break;
case "usage":
newModule = { type: "usage", icon: "📊", text: "{{inputTokens}} → {{outputTokens}}", color: "bright_magenta" };
newModule = {
type: "usage",
icon: "📊",
text: "{{inputTokens}} → {{outputTokens}}",
color: "bright_magenta",
};
break;
default:
newModule = { ...DEFAULT_MODULE, type: moduleType };
}
modules.push(newModule);
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
}
}}
@@ -461,8 +694,14 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<div
key={index}
className={`cursor-pointer ${
selectedModuleIndex === index ? "bg-white/20" : "hover:bg-white/10"
} ${statusLineConfig.currentStyle === 'powerline' ? 'p-0 rounded-none inline-flex overflow-visible relative' : 'flex items-center gap-1 px-2 py-1 rounded'}`}
selectedModuleIndex === index
? "bg-white/20"
: "hover:bg-white/10"
} ${
statusLineConfig.currentStyle === "powerline"
? "p-0 rounded-none inline-flex overflow-visible relative"
: "flex items-center gap-1 px-2 py-1 rounded"
}`}
onClick={() => setSelectedModuleIndex(index)}
draggable
onDragStart={(e) => {
@@ -473,24 +712,41 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
}}
onDrop={(e) => {
e.preventDefault();
const dragIndex = parseInt(e.dataTransfer.getData("dragIndex"));
const dragIndex = parseInt(
e.dataTransfer.getData("dragIndex")
);
if (!isNaN(dragIndex) && dragIndex !== index) {
// 重新排序模块
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
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 (dragIndex >= 0 && dragIndex < modules.length && index >= 0 && index <= modules.length) {
const [movedModule] = modules.splice(dragIndex, 1);
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [
...((themeConfig as StatusLineThemeConfig)
.modules || []),
]
: [];
if (
dragIndex >= 0 &&
dragIndex < modules.length &&
index >= 0 &&
index <= modules.length
) {
const [movedModule] = modules.splice(
dragIndex,
1
);
modules.splice(index, 0, movedModule);
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
// 更新选中项的索引
if (selectedModuleIndex === dragIndex) {
setSelectedModuleIndex(index);
@@ -501,16 +757,30 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
}
}}
>
{renderModulePreview(module, statusLineConfig.currentStyle === 'powerline')}
{renderModulePreview(
module,
statusLineConfig.currentStyle === "powerline"
)}
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center w-full py-4 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-gray-500 mb-2">
<path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z"/>
<path d="M12 8v8"/>
<path d="M8 12h8"/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-gray-500 mb-2"
>
<path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z" />
<path d="M12 8v8" />
<path d="M8 12h8" />
</svg>
<span className="text-gray-500 text-sm">
@@ -519,7 +789,7 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
)}
</div>
</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>
@@ -531,84 +801,148 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
<Combobox
options={MODULE_TYPES_OPTIONS}
value={selectedModule.type}
onChange={(value) => handleModuleChange(selectedModuleIndex, "type", value)}
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>
<Label htmlFor="module-icon">
{t("statusline.module_icon")}
</Label>
<Input
key={fontKey}
id="module-icon"
value={selectedModule.icon || ""}
onChange={(e) => handleModuleChange(selectedModuleIndex, "icon", e.target.value)}
onChange={(e) =>
handleModuleChange(
selectedModuleIndex,
"icon",
e.target.value
)
}
placeholder="例如: 󰉋"
style={fontStyle}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="module-text">{t("statusline.module_text")}</Label>
<Label htmlFor="module-text">
{t("statusline.module_text")}
</Label>
<Input
id="module-text"
value={selectedModule.text}
onChange={(e) => handleModuleChange(selectedModuleIndex, "text", e.target.value)}
onChange={(e) =>
handleModuleChange(
selectedModuleIndex,
"text",
e.target.value
)
}
placeholder="例如: {{workDirName}}"
/>
<div className="text-xs text-muted-foreground">
<p>使:</p>
<div className="flex flex-wrap gap-1 mt-1">
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{workDirName}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{gitBranch}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{model}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{inputTokens}}"}</Badge>
<Badge variant="secondary" className="text-xs py-0.5 px-1.5">{"{{outputTokens}}"}</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{workDirName}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{gitBranch}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{model}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{inputTokens}}"}
</Badge>
<Badge
variant="secondary"
className="text-xs py-0.5 px-1.5"
>
{"{{outputTokens}}"}
</Badge>
</div>
</div>
</div>
<div className="space-y-2">
<Label>{t("statusline.module_color")}</Label>
<ColorPicker
value={selectedModule.color || ""}
onChange={(value) => handleModuleChange(selectedModuleIndex, "color", value)}
onChange={(value) =>
handleModuleChange(
selectedModuleIndex,
"color",
value
)
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label>{t("statusline.module_background")}</Label>
<ColorPicker
value={selectedModule.background || ""}
onChange={(value) => handleModuleChange(selectedModuleIndex, "background", value)}
onChange={(value) =>
handleModuleChange(
selectedModuleIndex,
"background",
value
)
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => {
const currentTheme = statusLineConfig.currentStyle as keyof StatusLineConfig;
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules = themeConfig && typeof themeConfig === 'object' && 'modules' in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [
...((themeConfig as StatusLineThemeConfig)
.modules || []),
]
: [];
modules.splice(selectedModuleIndex, 1);
setStatusLineConfig(prev => ({
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules }
[currentTheme]: { modules },
}));
setSelectedModuleIndex(null);
}}
>
@@ -617,24 +951,26 @@ const selectedModule = selectedModuleIndex !== null && currentModules.length > s
</div>
) : (
<div className="flex items-center justify-center h-full min-h-[200px]">
<p className="text-muted-foreground text-sm"></p>
<p className="text-muted-foreground text-sm">
</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
<DialogFooter className="border-t pt-4 mt-4">
<Button
variant="outline"
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="transition-all hover:scale-105"
>
{t("app.cancel")}
</Button>
<Button
onClick={handleSave}
<Button
onClick={handleSave}
data-testid="save-statusline-config"
className="transition-all hover:scale-105"
>

View File

@@ -6,7 +6,6 @@ import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
interface ColorPickerProps {
value?: string;
@@ -15,42 +14,8 @@ interface ColorPickerProps {
showPreview?: boolean;
}
// 预定义的ANSI颜色映射
const ANSI_COLOR_MAP: Record<string, string> = {
"black": "#000000",
"red": "#ff0000",
"green": "#00ff00",
"yellow": "#ffff00",
"blue": "#0000ff",
"magenta": "#ff00ff",
"cyan": "#00ffff",
"white": "#ffffff",
"bright_black": "#808080",
"bright_red": "#ff8080",
"bright_green": "#80ff80",
"bright_yellow": "#ffff80",
"bright_blue": "#8080ff",
"bright_magenta": "#ff80ff",
"bright_cyan": "#80ffff",
"bright_white": "#ffffff"
}
// 背景颜色映射添加bg_前缀
const ANSI_BG_COLOR_MAP: Record<string, string> = Object.keys(ANSI_COLOR_MAP).reduce((acc, key) => {
acc[`bg_${key}`] = ANSI_COLOR_MAP[key]
return acc
}, {} as Record<string, string>)
// 合并所有颜色映射
const ALL_COLOR_MAP = { ...ANSI_COLOR_MAP, ...ANSI_BG_COLOR_MAP }
// 获取颜色值的函数
const getColorValue = (color: string): string => {
// 如果是预定义的ANSI颜色
if (ALL_COLOR_MAP[color]) {
return ALL_COLOR_MAP[color]
}
// 如果是十六进制颜色
if (color.startsWith("#")) {
return color
@@ -91,15 +56,8 @@ export function ColorPicker({
}
}
const handlePresetColorClick = (colorName: string) => {
handleColorChange(colorName)
setOpen(false)
}
const selectedColorValue = getColorValue(value)
// 获取ANSI颜色名称如果适用
const ansiColorName = Object.keys(ALL_COLOR_MAP).find(key => ALL_COLOR_MAP[key] === selectedColorValue) || value
const selectedColorValue = getColorValue(value)
return (
<div className="space-y-2">
@@ -120,7 +78,7 @@ export function ColorPicker({
/>
)}
<span className="truncate flex-1">
{value ? ansiColorName : placeholder}
{value || placeholder}
</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m7 15 5 5 5-5"/>
@@ -152,7 +110,7 @@ export function ColorPicker({
/>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{value ? ansiColorName : "未选择颜色"}
{value || "未选择颜色"}
</div>
{value && value.startsWith("#") && (
<div className="text-xs text-muted-foreground font-mono">
@@ -184,7 +142,12 @@ export function ColorPicker({
/>
<Button
size="sm"
onClick={() => customColor && handleColorChange(customColor)}
onClick={() => {
if (customColor && /^#[0-9A-F]{6}$/i.test(customColor)) {
handleColorChange(customColor)
setOpen(false)
}
}}
disabled={!customColor || !/^#[0-9A-F]{6}$/i.test(customColor)}
>
@@ -194,66 +157,6 @@ export function ColorPicker({
(: #FF0000)
</p>
</div>
{/* 预定义颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">ANSI </label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
{/* 背景颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium"></label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_BG_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
</div>
</PopoverContent>
</Popover>

View File

@@ -44,6 +44,7 @@ export interface StatusLineConfig {
currentStyle: string;
default: StatusLineThemeConfig;
powerline: StatusLineThemeConfig;
fontFamily?: string;
}
export interface Config {