返回 oh-my-ppt
ChartInspector.tsx
1 import { useState } from 'react'
2 import { ChartColumn, Upload } from 'lucide-react'
3 import { useT } from '@renderer/i18n'
4 import { ipc } from '@renderer/lib/ipc'
5 import { useToastStore } from '@renderer/store/toastStore'
6 import { ColorPicker } from '../../ui/ColorPicker'
7 import { Input } from '../../ui/Input'
8 import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/Tooltip'
9 import { InspectorSection } from './InspectorSection'
10 import type { ElementEditDraft, ElementEditorProps } from './types'
11
12 const CHART_FIELDS = [
13 'chartTitle',
14 'chartDataJson',
15 'chartPrimaryColor',
16 'chartAccentColor',
17 'chartTextColor',
18 'chartSmooth',
19 'chartHorizontal',
20 'chartStacked',
21 'chartAreaFill',
22 'chartShowPoints',
23 'chartShowLegend',
24 'chartDoughnutCutout',
25 'chartRadarFill',
26 'chartConfigJson'
27 ] as const
28
29 export function ChartInspector({ draft, onDraftChange }: ElementEditorProps): React.JSX.Element {
30 const t = useT()
31 const toastError = useToastStore((state) => state.error)
32 const [isImportingData, setIsImportingData] = useState(false)
33 const commit = (nextDraft = draft): void => {
34 onDraftChange(nextDraft, { commit: true, fields: [...CHART_FIELDS] })
35 }
36 const handleImportData = async (): Promise<void> => {
37 if (isImportingData) return
38 setIsImportingData(true)
39 try {
40 const result = await ipc.chooseAndParseChartData()
41 if (!result.canceled && result.dataJson) {
42 commit({
43 ...draft,
44 chartDataJson: result.dataJson
45 })
46 }
47 } catch (error) {
48 toastError(error instanceof Error ? error.message : t('sessionDetail.chartImportDataFailed'))
49 } finally {
50 setIsImportingData(false)
51 }
52 }
53 const renderToggleField = (
54 label: string,
55 field:
56 | 'chartSmooth'
57 | 'chartHorizontal'
58 | 'chartStacked'
59 | 'chartAreaFill'
60 | 'chartShowPoints'
61 | 'chartShowLegend'
62 | 'chartRadarFill'
63 ): React.JSX.Element => (
64 <label className="flex items-center justify-between gap-3 rounded-[9px] border border-[#d9cfbd]/72 bg-[#fffaf1]/82 px-2.5 py-2">
65 <span className="text-[11px] font-medium text-[#7a875f]">{label}</span>
66 <input
67 type="checkbox"
68 checked={Boolean(draft[field])}
69 onChange={(event) =>
70 commit({
71 ...draft,
72 [field]: event.target.checked
73 })
74 }
75 className="h-4 w-4 accent-[#7fa56f]"
76 />
77 </label>
78 )
79 const renderColorField = (
80 label: string,
81 field: 'chartPrimaryColor' | 'chartAccentColor' | 'chartTextColor'
82 ): React.JSX.Element => {
83 const fallback =
84 field === 'chartPrimaryColor'
85 ? '#5d6b4d'
86 : field === 'chartAccentColor'
87 ? '#8fbc8f'
88 : '#2f3b28'
89 const updateDraft = (value: string, shouldCommit = false): void => {
90 const nextDraft: ElementEditDraft = { ...draft, [field]: value }
91 if (shouldCommit) commit(nextDraft)
92 else onDraftChange(nextDraft)
93 }
94
95 return (
96 <label className="block space-y-1.5">
97 <span className="text-[11px] font-medium text-[#7a875f]">{label}</span>
98 <div className="flex items-center gap-2">
99 <ColorPicker
100 value={draft[field] || fallback}
101 onChange={(value) => updateDraft(value)}
102 onCommit={(value) => updateDraft(value, true)}
103 />
104 <Input
105 value={draft[field]}
106 onChange={(event) => updateDraft(event.target.value)}
107 onBlur={(event) => updateDraft(event.target.value, true)}
108 className="h-8 rounded-full border border-[#ded2bd]/72 bg-[#fffdf8]/88 px-2.5 text-xs text-[#3f4b35] shadow-[inset_0_1px_2px_rgba(74,59,42,0.05)] focus-visible:border-[#9bb98a] focus-visible:ring-0 focus-visible:ring-offset-0"
109 />
110 </div>
111 </label>
112 )
113 }
114
115 return (
116 <InspectorSection
117 title={t('sessionDetail.chartContent')}
118 icon={<ChartColumn className="h-3.5 w-3.5 text-[#7a875f]" />}
119 >
120 <div className="space-y-2.5">
121 <div className="block space-y-1.5">
122 <span className="text-[11px] font-medium text-[#7a875f]">
123 {t('sessionDetail.chartTitle')}
124 </span>
125 <Input
126 value={draft.chartTitle}
127 onChange={(event) => onDraftChange({ ...draft, chartTitle: event.target.value })}
128 onBlur={(event) => commit({ ...draft, chartTitle: event.target.value })}
129 className="h-8 rounded-full border border-[#ded2bd]/72 bg-[#fffdf8]/88 px-2.5 text-xs text-[#3f4b35] shadow-[inset_0_1px_2px_rgba(74,59,42,0.05)] focus-visible:border-[#9bb98a] focus-visible:ring-0 focus-visible:ring-offset-0"
130 />
131 </div>
132
133 <div className="block space-y-1.5">
134 <span className="flex items-center justify-between gap-2">
135 <span className="text-[11px] font-medium text-[#7a875f]">
136 {t('sessionDetail.chartData')}
137 </span>
138 <Tooltip>
139 <TooltipTrigger asChild>
140 <span className="inline-flex">
141 <button
142 type="button"
143 onClick={handleImportData}
144 disabled={isImportingData}
145 aria-label={t('sessionDetail.chartImportDataTooltip')}
146 title={t('sessionDetail.chartImportDataTooltip')}
147 className="inline-flex h-7 items-center gap-1.5 rounded-full border border-[#d9cfbd]/78 bg-[#fffdf8]/88 px-2.5 text-[11px] font-medium text-[#5d6b4d] transition-colors hover:border-[#9daf8a] hover:text-[#415235] disabled:cursor-not-allowed disabled:opacity-55"
148 >
149 <Upload className="h-3.5 w-3.5" />
150 {t('sessionDetail.chartImportData')}
151 </button>
152 </span>
153 </TooltipTrigger>
154 <TooltipContent side="top" align="end">
155 {t('sessionDetail.chartImportDataTooltip')}
156 </TooltipContent>
157 </Tooltip>
158 </span>
159 <textarea
160 value={draft.chartDataJson}
161 onChange={(event) => onDraftChange({ ...draft, chartDataJson: event.target.value })}
162 onBlur={(event) => commit({ ...draft, chartDataJson: event.target.value })}
163 className="min-h-[138px] w-full resize-y rounded-[9px] border border-[#d9cfbd]/72 bg-[#fffaf1]/92 px-2.5 py-2 font-mono text-[11px] leading-5 text-[#34402c] outline-none transition-colors placeholder:text-[#b1a58f] focus:border-[#9daf8a]"
164 placeholder={t('sessionDetail.chartDataPlaceholder')}
165 />
166 </div>
167
168 <div className="grid grid-cols-1 gap-2.5">
169 {renderColorField(t('sessionDetail.chartPrimaryColor'), 'chartPrimaryColor')}
170 {renderColorField(t('sessionDetail.chartAccentColor'), 'chartAccentColor')}
171 {renderColorField(t('sessionDetail.chartTextColor'), 'chartTextColor')}
172 </div>
173
174 {draft.chartType === 'bar' && (
175 <div className="space-y-2">
176 {renderToggleField(t('sessionDetail.chartHorizontal'), 'chartHorizontal')}
177 {renderToggleField(t('sessionDetail.chartStacked'), 'chartStacked')}
178 </div>
179 )}
180
181 {draft.chartType === 'line' && (
182 <div className="space-y-2">
183 {renderToggleField(t('sessionDetail.chartSmoothLine'), 'chartSmooth')}
184 {renderToggleField(t('sessionDetail.chartAreaFill'), 'chartAreaFill')}
185 {renderToggleField(t('sessionDetail.chartShowPoints'), 'chartShowPoints')}
186 </div>
187 )}
188
189 {(draft.chartType === 'pie' || draft.chartType === 'doughnut') && (
190 <div className="space-y-2">
191 {renderToggleField(t('sessionDetail.chartShowLegend'), 'chartShowLegend')}
192 {draft.chartType === 'doughnut' && (
193 <label className="block space-y-1.5">
194 <span className="text-[11px] font-medium text-[#7a875f]">
195 {t('sessionDetail.chartDoughnutCutout')}
196 </span>
197 <Input
198 type="number"
199 min={0}
200 max={85}
201 step={1}
202 value={draft.chartDoughnutCutout}
203 onChange={(event) =>
204 onDraftChange({ ...draft, chartDoughnutCutout: event.target.value })
205 }
206 onBlur={(event) =>
207 commit({ ...draft, chartDoughnutCutout: event.target.value })
208 }
209 className="h-8 rounded-full border border-[#ded2bd]/72 bg-[#fffdf8]/88 px-2.5 text-xs text-[#3f4b35] shadow-[inset_0_1px_2px_rgba(74,59,42,0.05)] focus-visible:border-[#9bb98a] focus-visible:ring-0 focus-visible:ring-offset-0"
210 />
211 </label>
212 )}
213 </div>
214 )}
215
216 {draft.chartType === 'radar' && (
217 <div className="space-y-2">
218 {renderToggleField(t('sessionDetail.chartRadarFill'), 'chartRadarFill')}
219 {renderToggleField(t('sessionDetail.chartShowPoints'), 'chartShowPoints')}
220 {renderToggleField(t('sessionDetail.chartShowLegend'), 'chartShowLegend')}
221 </div>
222 )}
223 </div>
224 </InspectorSection>
225 )
226 }
227
227 lines Plain Text