From 3a82078bd2126929c80c5c4faae58754a2cf1216 Mon Sep 17 00:00:00 2001 From: wintsa <770775984@qq.com> Date: Fri, 14 Aug 2026 18:12:25 +0800 Subject: [PATCH] 1 --- src/App.tsx | 374 ++++++++++++++++++++++++++++++++++++++----------- src/styles.css | 61 +++++++- 2 files changed, 348 insertions(+), 87 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 39fe608..7c2e66e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,7 +12,7 @@ import { } from 'ag-grid-community'; import type { AgCartesianChartOptions } from 'ag-charts-community'; import { ModuleRegistry } from 'ag-charts-community'; -import { Building2, Construction, LayoutGrid, Library, LocateFixed, MapPinned, PanelRightClose, PanelRightOpen, SquareFunction, Waypoints } from 'lucide-react'; +import { Building2, CalendarRange, Construction, LayoutGrid, Library, LocateFixed, MapPinned, PanelRightClose, PanelRightOpen, Ruler, SquareFunction, Waypoints } from 'lucide-react'; import { AnnotationsModule, ContextMenuModule, @@ -77,6 +77,8 @@ const filterOptions = [ { key: 'buildingFunction', label: '建筑功能', icon: SquareFunction }, { key: 'constructionStage', label: '建设阶段', icon: Construction }, { key: 'planningForm', label: '规划形式', icon: LayoutGrid }, + { key: 'time', label: '时间', icon: CalendarRange }, + { key: 'area', label: '面积', icon: Ruler }, ] as const; const chartFilterOptions = filterOptions.filter((option) => option.key !== 'templateLibrary' && option.key !== 'indicatorTree'); @@ -145,6 +147,17 @@ const defaultTemplateFilterNode = { label: '默认模板', } as const; const overallSummaryKey = 'summary'; +const pivotGridWanValueFields = new Set([ + 'lowValue', + 'centerValue', + 'highValue', + 'maxValue', + 'minValue', + 'avgValue', + 'medianValue', + 'standardDeviation', + 'interquartileRange', +]); // const mockGeoLocationPayload = { // checkStrictly: true, @@ -178,6 +191,7 @@ type StatisticKey = (typeof statisticOptions)[number]['key']; type MetricKey = (typeof metricOptions)[number]['key']; type ContentKey = (typeof contentOptions)[number]['key']; type FilterKey = (typeof filterOptions)[number]['key']; +type RangeFilterKey = Extract; type GroupKey = 'year'; type ChartViewKey = 'trend' | 'pivot'; type ApiBuildingFunctionStat = { @@ -289,20 +303,31 @@ function formatChartValue(value: number, metricKey: MetricKey) { return formatAreaMetricValue(value); } +function formatGridWanValue(value: number) { + const wanValue = value / 10000; + const fractionDigits = Math.abs(wanValue) > 0 && Math.abs(wanValue) < 1 ? 4 : 2; + return formatNumber(wanValue, fractionDigits); +} + function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum { const avgValue = row.avg_value ?? null; const medianValue = row.median_value ?? null; const fallbackThresholdLowValue = avgValue == null || medianValue == null ? null : Math.min(avgValue, medianValue); const fallbackThresholdHighValue = avgValue == null || medianValue == null ? null : Math.max(avgValue, medianValue); + const thresholdLowValue = row.threshold_low_value ?? fallbackThresholdLowValue; + const thresholdHighValue = row.threshold_high_value ?? fallbackThresholdHighValue; + const thresholdCenterValue = thresholdLowValue == null || thresholdHighValue == null + ? null + : (thresholdLowValue + thresholdHighValue) / 2; return { groupName: row.group_name || String(row.group_key ?? '未命名'), minValue: row.min_value ?? null, maxValue: row.max_value ?? null, avgValue, medianValue, - thresholdLowValue: row.threshold_low_value ?? fallbackThresholdLowValue, - thresholdCenterValue: row.threshold_center_value ?? medianValue, - thresholdHighValue: row.threshold_high_value ?? fallbackThresholdHighValue, + thresholdLowValue, + thresholdCenterValue, + thresholdHighValue, standardDeviation: row.stddev_value ?? row.standard_deviation ?? null, interquartileRange: row.iqr_value ?? row.quartile_range ?? null, coefficientOfVariation: row.variation_coefficient ?? row.coefficient_of_variation ?? null, @@ -578,6 +603,10 @@ function isIndicatorTreeFilterKey(filterKey: FilterKey): filterKey is 'indicator return filterKey === 'indicatorTree'; } +function isRangeFilterKey(filterKey: FilterKey): filterKey is RangeFilterKey { + return filterKey === 'time' || filterKey === 'area'; +} + function isSingleSelectFilterKey(filterKey: FilterKey) { return isTemplateFilterKey(filterKey) || isIndicatorTreeFilterKey(filterKey); } @@ -741,6 +770,8 @@ function App() { buildingFunction: false, constructionStage: false, planningForm: false, + time: false, + area: false, }); const [statisticKey, setStatisticKey] = useState('avgValue'); const [metricKey, setMetricKey] = useState('cost'); @@ -789,6 +820,8 @@ function App() { buildingFunction: [], constructionStage: [], planningForm: [], + time: [], + area: [], }); const [filterTreeLoadingByKey, setFilterTreeLoadingByKey] = useState>({ templateLibrary: false, @@ -799,6 +832,8 @@ function App() { buildingFunction: false, constructionStage: false, planningForm: false, + time: false, + area: false, }); const [filterTreeErrorByKey, setFilterTreeErrorByKey] = useState>({ templateLibrary: null, @@ -809,6 +844,8 @@ function App() { buildingFunction: null, constructionStage: null, planningForm: null, + time: null, + area: null, }); const [filterSearchTreeByKey, setFilterSearchTreeByKey] = useState>({ templateLibrary: [], @@ -819,6 +856,8 @@ function App() { buildingFunction: [], constructionStage: [], planningForm: [], + time: [], + area: [], }); const [filterSearchLoadingByKey, setFilterSearchLoadingByKey] = useState>({ templateLibrary: false, @@ -829,6 +868,8 @@ function App() { buildingFunction: false, constructionStage: false, planningForm: false, + time: false, + area: false, }); const [filterSearchErrorByKey, setFilterSearchErrorByKey] = useState>({ templateLibrary: null, @@ -839,6 +880,8 @@ function App() { buildingFunction: null, constructionStage: null, planningForm: null, + time: null, + area: null, }); const [appliedFilters, setAppliedFilters] = useState>({ templateLibrary: getDefaultTemplateFilterNodes(), @@ -849,9 +892,12 @@ function App() { buildingFunction: [], constructionStage: [], planningForm: [], + time: [], + area: [], }); const [filterModalKey, setFilterModalKey] = useState(null); const [draftFilterNodes, setDraftFilterNodes] = useState([]); + const [draftRangeValues, setDraftRangeValues] = useState({ min: '', max: '' }); const [filterSearchValue, setFilterSearchValue] = useState(''); const filterSearchComposingRef = useRef(false); const filterSearchTimerRef = useRef(null); @@ -864,6 +910,8 @@ function App() { buildingFunction: 0, constructionStage: 0, planningForm: 0, + time: 0, + area: 0, }); const lastFilterSearchRef = useRef(''); @@ -887,6 +935,26 @@ function App() { const activeFilterTreeError = filterModalKey ? trimmedFilterSearchValue ? filterSearchErrorByKey[filterModalKey] : filterTreeErrorByKey[filterModalKey] : null; + const draftRangeMin = draftRangeValues.min === '' ? null : Number(draftRangeValues.min); + const draftRangeMax = draftRangeValues.max === '' ? null : Number(draftRangeValues.max); + const draftRangeError = filterModalKey && isRangeFilterKey(filterModalKey) + ? (draftRangeMin != null && !Number.isFinite(draftRangeMin)) + || (draftRangeMax != null && !Number.isFinite(draftRangeMax)) + ? '请输入有效数字' + : draftRangeMin != null && draftRangeMax != null && draftRangeMin > draftRangeMax + ? '起始值不能大于截止值' + : filterModalKey === 'time' && ( + (draftRangeMin != null && (!Number.isInteger(draftRangeMin) || draftRangeMin < 1900 || draftRangeMin > 2100)) + || (draftRangeMax != null && (!Number.isInteger(draftRangeMax) || draftRangeMax < 1900 || draftRangeMax > 2100)) + ) + ? '年份请输入1900至2100之间的整数' + : filterModalKey === 'area' && ( + (draftRangeMin != null && draftRangeMin < 0) + || (draftRangeMax != null && draftRangeMax < 0) + ) + ? '面积不能小于0' + : '' + : ''; const selectedTemplateId = appliedFilters.templateLibrary[0]?.id || defaultTemplateFilterNode.id; const defaultIndicatorTreeNodes = useMemo( () => getDefaultIndicatorTreeFilterNodes(filterTreeByKey.indicatorTree), @@ -907,11 +975,31 @@ function App() { const selectedValueKey = metricKey === 'dataCount' ? 'dataCount' : statisticKey; const requestMetricKey = metricKey === 'dataCount' ? 'cost' : metricKey; const seriesValueLabel = metricKey === 'dataCount' ? selectedMetric.label : selectedStatistic.label; + const appliedTimeRange = useMemo(() => { + const [minText = '', maxText = ''] = (appliedFilters.time[0]?.id ?? ':').split(':'); + return { + min: minText === '' ? null : Number(minText), + max: maxText === '' ? null : Number(maxText), + }; + }, [appliedFilters.time]); + const hasTimeFilter = appliedFilters.time.length > 0; + const visibleChartDataBySelection = useMemo(() => { + if (!hasTimeFilter) return chartDataBySelection; + return Object.fromEntries(Object.entries(chartDataBySelection).map(([key, rows]) => [ + key, + rows.filter((datum) => { + const year = Number(datum.groupName); + return Number.isFinite(year) + && (appliedTimeRange.min == null || year >= appliedTimeRange.min) + && (appliedTimeRange.max == null || year <= appliedTimeRange.max); + }), + ])); + }, [appliedTimeRange.max, appliedTimeRange.min, chartDataBySelection, hasTimeFilter]); const groupNames = useMemo(() => { const names: string[] = []; const seen = new Set(); selectedContentNodes.forEach((node) => { - const rows = chartDataBySelection[getSelectionKey(node.contentKey, node.id)] ?? []; + const rows = visibleChartDataBySelection[getSelectionKey(node.contentKey, node.id)] ?? []; rows.forEach((datum) => { if (seen.has(datum.groupName)) return; seen.add(datum.groupName); @@ -919,10 +1007,10 @@ function App() { }); }); return names.sort(compareGroupNames); - }, [chartDataBySelection, selectedContentNodes]); + }, [selectedContentNodes, visibleChartDataBySelection]); const pivotGridRowData = useMemo( () => selectedContentNodes.flatMap((node) => { - const rows = chartDataBySelection[getSelectionKey(node.contentKey, node.id)] ?? []; + const rows = visibleChartDataBySelection[getSelectionKey(node.contentKey, node.id)] ?? []; return rows.map((datum) => ({ year: datum.groupName, name: node.label, @@ -940,10 +1028,11 @@ function App() { dataCount: datum.dataCount, })); }), - [chartDataBySelection, selectedContentNodes], + [selectedContentNodes, visibleChartDataBySelection], ); const pivotGridPinnedBottomRowData = useMemo( () => { + if (hasTimeFilter) return []; const datum = chartSummaryBySelection[overallSummaryKey] ?? selectedContentNodes .map((node) => chartSummaryBySelection[getSelectionKey(node.contentKey, node.id)]) @@ -966,13 +1055,14 @@ function App() { dataCount: datum.dataCount, }]; }, - [chartSummaryBySelection, selectedContentNodes], + [chartSummaryBySelection, hasTimeFilter, selectedContentNodes], ); - const pivotGridSampleCount = pivotGridPinnedBottomRowData[0]?.dataCount ?? 0; const pivotGridColumnDefs = useMemo<(ColDef | ColGroupDef)[]>( () => { + const valueUnit = requestMetricKey === 'cost' ? '万元' : '万元/m²'; + const valueColumnMinWidth = requestMetricKey === 'cost' ? 112 : 142; const valueFormatter = ({ value }: ValueFormatterParams) => ( - value == null ? '' : formatChartValue(Number(value), requestMetricKey) + value == null ? '' : formatGridWanValue(Number(value)) ); return [ @@ -994,75 +1084,75 @@ function App() { children: [ { field: 'lowValue', - headerName: '低值', + headerName: `低值(${valueUnit})`, type: 'numericColumn', - minWidth: 64, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'centerValue', - headerName: '中心值', + headerName: `中心值(${valueUnit})`, type: 'numericColumn', - minWidth: 68, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'highValue', - headerName: '高值', + headerName: `高值(${valueUnit})`, type: 'numericColumn', - minWidth: 64, + minWidth: valueColumnMinWidth, valueFormatter, }, ], }, { - headerName: `样本统计值(${formatNumber(pivotGridSampleCount, 0)})`, + headerName: '样本统计值', children: [ { field: 'maxValue', - headerName: '最大值', + headerName: `最大值(${valueUnit})`, type: 'numericColumn', - minWidth: 68, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'minValue', - headerName: '最小值', + headerName: `最小值(${valueUnit})`, type: 'numericColumn', - minWidth: 68, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'avgValue', - headerName: '平均值', + headerName: `平均值(${valueUnit})`, type: 'numericColumn', - minWidth: 68, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'medianValue', - headerName: '中位数', + headerName: `中位数(${valueUnit})`, type: 'numericColumn', - minWidth: 68, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'standardDeviation', - headerName: '标准差', + headerName: `标准差(${valueUnit})`, type: 'numericColumn', - minWidth: 68, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'interquartileRange', - headerName: '四分位距', + headerName: `四分位距(${valueUnit})`, type: 'numericColumn', - minWidth: 76, + minWidth: valueColumnMinWidth, valueFormatter, }, { field: 'coefficientOfVariation', - headerName: '变异系数', + headerName: '变异系数(无量纲)', type: 'numericColumn', minWidth: 76, valueFormatter: ({ value }: ValueFormatterParams) => ( @@ -1071,9 +1161,18 @@ function App() { }, ], }, + { + field: 'dataCount', + headerName: '样本数量(个)', + type: 'numericColumn', + minWidth: 104, + valueFormatter: ({ value }: ValueFormatterParams) => ( + value == null ? '' : formatNumber(Number(value), 0) + ), + }, ]; }, - [pivotGridSampleCount, requestMetricKey], + [requestMetricKey], ); const fitPivotGridColumns = useCallback(() => { window.requestAnimationFrame(() => { @@ -1087,7 +1186,15 @@ function App() { }); }, []); const exportPivotGrid = useCallback(() => { - pivotGridApiRef.current?.exportDataAsCsv({ fileName: '建筑设施对象统计表.csv' }); + pivotGridApiRef.current?.exportDataAsCsv({ + fileName: '建筑设施对象统计表.csv', + processCellCallback: ({ column, value }) => { + if (value == null) return ''; + return pivotGridWanValueFields.has(column.getColId()) + ? String(Number(value) / 10000) + : String(value); + }, + }); setGridContextMenuPosition(null); }, []); @@ -1127,12 +1234,23 @@ function App() { }, [defaultIndicatorTreeNodes]); const appliedFilterPayload = useMemo( () => filterOptions + .filter((option) => option.key !== 'time') .map((option) => ({ key: option.key, nodes: appliedFilters[option.key].map((node) => ({ nodeId: node.id })), })) .filter((filter) => filter.nodes.length > 0), - [appliedFilters], + [ + appliedFilters.area, + appliedFilters.buildingFunction, + appliedFilters.constructionStage, + appliedFilters.facilityType, + appliedFilters.geoLocation, + appliedFilters.indicatorTree, + appliedFilters.planningForm, + appliedFilters.region, + appliedFilters.templateLibrary, + ], ); const getNodeColor = (contentKey: ContentKey, nodeId: string) => { @@ -1418,6 +1536,7 @@ function App() { }; const ensureFilterTreeLoaded = (filterKey: FilterKey) => { + if (isRangeFilterKey(filterKey)) return; if (filterTreeByKey[filterKey].length > 0 || filterTreeInitialLoadStartedRef.current[filterKey]) return; filterTreeInitialLoadStartedRef.current[filterKey] = true; @@ -1445,6 +1564,10 @@ function App() { const openFilterModal = (filterKey: FilterKey) => { setFilterModalKey(filterKey); + if (isRangeFilterKey(filterKey)) { + const [min = '', max = ''] = (appliedFilters[filterKey][0]?.id ?? ':').split(':'); + setDraftRangeValues({ min, max }); + } if (isIndicatorTreeFilterKey(filterKey) && appliedFilters[filterKey].length === 0 && defaultIndicatorTreeNodes.length > 0) { setDraftFilterNodes(defaultIndicatorTreeNodes); } else { @@ -1456,7 +1579,9 @@ function App() { window.clearTimeout(filterSearchTimerRef.current); filterSearchTimerRef.current = null; } - ensureFilterTreeLoaded(filterKey); + if (!isRangeFilterKey(filterKey)) { + ensureFilterTreeLoaded(filterKey); + } }; const closeFilterModal = () => { @@ -1470,6 +1595,7 @@ function App() { lastFilterSearchRef.current = ''; setFilterModalKey(null); setDraftFilterNodes([]); + setDraftRangeValues({ min: '', max: '' }); setFilterSearchValue(''); }; @@ -1605,6 +1731,40 @@ function App() { const applyFilterModal = () => { if (!filterModalKey) return; + if (isRangeFilterKey(filterModalKey)) { + if (draftRangeError) return; + const min = draftRangeValues.min.trim(); + const max = draftRangeValues.max.trim(); + const unit = filterModalKey === 'time' ? '年' : 'm²'; + const label = min && max + ? `${min} - ${max}${unit}` + : min + ? `${min}${unit}及以上` + : `${max}${unit}及以下`; + const nextRangeNodes: SelectedFilterNode[] = min || max ? [{ + id: `${min}:${max}`, + filterKey: filterModalKey, + label, + }] : []; + setAppliedFilters((current) => ({ + ...current, + [filterModalKey]: nextRangeNodes, + })); + if (filterModalKey === 'time') { + closeFilterModal(); + return; + } + setChartDataBySelection({}); + setChartSummaryBySelection({}); + setLoadError(null); + if (selectedContentNodes.length > 0) { + setLoadingHint('正在按筛选条件重新计算'); + setLoading(true); + } + setChartQueryVersion((version) => version + 1); + closeFilterModal(); + return; + } let nextDraftNodes = draftFilterNodes; if (isTemplateFilterKey(filterModalKey) && nextDraftNodes.length === 0) { nextDraftNodes = getDefaultTemplateFilterNodes(); @@ -1657,6 +1817,9 @@ function App() { } return nextFilters; }); + if (filterKey === 'time') { + return; + } if (isTemplateFilterKey(filterKey)) { resetIndicatorTreeState(); } @@ -2007,7 +2170,7 @@ function App() { const visibleData = groupNames.map((groupName) => { const row: Record = { groupName }; selectedContentNodes.forEach((node) => { - const datum = chartDataBySelection[getSelectionKey(node.contentKey, node.id)]?.find((item) => item.groupName === groupName); + const datum = visibleChartDataBySelection[getSelectionKey(node.contentKey, node.id)]?.find((item) => item.groupName === groupName); row[getSeriesValueKey(node.contentKey, node.id)] = datum?.[selectedValueKey] ?? null; }); return row; @@ -2211,7 +2374,6 @@ function App() { }; }, [ activeFilterCount, - chartDataBySelection, chartEmptyText, groupNames, metricKey, @@ -2223,6 +2385,7 @@ function App() { seriesValueLabel, selectedValueKey, statisticKey, + visibleChartDataBySelection, ]); const renderMetricSwitcher = (variant: 'chart' | 'grid') => ( @@ -2311,6 +2474,8 @@ function App() { buildingFunction: [], constructionStage: [], planningForm: [], + time: [], + area: [], }); setChartDataBySelection({}); setChartSummaryBySelection({}); @@ -2495,7 +2660,7 @@ function App() { {filterModalKey && activeFilter ? (
{activeFilter.label} -
- { - const nextValue = event.target.value; - setFilterSearchValue(nextValue); - if (!filterSearchComposingRef.current && filterModalKey) { - scheduleFilterSearch(filterModalKey, nextValue); - } - }} - onCompositionStart={() => { - filterSearchComposingRef.current = true; - }} - onCompositionEnd={(event) => { - filterSearchComposingRef.current = false; - const nextValue = event.currentTarget.value; - setFilterSearchValue(nextValue); - if (filterModalKey) { - scheduleFilterSearch(filterModalKey, nextValue); - } - }} - /> -
-
- {draftFilterNodes.length > 0 ? `已选 ${draftFilterNodes.length} 项` : '未选择'} -
-
- {activeFilterTreeLoading ? ( -
加载中
- ) : activeFilterTreeError ? ( -
{activeFilterTreeError}
- ) : activeFilterDisplayTree.length > 0 ? ( - renderFilterTreeNodes(activeFilterDisplayTree, filterModalKey, draftFilterNodeKeys, toggleFilterTreeNode, toggleDraftFilterNode) - ) : ( -
{trimmedFilterSearchValue ? '无匹配结果' : '暂无数据'}
- )} -
+ {isRangeFilterKey(filterModalKey) ? ( +
+ + + +
{draftRangeError}
+
+ ) : ( + <> +
+ { + const nextValue = event.target.value; + setFilterSearchValue(nextValue); + if (!filterSearchComposingRef.current && filterModalKey) { + scheduleFilterSearch(filterModalKey, nextValue); + } + }} + onCompositionStart={() => { + filterSearchComposingRef.current = true; + }} + onCompositionEnd={(event) => { + filterSearchComposingRef.current = false; + const nextValue = event.currentTarget.value; + setFilterSearchValue(nextValue); + if (filterModalKey) { + scheduleFilterSearch(filterModalKey, nextValue); + } + }} + /> +
+
+ {draftFilterNodes.length > 0 ? `已选 ${draftFilterNodes.length} 项` : '未选择'} +
+
+ {activeFilterTreeLoading ? ( +
加载中
+ ) : activeFilterTreeError ? ( +
{activeFilterTreeError}
+ ) : activeFilterDisplayTree.length > 0 ? ( + renderFilterTreeNodes(activeFilterDisplayTree, filterModalKey, draftFilterNodeKeys, toggleFilterTreeNode, toggleDraftFilterNode) + ) : ( +
{trimmedFilterSearchValue ? '无匹配结果' : '暂无数据'}
+ )} +
+ + )} diff --git a/src/styles.css b/src/styles.css index 4a84cc7..71cb56a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -171,10 +171,11 @@ button { .chart-filter-bar { display: flex; + flex-wrap: wrap; align-items: center; gap: 8px; min-width: 0; - overflow: hidden; + overflow: visible; justify-content: center; } @@ -412,7 +413,7 @@ button { font-size: 15px; font-weight: 600; line-height: 20px; - overflow: hidden; + overflow: visible; pointer-events: none; text-overflow: ellipsis; white-space: nowrap; @@ -1041,6 +1042,57 @@ button { overflow: hidden; } +.filter-modal--range { + grid-template-rows: auto auto auto; + height: auto; + min-height: 0; +} + +.filter-range-fields { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: end; + gap: 12px; + padding: 22px 18px 26px; +} + +.filter-range-fields label { + display: grid; + gap: 7px; + color: #5d554d; + font-size: 13px; +} + +.filter-range-fields input { + width: 100%; + height: 36px; + padding: 0 10px; + border: 1px solid rgba(90, 82, 72, 0.2); + border-radius: 3px; + color: #262a33; + background: #fffdfa; + font-size: 14px; + outline: none; +} + +.filter-range-fields input:focus { + border-color: rgba(0, 120, 168, 0.46); + box-shadow: 0 0 0 2px rgba(0, 120, 168, 0.12); +} + +.filter-range-separator { + padding-bottom: 8px; + color: #776e65; + font-size: 13px; +} + +.filter-range-error { + grid-column: 1 / -1; + min-height: 18px; + color: #a13f2c; + font-size: 12px; +} + .filter-modal-header { display: flex; align-items: center; @@ -1249,3 +1301,8 @@ button { flex-wrap: wrap; } } + +.filter-modal-confirm:disabled { + cursor: not-allowed; + opacity: 0.45; +}