From 593feddc3fa04c495c9e21c25197b42319d3b60b Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Sat, 23 May 2026 15:56:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor(canvas):=20=E4=BC=98=E5=8C=96=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=80=89=E6=8B=A9=E5=99=A8=E5=92=8C=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将模型选择器替换为统一的 shadcn 风格下拉组件 - 移除原生数字输入框的步进箭头样式 - 更新默认定时同步配置为每5分钟执行 - 修复画布助手和节点配置中的模型选择逻辑 - 统一各页面中的模型选择器样式和交互行为 --- CHANGELOG.md | 3 + docs/backend-database.md | 4 +- docs/pending-test.md | 4 +- docs/system-settings.md | 8 +- model/setting.go | 2 +- service/prompt_sync_scheduler.go | 8 +- web/src/app/(admin)/admin/settings/page.tsx | 10 +- .../components/canvas-assistant-panel.tsx | 48 +---- .../canvas-image-settings-popover.tsx | 6 +- web/src/app/globals.css | 113 ++-------- web/src/components/model-picker.module.css | 29 --- web/src/components/model-picker.tsx | 85 +++++--- web/src/components/ui/select.tsx | 196 ++++++++++++++++++ 13 files changed, 303 insertions(+), 213 deletions(-) delete mode 100644 web/src/components/model-picker.module.css create mode 100644 web/src/components/ui/select.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 430d0eb..35b1b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,12 @@ ## Unreleased +## v0.0.7 - 2026-05-23 + + [新增] 管理后台提示词管理支持多选批量删除。 + [新增] 新增定义拉取GitHub提示词源功能。 + [新增] 新增awesome-gpt-image2-prompts提示词来源。 ++ [优化] 优化模型下拉选择样式、优化生图编辑设置 ## v0.0.6 - 2026-05-22 diff --git a/docs/backend-database.md b/docs/backend-database.md index d657e6a..16e0c08 100644 --- a/docs/backend-database.md +++ b/docs/backend-database.md @@ -148,8 +148,8 @@ | 字段 | 类型 | 说明 | | --- | --- | --- | -| `enabled` | bool | 是否开启定时同步 | -| `cron` | string | Cron 表达式,默认每天 03:00 | +| `enabled` | bool | 是否开启定时同步,默认开启 | +| `cron` | string | Cron 表达式,默认每 5 分钟 | 后端请求模型时,先按模型名筛选启用且包含该模型的渠道,再按 `weight` 加权随机选择一个渠道。 diff --git a/docs/pending-test.md b/docs/pending-test.md index 6b1ab7e..9876946 100644 --- a/docs/pending-test.md +++ b/docs/pending-test.md @@ -1,6 +1,8 @@ # 待测试 -- 模型选择器会按模型名称模糊匹配并显示 OpenAI、Claude、Gemini 图标,且下拉滚动条改为更窄更轻的样式,需要确认生图页、画布助手、节点配置和配置弹窗中的模型下拉项及选中态显示正常,搜索仍可按模型名称过滤。 +- 模型选择器改为共享的 shadcn 风格下拉,按模型名称显示 OpenAI、Claude、Gemini 图标,并把画布节点下方、右侧助手和配置弹窗统一到同一套组件;右侧助手的模型和模式按钮会随工具栏宽度自动在文字态和图标态之间切换,需要确认生图页、画布助手、节点配置和配置弹窗中的选中态和下拉列表显示正常。 +- 画布图像设置里的尺寸输入去掉了原生数字步进箭头,需要确认宽高仍可直接输入、禁用态正常、设置值回写正常。 +- 后台提示词定时同步默认改为开启且默认每 5 分钟执行一次,需要确认新环境未保存过设置时开关默认打开、Cron 默认值正确,用户手动关闭或修改 Cron 后能保持配置。 - 画布右侧助手和节点下方对话框的生图设置弹层改为紧凑图像设置面板,面板只包含质量、尺寸、宽高比和生成数量选择,模型选择保留在对话框底部工具栏;需要确认助手内设置面板不会被右侧面板裁切,节点内质量可正常选择并参与生成、尺寸可编辑、宽高比预览显示正常且为直角矩形、auto 不重复显示、张数每行 4 个且自定义输入框能显示当前值、实际生图参数正常;节点设置面板打开时会隐藏节点悬浮工具条,避免遮挡质量选择。 - 管理后台 `/admin/prompts` 新增提示词批量删除,需要确认多选、确认弹窗、删除后列表刷新和筛选条件下删除行为。 - 管理后台 `/admin/settings` 的私有配置新增提示词定时同步开关和 Cron 表达式;开启后后端会按配置同步内置 GitHub 远程提示词源,需要确认保存配置和到点同步行为。 diff --git a/docs/system-settings.md b/docs/system-settings.md index de22913..f1db66f 100644 --- a/docs/system-settings.md +++ b/docs/system-settings.md @@ -61,8 +61,8 @@ } ], "promptSync": { - "enabled": false, - "cron": "0 3 * * *" + "enabled": true, + "cron": "*/5 * * * *" } } ``` @@ -91,5 +91,5 @@ | 字段 | 类型 | 说明 | | --- | --- | --- | -| `enabled` | boolean | 是否开启定时同步 | -| `cron` | string | Cron 表达式,默认每天 03:00 | +| `enabled` | boolean | 是否开启定时同步,默认开启 | +| `cron` | string | Cron 表达式,默认每 5 分钟 | diff --git a/model/setting.go b/model/setting.go index f4786ab..c4cdf1a 100644 --- a/model/setting.go +++ b/model/setting.go @@ -44,7 +44,7 @@ type PrivateSetting struct { // PromptSyncSetting 提示词定时同步配置。 type PromptSyncSetting struct { - Enabled bool `json:"enabled"` + Enabled *bool `json:"enabled"` Cron string `json:"cron"` } diff --git a/service/prompt_sync_scheduler.go b/service/prompt_sync_scheduler.go index 4951ba9..387864c 100644 --- a/service/prompt_sync_scheduler.go +++ b/service/prompt_sync_scheduler.go @@ -9,7 +9,7 @@ import ( "github.com/robfig/cron/v3" ) -const defaultPromptSyncCron = "0 3 * * *" +const defaultPromptSyncCron = "*/5 * * * *" var ( promptSyncCron *cron.Cron @@ -40,7 +40,7 @@ func RefreshPromptSyncScheduler() { return } setting := normalizePromptSyncSetting(settings.Private.PromptSync) - if !setting.Enabled { + if setting.Enabled == nil || !*setting.Enabled { return } if _, err := promptSyncCron.AddFunc(setting.Cron, SyncRemotePromptCategories); err != nil { @@ -66,5 +66,9 @@ func normalizePromptSyncSetting(setting model.PromptSyncSetting) model.PromptSyn if setting.Cron == "" { setting.Cron = defaultPromptSyncCron } + if setting.Enabled == nil { + enabled := true + setting.Enabled = &enabled + } return setting } diff --git a/web/src/app/(admin)/admin/settings/page.tsx b/web/src/app/(admin)/admin/settings/page.tsx index 3e8cbcc..3177735 100644 --- a/web/src/app/(admin)/admin/settings/page.tsx +++ b/web/src/app/(admin)/admin/settings/page.tsx @@ -35,7 +35,7 @@ const emptySettings: AdminSettings = { allowCustomChannel: true, }, }, - private: { channels: [], promptSync: { enabled: false, cron: "0 3 * * *" } }, + private: { channels: [], promptSync: { enabled: true, cron: "*/5 * * * *" } }, }; const emptyChannel: AdminModelChannel = { protocol: "openai", name: "", baseUrl: "", apiKey: "", models: [], weight: 1, enabled: true, remark: "" }; @@ -367,8 +367,8 @@ export default function AdminSettingsPage() { - - + + @@ -601,8 +601,8 @@ function normalizePrivateSetting(setting: Partial = {} return { channels: (setting.channels || []).map(normalizeChannel), promptSync: { - enabled: setting.promptSync?.enabled === true, - cron: setting.promptSync?.cron || "0 3 * * *", + enabled: setting.promptSync?.enabled !== false, + cron: setting.promptSync?.cron || "*/5 * * * *", }, }; } diff --git a/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx b/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx index 7f7a6ae..0f44656 100644 --- a/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx +++ b/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx @@ -1,11 +1,12 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import { ArrowUp, ChevronDown, Cpu, History, ImageIcon, LoaderCircle, MessageSquare, PanelRightClose, Plus, RotateCcw, Settings2, Sparkles, Trash2, X } from "lucide-react"; -import { Button, Dropdown, Modal, Tooltip } from "antd"; +import { ArrowUp, History, ImageIcon, LoaderCircle, MessageSquare, PanelRightClose, Plus, RotateCcw, Settings2, Sparkles, Trash2, X } from "lucide-react"; +import { Button, Modal, Tooltip } from "antd"; import { motion } from "motion/react"; import { ImageGenerationPending } from "@/components/image-generation-pending"; +import { ModelPicker } from "@/components/model-picker"; import { useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; import { canvasThemes } from "@/lib/canvas-theme"; import { nanoid } from "nanoid"; @@ -421,11 +422,11 @@ function AssistantComposer({ {mode === "image" ? ( <> - onConfigChange("imageModel", model)} onMissingConfig={onMissingConfig} /> + onConfigChange("imageModel", model)} onMissingConfig={onMissingConfig} /> document.body} buttonClassName="canvas-composer-settings canvas-composer-icon !h-8 !min-w-8 !rounded-full !px-2" onConfigChange={onConfigChange} onMissingConfig={onMissingConfig} /> ) : ( - onConfigChange("textModel", model)} onMissingConfig={onMissingConfig} /> + onConfigChange("textModel", model)} onMissingConfig={onMissingConfig} /> )} - - ); -} - -function ModelMenuLabel({ model }: { model: string }) { - return ( - - - {model} - - ); -} - -function ModelIcon({ model }: { model: string }) { - const name = model.toLowerCase(); - const icon = name.includes("claude") || name.includes("anthropic") ? "/icons/claude.svg" : name.includes("gemini") || name.includes("google") ? "/icons/gemini.svg" : name.includes("gpt") || name.includes("openai") ? "/icons/openai.svg" : ""; - return icon ? : ; -} - function AssistantModeSwitch({ mode, theme, onChange }: { mode: AssistantMode; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (mode: AssistantMode) => void }) { return (
diff --git a/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx b/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx index 03b01ff..f0ce3dd 100644 --- a/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx +++ b/web/src/app/(user)/canvas/components/canvas-image-settings-popover.tsx @@ -105,7 +105,7 @@ export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChang
- 生成数量 + 生成张数
{Array.from({ length: 10 }, (_, index) => index + 1).map((value) => ( onConfigChange("count", String(value))}> @@ -165,7 +165,7 @@ function DimensionInput({ prefix, value, disabled, theme, onChange }: { prefix: type="number" min={1} disabled={disabled} - className="min-w-0 flex-1 bg-transparent px-2 outline-none" + className="min-w-0 flex-1 bg-transparent px-2 outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" value={value || ""} onChange={(event) => onChange(Number(event.target.value) || null)} onMouseDown={(event) => event.stopPropagation()} @@ -181,7 +181,7 @@ function CountInput({ value, theme, onChange }: { value: number; theme: (typeof type="number" min={1} max={15} - className="min-w-0 flex-1 bg-transparent px-3 text-center outline-none" + className="min-w-0 flex-1 bg-transparent px-3 text-center outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" style={{ color: theme.node.text, WebkitTextFillColor: theme.node.text }} value={value || ""} onChange={(event) => onChange(Number(event.target.value) || null)} diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 5871b3a..74431ae 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -319,65 +319,46 @@ line-height: 1 !important; } - .canvas-composer-tools { - container-type: inline-size; - } - - .canvas-composer-icon.ant-btn, - .canvas-composer-model-pill { + .canvas-composer-icon.ant-btn { flex: 0 0 32px; width: 32px; } + .canvas-composer-tools { + container-type: inline-size; + } + .canvas-composer-mode-button { width: 32px; } - .canvas-composer-model-text, - .canvas-composer-model-arrow, .canvas-composer-mode-button span, + .canvas-model-picker-text, + .canvas-select-chevron, .canvas-composer-icon.ant-btn > span:not(.ant-btn-icon) { display: none; } - .canvas-composer-model-pill { - align-items: center; - border: 0; - border-radius: 999px; - cursor: pointer; - display: flex; - gap: 7px; - height: 32px; + .canvas-composer-model-picker { + width: 32px; + min-width: 32px; justify-content: center; - padding-inline: 8px; - transition: opacity 0.15s ease; + gap: 0; + padding-inline: 0; } - .canvas-composer-model-pill:hover { - opacity: 0.82; - } - - @container (min-width: 400px) { + @container (min-width: 440px) { .canvas-composer-mode-button { padding-inline: 8px; width: auto; } - .canvas-composer-mode-button span, - .canvas-composer-model-text, - .canvas-composer-model-arrow { + .canvas-composer-mode-button span { display: inline; font-size: 12px; line-height: 1; } - .canvas-composer-model-pill { - flex-basis: 180px; - max-width: 180px; - width: 180px !important; - justify-content: flex-start; - } - .canvas-composer-icon.ant-btn { flex: 0 1 auto; max-width: 170px; @@ -388,69 +369,21 @@ display: inline-flex; min-width: 0; } - } - @container (min-width: 560px) { - .canvas-composer-model-pill { - flex-basis: 240px; - max-width: 240px; - width: 240px !important; + .canvas-composer-model-picker { + width: auto; + min-width: 9rem; + justify-content: flex-start; + gap: 0.5rem; + padding-inline: 0.75rem; } - .canvas-composer-icon.ant-btn { - flex: 0 1 auto; - max-width: 220px; + .canvas-model-picker-text, + .canvas-select-chevron { + display: inline; } } - .canvas-model-dropdown .ant-dropdown-menu { - border: 1px solid rgba(120, 113, 108, 0.18); - border-radius: 12px; - box-shadow: 0 18px 42px rgba(28, 25, 23, 0.18); - min-width: 220px; - padding: 6px; - } - - .canvas-model-dropdown .ant-dropdown-menu-item { - border-radius: 8px; - color: #292524; - font-size: 13px; - line-height: 1; - margin: 1px 0; - min-height: 32px; - padding: 7px 10px !important; - } - - .canvas-model-dropdown .ant-dropdown-menu-item:hover { - background: rgba(28, 25, 23, 0.06) !important; - } - - .canvas-model-dropdown .ant-dropdown-menu-item-selected, - .canvas-model-dropdown .ant-dropdown-menu-item-selected:hover { - background: rgba(28, 25, 23, 0.1) !important; - color: #1c1917 !important; - font-weight: 500; - } - - .dark .canvas-model-dropdown .ant-dropdown-menu { - background: #1f1d1a; - border-color: rgba(214, 211, 209, 0.14); - } - - .dark .canvas-model-dropdown .ant-dropdown-menu-item { - color: #f5f5f4; - } - - .dark .canvas-model-dropdown .ant-dropdown-menu-item:hover { - background: rgba(245, 245, 244, 0.08) !important; - } - - .dark .canvas-model-dropdown .ant-dropdown-menu-item-selected, - .dark .canvas-model-dropdown .ant-dropdown-menu-item-selected:hover { - background: rgba(245, 245, 244, 0.14) !important; - color: #fafaf9 !important; - } - .canvas-control-select.ant-select-focused .ant-select-selector, .canvas-control-number.ant-input-number-focused { border-color: #a8a29e !important; diff --git a/web/src/components/model-picker.module.css b/web/src/components/model-picker.module.css deleted file mode 100644 index d4b379e..0000000 --- a/web/src/components/model-picker.module.css +++ /dev/null @@ -1,29 +0,0 @@ -.dropdown :global(.ant-select-item) { - font-weight: 400; - min-height: 34px; - padding-inline: 10px 28px; -} - -.dropdown :global(.ant-select-item-option-selected) { - font-weight: 500; -} - -.dropdown :global(.rc-virtual-list-holder) { - scrollbar-width: none; -} - -.dropdown :global(.rc-virtual-list-holder)::-webkit-scrollbar, -.dropdown :global(.rc-virtual-list-holder)::-webkit-scrollbar-button { - height: 0; - width: 0; -} - -.dropdown :global(.rc-virtual-list-scrollbar) { - right: 4px !important; - width: 2px !important; -} - -.dropdown :global(.rc-virtual-list-scrollbar-thumb) { - background: rgba(120, 113, 108, 0.42) !important; - border-radius: 999px !important; -} diff --git a/web/src/components/model-picker.tsx b/web/src/components/model-picker.tsx index f407ae6..ac826c3 100644 --- a/web/src/components/model-picker.tsx +++ b/web/src/components/model-picker.tsx @@ -1,9 +1,10 @@ "use client"; -import { Select } from "antd"; +import { useMemo, useState } from "react"; import { Cpu } from "lucide-react"; -import styles from "./model-picker.module.css"; +import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; import type { AiConfig } from "@/stores/use-config-store"; type ModelPickerProps = { @@ -17,50 +18,68 @@ type ModelPickerProps = { }; export function ModelPicker({ config, value, onChange, className, fullWidth = false, placeholder = "选择模型", onMissingConfig }: ModelPickerProps) { - const options = Array.from(new Set([value, ...config.models].filter(Boolean))).map((model) => ({ value: model, label: })); - const width = fullWidth ? "100%" : `min(${Math.max(156, (value || placeholder).length * 8 + 64)}px, 100%)`; + const [open, setOpen] = useState(false); + const options = useMemo(() => Array.from(new Set([value, ...config.models].filter(Boolean))), [config.models, value]); + const current = value || ""; return ( ); } function ModelLabel({ model }: { model: string }) { - const icon = resolveModelIcon(model); return ( - - {icon ? : } - {model} + + + {model} ); } +function ModelIcon({ model }: { model: string }) { + const icon = resolveModelIcon(model); + return icon ? : ; +} + function resolveModelIcon(model: string) { const name = model.toLowerCase(); if (name.includes("claude") || name.includes("anthropic")) return "/icons/claude.svg"; diff --git a/web/src/components/ui/select.tsx b/web/src/components/ui/select.tsx new file mode 100644 index 0000000..706d3d9 --- /dev/null +++ b/web/src/components/ui/select.tsx @@ -0,0 +1,196 @@ +"use client" + +import * as React from "react" +import { Select as SelectPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + hideChevron = false, + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" + hideChevron?: boolean +}) { + return ( + + {children} + {!hideChevron ? ( + + + + ) : null} + + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}