From 8a09806b8c40cf15b6bc46df56cce737e0ce91a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Thu, 17 Jul 2025 08:18:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=B9=E6=A1=88=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=99=A8):=20=E6=B7=BB=E5=8A=A0Q2=E6=96=B9=E6=A1=88=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=99=A8=E7=95=8C=E9=9D=A2=E5=8F=8A=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现Q2方案构建器的完整界面,包括颜色选择、自定义颜色管理、自动化参数配置和方案设置 添加构建和保存草稿功能,支持生成完整的色彩方案 包含错误处理和表单验证逻辑 --- src/page-components/scheme/Q2Scheme.tsx | 4 + .../scheme/q-2-scheme/Builder.module.css | 44 ++ .../scheme/q-2-scheme/Builder.tsx | 515 +++++++++++++++++- 3 files changed, 561 insertions(+), 2 deletions(-) diff --git a/src/page-components/scheme/Q2Scheme.tsx b/src/page-components/scheme/Q2Scheme.tsx index 3a79ae9..f15ac69 100644 --- a/src/page-components/scheme/Q2Scheme.tsx +++ b/src/page-components/scheme/Q2Scheme.tsx @@ -5,6 +5,7 @@ import { SchemeContent } from '../../models'; import { Q2SchemeStorage } from '../../q-2-scheme'; import { isNilOrEmpty } from '../../utls'; import { SchemeExport } from './Export'; +import { Q2SchemeBuilder } from './q-2-scheme/Builder'; const tabOptions = [ { title: 'Overview', id: 'overview' }, @@ -32,6 +33,9 @@ export function Q2Scheme({ scheme }: Q2SchemeProps) { export: isNilOrEmpty(scheme.schemeStorage?.cssVariables), }} /> + {isEqual(activeTab, 'builder') && ( + setActiveTab('overview')} /> + )} {isEqual(activeTab, 'export') && } ); diff --git a/src/page-components/scheme/q-2-scheme/Builder.module.css b/src/page-components/scheme/q-2-scheme/Builder.module.css index e69de29..9e95805 100644 --- a/src/page-components/scheme/q-2-scheme/Builder.module.css +++ b/src/page-components/scheme/q-2-scheme/Builder.module.css @@ -0,0 +1,44 @@ +@layer pages { + .builder_layout { + padding: var(--spacing-s) var(--spacing-m); + font-size: var(--font-size-s); + line-height: 1.3em; + display: grid; + grid-template-columns: repeat(3, 200px); + align-items: center; + gap: var(--spacing-xs); + .label { + max-width: 200px; + grid-column: 1; + padding-inline-end: var(--spacing-m); + text-align: right; + } + .color_picker_row { + grid-column: 2 / span 2; + display: flex; + align-items: center; + gap: var(--spacing-s); + .error_msg { + color: var(--color-danger); + font-size: var(--font-size-xs); + } + } + .segment_title { + grid-column: 1 / span 2; + text-align: center; + } + .parameter_input { + max-width: 8em; + } + .button_row { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--spacing-s); + } + h5 { + font-size: var(--font-size-m); + line-height: 1.7em; + } + } +} diff --git a/src/page-components/scheme/q-2-scheme/Builder.tsx b/src/page-components/scheme/q-2-scheme/Builder.tsx index 01b63ec..f434d97 100644 --- a/src/page-components/scheme/q-2-scheme/Builder.tsx +++ b/src/page-components/scheme/q-2-scheme/Builder.tsx @@ -1,6 +1,17 @@ +import { ColorExpand, ColorShifting, SchemeSetting, WACGSetting } from 'color-module'; +import { includes, isEmpty, isNil } from 'lodash-es'; +import { useActionState, useCallback, useMemo, useState } from 'react'; +import { useColorFunction } from '../../../ColorFunctionContext'; +import { FloatColorPicker } from '../../../components/FloatColorPicker'; +import { NotificationType, useNotification } from '../../../components/Notifications'; import { ScrollArea } from '../../../components/ScrollArea'; +import { VSegmentedControl } from '../../../components/VSegmentedControl'; import { SchemeContent } from '../../../models'; -import { Q2SchemeStorage } from '../../../q-2-scheme'; +import { Q2SchemeSource, Q2SchemeStorage } from '../../../q-2-scheme'; +import { useUpdateScheme } from '../../../stores/schemes'; +import { isNilOrEmpty } from '../../../utls'; +import { ColorEntry, IdenticalColorEntry } from '../ColorEntry'; +import styles from './Builder.module.css'; type Q2SchemeBuilderProps = { scheme: SchemeContent; @@ -8,5 +19,505 @@ type Q2SchemeBuilderProps = { }; export function Q2SchemeBuilder({ scheme, onBuildCompleted }: Q2SchemeBuilderProps) { - return ; + const { showToast } = useNotification(); + const { colorFn } = useColorFunction(); + const updateScheme = useUpdateScheme(scheme.id); + + // Load scheme setting and scheme default setting. + const defaultSetting = useMemo(() => { + try { + if (!colorFn) throw 'Web Assembly functions is not available'; + const defaultValues = colorFn.q_scheme_default_settings(); + if (scheme.schemeStorage.source?.setting) { + return new SchemeSetting( + new ColorShifting( + scheme.schemeStorage.source?.setting.hover.chroma ?? defaultValues.hover.chroma, + scheme.schemeStorage.source?.setting.hover.lightness ?? defaultValues.hover.lightness, + ), + new ColorShifting( + scheme.schemeStorage.source?.setting?.active.chroma ?? defaultValues.active.chroma, + scheme.schemeStorage.source?.setting?.active.lightness ?? + defaultValues.active.lightness, + ), + new ColorShifting( + scheme.schemeStorage.source?.setting?.focus.chroma ?? defaultValues.focus.chroma, + scheme.schemeStorage.source?.setting?.focus.lightness ?? defaultValues.focus.lightness, + ), + new ColorShifting( + scheme.schemeStorage.source?.setting?.disabled.chroma ?? defaultValues.disabled.chroma, + scheme.schemeStorage.source?.setting?.disabled.lightness ?? + defaultValues.disabled.lightness, + ), + new ColorShifting( + scheme.schemeStorage.source?.setting?.dark_convert.chroma ?? + defaultValues.dark_convert.chroma, + scheme.schemeStorage.source?.setting?.dark_convert.lightness ?? + defaultValues.dark_convert.lightness, + ), + scheme.schemeStorage.source?.setting?.expand_method ?? defaultValues.expand_method, + scheme.schemeStorage.source?.setting?.wacg_follows ?? defaultValues.wacg_follows, + ); + } + return defaultValues; + } catch (e) { + console.error('[Q2 Scheme builder]', e); + } + }, [scheme]); + + // Collect choices in color scheme settings + const expandingMethods = useMemo(() => { + try { + if (!colorFn) throw 'Web Assembly functions is not available'; + return colorFn.q_scheme_color_expanding_methods(); + } catch (e) { + console.error('[Q scheme builder]', e); + } + return []; + }, []); + const wacgFollowStrategies = useMemo(() => { + try { + if (!colorFn) throw 'Web Assembly functions is not available'; + return colorFn.q_scheme_wacg_settings(); + } catch (e) { + console.error('[Q scheme builder]', e); + } + return []; + }, []); + + // Custom Colors processing + const originalColors = useMemo(() => { + return Object.entries(scheme.schemeStorage.source?.custom_colors ?? {}).map( + ([name, color], index) => ({ id: `oc_${index}`, name, color } as IdenticalColorEntry), + ); + }, [scheme.schemeStorage.source]); + const [newColors, setNewColors] = useState([]); + const [deleted, setDeleted] = useState([]); + const addEntryAction = useCallback(() => { + setNewColors((prev) => [...prev, { id: `nc_${prev.length}`, name: '', color: '' }]); + }, []); + const colorKeys = useMemo( + () => + [...originalColors, ...newColors] + .map((color) => color.id) + .filter((c) => !includes(deleted, c)), + [originalColors, newColors, deleted], + ); + + // Collect scheme source + const collectSchemeSource = (formData: FormData): [Q2SchemeSource, QSchemeSetting] => { + const primaryColor = formData.get('primary')?.toString(); + const secondaryColor = formData.get('secondary')?.toString(); + const tertiaryColor = formData.get('tertiary')?.toString(); + const accentColor = formData.get('accent')?.toString(); + const dangerColor = formData.get('danger')?.toString(); + const successColor = formData.get('success')?.toString(); + const warnColor = formData.get('warn')?.toString(); + const infoColor = formData.get('info')?.toString(); + const foregroundColor = formData.get('foreground')?.toString(); + const backgroundColor = formData.get('background')?.toString(); + + const customColors: Record = {}; + for (const key of colorKeys) { + const name = formData.get(`name_${key}`)?.toString(); + const color = formData.get(`color_${key}`)?.toString(); + if (isNil(name) || isEmpty(name) || isNil(color) || isEmpty(color)) continue; + customColors[name] = color; + } + + // collect scheme settings + const schemeSetting = new SchemeSetting( + new ColorShifting( + Number(formData.get('hover_chroma')) / 100, + Number(formData.get('hover_lightness')) / 100, + ), + new ColorShifting( + Number(formData.get('active_chroma')) / 100, + Number(formData.get('active_lightness')) / 100, + ), + new ColorShifting( + Number(formData.get('focus_chroma')) / 100, + Number(formData.get('focus_lightness')) / 100, + ), + new ColorShifting( + Number(formData.get('disabled_chroma')) / 100, + Number(formData.get('disabled_lightness')) / 100, + ), + new ColorShifting( + Number(formData.get('dark_chroma')) / 100, + Number(formData.get('dark_lightness')) / 100, + ), + Number(formData.get('expanding')) as ColorExpand, + Number(formData.get('wacg')) as WACGSetting, + ); + const dumpedSetting = schemeSetting.toJsValue() as QSchemeSetting; + + return [ + { + primary: primaryColor, + secondary: secondaryColor, + tertiary: tertiaryColor, + accent: accentColor, + danger: dangerColor, + success: successColor, + warn: warnColor, + info: infoColor, + foreground: foregroundColor, + background: backgroundColor, + custom_colors: customColors, + setting: dumpedSetting, + } as Q2SchemeSource, + schemeSetting, + ]; + }; + + // Scheme save actions + const handleDraftAction = (formData: FormData) => { + const [source] = collectSchemeSource(formData); + updateScheme((prev) => { + prev.schemeStorage.source = source; + return prev; + }); + showToast(NotificationType.SUCCESS, 'Scheme draft saved!', 'tabler:device-floppy', 3000); + }; + const [errMsg, handleSubmitAction] = useActionState, FormData>( + (_state, formData) => { + const errMsg = new Map(); + + // Check required color + const requiredFields = [ + 'primary', + 'danger', + 'success', + 'warn', + 'info', + 'foreground', + 'background', + ]; + for (const field of requiredFields) { + if (!formData.get(field)) { + errMsg.set(field, 'This color is required for scheme generating.'); + } + } + if (!isEmpty(errMsg)) return errMsg; + + try { + const [source, settings] = collectSchemeSource(formData); + console.log('[Collected form data]', source, settings); + const generatedScheme = colorFn?.generate_q_scheme_2_manually( + source.primary ?? '', + isEmpty(source.secondary) ? undefined : source.secondary, + isEmpty(source.tertiary) ? undefined : source.tertiary, + isEmpty(source.accent) ? undefined : source.accent, + source.danger ?? '', + source.success ?? '', + source.warn ?? '', + source.info ?? '', + source.foreground ?? '', + source.background ?? '', + source.custom_colors, + settings, + ); + console.log('[Generated scheme]', generatedScheme); + updateScheme((prev) => { + prev.schemeStorage.source = source; + prev.schemeStorage.scheme = generatedScheme[0]; + prev.schemeStorage.cssVariables = generatedScheme[1]; + prev.schemeStorage.cssAutoSchemeVariables = generatedScheme[2]; + prev.schemeStorage.scssVariables = generatedScheme[3]; + prev.schemeStorage.jsVariables = generatedScheme[4]; + return prev; + }); + onBuildCompleted?.(); + } catch (e) { + console.error('[build q2 scheme]', e); + } + + return errMsg; + }, + new Map(), + ); + + return ( + +
+
Original Colors
+ +
+ + {errMsg.has('primary') && ( + {errMsg.get('primary')} + )} +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + {errMsg.has('danger') && {errMsg.get('danger')}} +
+ +
+ + {errMsg.has('success') && ( + {errMsg.get('success')} + )} +
+ +
+ + {errMsg.has('warn') && {errMsg.get('warn')}} +
+ +
+ + {errMsg.has('info') && {errMsg.get('info')}} +
+ +
+ + {errMsg.has('foreground') && ( + {errMsg.get('foreground')} + )} +
+ +
+ + {errMsg.has('background') && ( + {errMsg.get('background')} + )} +
+ +
Custom Colors
+ + +
+ +
+ {originalColors + .filter((color) => !includes(deleted, color.id)) + .map((color) => ( + setDeleted((prev) => [...prev, index])} + /> + ))} + {newColors + .filter((color) => !includes(deleted, color.id)) + .map((color) => ( + setDeleted((prev) => [...prev, index])} + /> + ))} + +
Automated parameters
+ + + +
+ + % +
+
+ + % +
+ +
+ + % +
+
+ + % +
+ +
+ + % +
+
+ + % +
+ +
+ + % +
+
+ + % +
+ +
+ + % +
+
+ + % +
+
Settings
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ ); }