数据
This commit is contained in:
13
src/App.tsx
13
src/App.tsx
@@ -703,7 +703,7 @@ export default function App() {
|
|||||||
if (previewSnapshot !== null && appliedExportData !== null) {
|
if (previewSnapshot !== null && appliedExportData !== null) {
|
||||||
setPreviewLayoutStale(true);
|
setPreviewLayoutStale(true);
|
||||||
}
|
}
|
||||||
}, [workingTemplate.elements, workingTemplate.width, workingTemplate.height]);
|
}, [workingTemplate.elements, workingTemplate.width, workingTemplate.height, workingTemplate.dataFilterRules]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workflowStep !== 'print-view') return;
|
if (workflowStep !== 'print-view') return;
|
||||||
@@ -734,6 +734,7 @@ export default function App() {
|
|||||||
appliedMappedRows,
|
appliedMappedRows,
|
||||||
workingTemplate.width,
|
workingTemplate.width,
|
||||||
workingTemplate.height,
|
workingTemplate.height,
|
||||||
|
workingTemplate.dataFilterRules,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const pdfExportImage = useMemo(() => {
|
const pdfExportImage = useMemo(() => {
|
||||||
@@ -770,6 +771,14 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDataFilterRulesChange = (rules: VisibilityRule[]) => {
|
||||||
|
if (!activeTemplate) return;
|
||||||
|
handleTemplateChange({
|
||||||
|
...activeTemplate,
|
||||||
|
dataFilterRules: rules.length > 0 ? rules : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderAppliedLabelToDataUrl = useCallback(
|
const renderAppliedLabelToDataUrl = useCallback(
|
||||||
(label: PrintableLabel) =>
|
(label: PrintableLabel) =>
|
||||||
renderLabelToDataUrl({
|
renderLabelToDataUrl({
|
||||||
@@ -1440,6 +1449,7 @@ export default function App() {
|
|||||||
rows={mappedRows}
|
rows={mappedRows}
|
||||||
draftExportRange={draftExportRange}
|
draftExportRange={draftExportRange}
|
||||||
onChangeDraftExportRange={setDraftExportRange}
|
onChangeDraftExportRange={setDraftExportRange}
|
||||||
|
onChangeDataFilterRules={handleDataFilterRulesChange}
|
||||||
cutLine={cutLine}
|
cutLine={cutLine}
|
||||||
onChangeCutLine={handleCutLineChange}
|
onChangeCutLine={handleCutLineChange}
|
||||||
printablePages={previewSnapshot?.pages ?? []}
|
printablePages={previewSnapshot?.pages ?? []}
|
||||||
@@ -1561,6 +1571,7 @@ export default function App() {
|
|||||||
rows={mappedRows}
|
rows={mappedRows}
|
||||||
draftExportRange={draftExportRange}
|
draftExportRange={draftExportRange}
|
||||||
onChangeDraftExportRange={setDraftExportRange}
|
onChangeDraftExportRange={setDraftExportRange}
|
||||||
|
onChangeDataFilterRules={handleDataFilterRulesChange}
|
||||||
cutLine={cutLine}
|
cutLine={cutLine}
|
||||||
onChangeCutLine={handleCutLineChange}
|
onChangeCutLine={handleCutLineChange}
|
||||||
previewReady={previewSnapshot !== null}
|
previewReady={previewSnapshot !== null}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ExportRangeConfig, LabelTemplate, PaperConfig, RecordRow } from '../types';
|
import { ExportRangeConfig, LabelTemplate, PaperConfig, RecordRow, VisibilityRule } from '../types';
|
||||||
import { buildPrintablePages, extractTemplateVariables } from '../utils';
|
import {
|
||||||
|
buildPrintablePages,
|
||||||
|
extractTemplateVariables,
|
||||||
|
getDataFilteredIndices,
|
||||||
|
} from '../utils';
|
||||||
import { PageInput } from './PreviewGrid';
|
import { PageInput } from './PreviewGrid';
|
||||||
import { FieldSelect } from './PsSelect';
|
import { FieldSelect } from './PsSelect';
|
||||||
import { VariableFilterRulesEditor } from './VariableFilterRulesEditor';
|
import { VariableFilterRulesEditor } from './VariableFilterRulesEditor';
|
||||||
@@ -9,82 +13,104 @@ interface DataRangeFieldsProps {
|
|||||||
variant?: 'ps' | 'mobile';
|
variant?: 'ps' | 'mobile';
|
||||||
exportRange: ExportRangeConfig;
|
exportRange: ExportRangeConfig;
|
||||||
onChangeExportRange: (range: ExportRangeConfig) => void;
|
onChangeExportRange: (range: ExportRangeConfig) => void;
|
||||||
|
onChangeDataFilterRules: (rules: VisibilityRule[]) => void;
|
||||||
rows: RecordRow[];
|
rows: RecordRow[];
|
||||||
paper: PaperConfig;
|
paper: PaperConfig;
|
||||||
template: LabelTemplate;
|
template: LabelTemplate;
|
||||||
disabled?: boolean;
|
/** 排版过滤在无数据时不可用;数据过滤始终可编辑 */
|
||||||
|
layoutDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataRangeFields: React.FC<DataRangeFieldsProps> = ({
|
export const DataRangeFields: React.FC<DataRangeFieldsProps> = ({
|
||||||
variant = 'ps',
|
variant = 'ps',
|
||||||
exportRange,
|
exportRange,
|
||||||
onChangeExportRange,
|
onChangeExportRange,
|
||||||
|
onChangeDataFilterRules,
|
||||||
rows,
|
rows,
|
||||||
paper,
|
paper,
|
||||||
template,
|
template,
|
||||||
disabled = false,
|
layoutDisabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = variant === 'mobile';
|
const isMobile = variant === 'mobile';
|
||||||
const variableDefaults = template.variableDefaults ?? null;
|
const variableDefaults = template.variableDefaults ?? null;
|
||||||
const templateVariables = useMemo(() => extractTemplateVariables(template), [template]);
|
const templateVariables = useMemo(() => extractTemplateVariables(template), [template]);
|
||||||
|
const dataFilteredCount = useMemo(
|
||||||
|
() => getDataFilteredIndices(rows, template, variableDefaults).length,
|
||||||
|
[rows, template, variableDefaults]
|
||||||
|
);
|
||||||
const rangeStats = buildPrintablePages(rows, paper, template, exportRange, variableDefaults);
|
const rangeStats = buildPrintablePages(rows, paper, template, exportRange, variableDefaults);
|
||||||
|
|
||||||
const patchRange = (patch: Partial<ExportRangeConfig>) => {
|
const patchRange = (patch: Partial<ExportRangeConfig>) => {
|
||||||
onChangeExportRange({ ...exportRange, ...patch });
|
onChangeExportRange({ ...exportRange, ...patch });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sectionTitleClass = isMobile ? 'mobile-field-label mb-1 block' : 'ps-section-title';
|
||||||
|
const hintClass = isMobile ? 'text-[10px] text-slate-500' : 'text-[10px] text-[#888]';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={isMobile ? 'space-y-4' : 'space-y-2'}>
|
<div className={isMobile ? 'space-y-4' : 'space-y-3'}>
|
||||||
{!isMobile && <div className="ps-section-title">数据范围</div>}
|
<div className="space-y-2">
|
||||||
<div>
|
{rows.length > 0 && (
|
||||||
{isMobile && <label className="mobile-field-label">范围</label>}
|
<div className="space-y-2">
|
||||||
<FieldSelect
|
<div className={sectionTitleClass}>排版过滤</div>
|
||||||
value={exportRange.mode}
|
<p className={hintClass}>仅影响当前导出会话</p>
|
||||||
disabled={disabled}
|
<div>
|
||||||
onChange={(e) => patchRange({ mode: e.target.value as ExportRangeConfig['mode'] })}
|
{isMobile && <label className="mobile-field-label">范围</label>}
|
||||||
className={isMobile ? 'mobile-field' : 'ps-field text-[11px]'}
|
<FieldSelect
|
||||||
>
|
value={exportRange.mode}
|
||||||
<option value="all">全部数据</option>
|
disabled={layoutDisabled}
|
||||||
<option value="odd">仅限奇数序号的行</option>
|
onChange={(e) => patchRange({ mode: e.target.value as ExportRangeConfig['mode'] })}
|
||||||
<option value="even">仅限偶数序号的行</option>
|
className={isMobile ? 'mobile-field' : 'ps-field text-[11px]'}
|
||||||
<option value="custom">自定义行序号</option>
|
>
|
||||||
<option value="conditional">按条件过滤</option>
|
<option value="all">全部数据</option>
|
||||||
</FieldSelect>
|
<option value="odd">仅限奇数序号的行</option>
|
||||||
</div>
|
<option value="even">仅限偶数序号的行</option>
|
||||||
{exportRange.mode === 'custom' && (
|
<option value="custom">自定义行序号</option>
|
||||||
<div>
|
</FieldSelect>
|
||||||
{isMobile ? (
|
</div>
|
||||||
<label className="mobile-field-label">自定义序号</label>
|
{exportRange.mode === 'custom' && (
|
||||||
) : (
|
<div>
|
||||||
<label className="ps-field-label">自定义序号</label>
|
{isMobile ? (
|
||||||
)}
|
<label className="mobile-field-label">自定义序号</label>
|
||||||
<PageInput
|
) : (
|
||||||
type="text"
|
<label className="ps-field-label">自定义序号</label>
|
||||||
value={exportRange.custom ?? ''}
|
)}
|
||||||
disabled={disabled}
|
<PageInput
|
||||||
onCommit={(val) => patchRange({ custom: val })}
|
type="text"
|
||||||
placeholder="如 1,3,5-10"
|
value={exportRange.custom ?? ''}
|
||||||
inputClassName={isMobile ? 'mobile-field font-mono' : 'ps-field font-mono text-[11px]'}
|
disabled={layoutDisabled}
|
||||||
|
onCommit={(val) => patchRange({ custom: val })}
|
||||||
|
placeholder="如 1,3,5-10"
|
||||||
|
inputClassName={isMobile ? 'mobile-field font-mono' : 'ps-field font-mono text-[11px]'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className={hintClass}>
|
||||||
|
最终范围:{rangeStats.selectedCount} / {rows.length} 行
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className={sectionTitleClass}>数据过滤</div>
|
||||||
|
<p className={hintClass}>条件写入模板;全部满足的数据行才参与排版与导出。</p>
|
||||||
|
<VariableFilterRulesEditor
|
||||||
|
rules={template.dataFilterRules ?? []}
|
||||||
|
templateVariables={templateVariables}
|
||||||
|
emptyHint="尚未添加过滤条件,将使用全部数据行"
|
||||||
|
description="按数据行与变量默认值判断;上传数据后可预览过滤结果。"
|
||||||
|
addTitle="添加数据过滤条件"
|
||||||
|
editTitle="编辑数据过滤条件"
|
||||||
|
onChangeRules={onChangeDataFilterRules}
|
||||||
/>
|
/>
|
||||||
|
{rows.length > 0 && (
|
||||||
|
<p className={hintClass}>
|
||||||
|
数据过滤后:{dataFilteredCount} / {rows.length} 行
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{exportRange.mode === 'conditional' && (
|
|
||||||
<VariableFilterRulesEditor
|
|
||||||
rules={exportRange.filterRules ?? []}
|
|
||||||
templateVariables={templateVariables}
|
|
||||||
disabled={disabled}
|
|
||||||
emptyHint="尚未添加过滤条件,将使用全部数据行"
|
|
||||||
description="全部条件满足的数据行才纳入排版与导出;按数据行与变量默认值判断。"
|
|
||||||
addTitle="添加数据范围条件"
|
|
||||||
editTitle="编辑数据范围条件"
|
|
||||||
onChangeRules={(filterRules) => patchRange({ filterRules })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{rows.length > 0 && (
|
|
||||||
<p className={isMobile ? 'text-[10px] text-slate-500' : 'text-[10px] text-[#888]'}>
|
|
||||||
当前范围:{rangeStats.selectedCount} / {rows.length} 行
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface ExportSidebarProps {
|
|||||||
rows: RecordRow[];
|
rows: RecordRow[];
|
||||||
draftExportRange: ExportRangeConfig;
|
draftExportRange: ExportRangeConfig;
|
||||||
onChangeDraftExportRange: (range: ExportRangeConfig) => void;
|
onChangeDraftExportRange: (range: ExportRangeConfig) => void;
|
||||||
|
onChangeDataFilterRules: (rules: VisibilityRule[]) => void;
|
||||||
cutLine: CutLineConfig;
|
cutLine: CutLineConfig;
|
||||||
onChangeCutLine: (cutLine: CutLineConfig) => void;
|
onChangeCutLine: (cutLine: CutLineConfig) => void;
|
||||||
previewReady: boolean;
|
previewReady: boolean;
|
||||||
@@ -64,6 +65,7 @@ export const ExportSidebar: React.FC<ExportSidebarProps> = ({
|
|||||||
rows,
|
rows,
|
||||||
draftExportRange,
|
draftExportRange,
|
||||||
onChangeDraftExportRange,
|
onChangeDraftExportRange,
|
||||||
|
onChangeDataFilterRules,
|
||||||
cutLine,
|
cutLine,
|
||||||
onChangeCutLine,
|
onChangeCutLine,
|
||||||
previewReady,
|
previewReady,
|
||||||
@@ -102,10 +104,11 @@ export const ExportSidebar: React.FC<ExportSidebarProps> = ({
|
|||||||
variant={compact ? 'ps' : 'ps'}
|
variant={compact ? 'ps' : 'ps'}
|
||||||
exportRange={draftExportRange}
|
exportRange={draftExportRange}
|
||||||
onChangeExportRange={onChangeDraftExportRange}
|
onChangeExportRange={onChangeDraftExportRange}
|
||||||
|
onChangeDataFilterRules={onChangeDataFilterRules}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
paper={paper}
|
paper={paper}
|
||||||
template={template}
|
template={template}
|
||||||
disabled={rows.length === 0}
|
layoutDisabled={rows.length === 0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -155,6 +158,7 @@ export const ExportSidebar: React.FC<ExportSidebarProps> = ({
|
|||||||
templateVariables={templateVariables}
|
templateVariables={templateVariables}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{dataRangeSection}
|
||||||
{hasData && (
|
{hasData && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
@@ -167,7 +171,6 @@ export const ExportSidebar: React.FC<ExportSidebarProps> = ({
|
|||||||
variableDefaults={template.variableDefaults}
|
variableDefaults={template.variableDefaults}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{dataRangeSection}
|
|
||||||
{applyDataSection}
|
{applyDataSection}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -194,6 +197,7 @@ export const ExportSidebar: React.FC<ExportSidebarProps> = ({
|
|||||||
templateName={templateName}
|
templateName={templateName}
|
||||||
templateVariables={templateVariables}
|
templateVariables={templateVariables}
|
||||||
/>
|
/>
|
||||||
|
{dataRangeSection}
|
||||||
{hasData && (
|
{hasData && (
|
||||||
<>
|
<>
|
||||||
<VariableMappingPanel
|
<VariableMappingPanel
|
||||||
@@ -204,7 +208,6 @@ export const ExportSidebar: React.FC<ExportSidebarProps> = ({
|
|||||||
onChangeMapping={onChangeMapping}
|
onChangeMapping={onChangeMapping}
|
||||||
variableDefaults={template.variableDefaults}
|
variableDefaults={template.variableDefaults}
|
||||||
/>
|
/>
|
||||||
{dataRangeSection}
|
|
||||||
{applyDataSection}
|
{applyDataSection}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ interface MobileExportViewProps {
|
|||||||
rows: RecordRow[];
|
rows: RecordRow[];
|
||||||
draftExportRange: ExportRangeConfig;
|
draftExportRange: ExportRangeConfig;
|
||||||
onChangeDraftExportRange: (range: ExportRangeConfig) => void;
|
onChangeDraftExportRange: (range: ExportRangeConfig) => void;
|
||||||
|
onChangeDataFilterRules: (rules: VisibilityRule[]) => void;
|
||||||
cutLine: CutLineConfig;
|
cutLine: CutLineConfig;
|
||||||
onChangeCutLine: (cutLine: CutLineConfig) => void;
|
onChangeCutLine: (cutLine: CutLineConfig) => void;
|
||||||
printablePages: (PrintableLabel | null)[][];
|
printablePages: (PrintableLabel | null)[][];
|
||||||
@@ -97,6 +98,7 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
rows,
|
rows,
|
||||||
draftExportRange,
|
draftExportRange,
|
||||||
onChangeDraftExportRange,
|
onChangeDraftExportRange,
|
||||||
|
onChangeDataFilterRules,
|
||||||
cutLine,
|
cutLine,
|
||||||
onChangeCutLine,
|
onChangeCutLine,
|
||||||
printablePages,
|
printablePages,
|
||||||
@@ -247,28 +249,27 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
templateVariables={templateVariables}
|
templateVariables={templateVariables}
|
||||||
/>
|
/>
|
||||||
{hasData && (
|
{hasData && (
|
||||||
<>
|
<div>
|
||||||
<div>
|
<p className="text-[11px] font-semibold text-[#e8e8e8] mb-2">关联变量</p>
|
||||||
<p className="text-[11px] font-semibold text-[#e8e8e8] mb-2">关联变量</p>
|
<VariableMappingPanel
|
||||||
<VariableMappingPanel
|
|
||||||
variant="ps"
|
|
||||||
variables={templateVariables}
|
|
||||||
columns={dataColumns}
|
|
||||||
mapping={variableColumnMapping}
|
|
||||||
onChangeMapping={onChangeMapping}
|
|
||||||
variableDefaults={template.variableDefaults}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DataRangeFields
|
|
||||||
variant="ps"
|
variant="ps"
|
||||||
exportRange={draftExportRange}
|
variables={templateVariables}
|
||||||
onChangeExportRange={onChangeDraftExportRange}
|
columns={dataColumns}
|
||||||
rows={rows}
|
mapping={variableColumnMapping}
|
||||||
paper={paper}
|
onChangeMapping={onChangeMapping}
|
||||||
template={template}
|
variableDefaults={template.variableDefaults}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<DataRangeFields
|
||||||
|
variant="ps"
|
||||||
|
exportRange={draftExportRange}
|
||||||
|
onChangeExportRange={onChangeDraftExportRange}
|
||||||
|
onChangeDataFilterRules={onChangeDataFilterRules}
|
||||||
|
rows={rows}
|
||||||
|
paper={paper}
|
||||||
|
template={template}
|
||||||
|
/>
|
||||||
{/* {exportBlockedReason && !pdfGenerating && (
|
{/* {exportBlockedReason && !pdfGenerating && (
|
||||||
<p className="text-[10px] text-amber-500/90 text-center">{exportBlockedReason}</p>
|
<p className="text-[10px] text-amber-500/90 text-center">{exportBlockedReason}</p>
|
||||||
)} */}
|
)} */}
|
||||||
|
|||||||
@@ -222,14 +222,12 @@ export interface CutLineConfig {
|
|||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExportRangeMode = 'all' | 'odd' | 'even' | 'custom' | 'conditional';
|
export type ExportRangeMode = 'all' | 'odd' | 'even' | 'custom';
|
||||||
|
|
||||||
export interface ExportRangeConfig {
|
export interface ExportRangeConfig {
|
||||||
mode: ExportRangeMode;
|
mode: ExportRangeMode;
|
||||||
/** 自定义范围:1-based 序号,如 "1,3,5-10" */
|
/** 自定义范围:1-based 序号,如 "1,3,5-10"(排版过滤,不写入模板) */
|
||||||
custom?: string;
|
custom?: string;
|
||||||
/** mode=conditional 时,全部满足才纳入排版/导出范围 */
|
|
||||||
filterRules?: VisibilityRule[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LabelTemplate {
|
export interface LabelTemplate {
|
||||||
@@ -242,6 +240,8 @@ export interface LabelTemplate {
|
|||||||
variableList?: string[];
|
variableList?: string[];
|
||||||
/** 设计预览用:变量名 → 默认展示值 */
|
/** 设计预览用:变量名 → 默认展示值 */
|
||||||
variableDefaults?: Record<string, string>;
|
variableDefaults?: Record<string, string>;
|
||||||
|
/** 数据过滤条件(写入模板);全部满足的数据行才参与排版/导出 */
|
||||||
|
dataFilterRules?: VisibilityRule[];
|
||||||
/** 整页排版配置(每模板独立) */
|
/** 整页排版配置(每模板独立) */
|
||||||
paper?: PaperConfig;
|
paper?: PaperConfig;
|
||||||
/** PDF 裁切参考线配置 */
|
/** PDF 裁切参考线配置 */
|
||||||
|
|||||||
94
src/utils.ts
94
src/utils.ts
@@ -155,9 +155,63 @@ export function applyRowVariableMapping(
|
|||||||
export const DEFAULT_EXPORT_RANGE: ExportRangeConfig = { mode: 'all' };
|
export const DEFAULT_EXPORT_RANGE: ExportRangeConfig = { mode: 'all' };
|
||||||
|
|
||||||
export const exportRangeEquals = (a: ExportRangeConfig, b: ExportRangeConfig) =>
|
export const exportRangeEquals = (a: ExportRangeConfig, b: ExportRangeConfig) =>
|
||||||
a.mode === b.mode &&
|
a.mode === b.mode && (a.custom ?? '') === (b.custom ?? '');
|
||||||
(a.custom ?? '') === (b.custom ?? '') &&
|
|
||||||
JSON.stringify(a.filterRules ?? []) === JSON.stringify(b.filterRules ?? []);
|
/** 模板数据过滤规则(仅 variable 目标) */
|
||||||
|
export function resolveDataFilterRules(
|
||||||
|
template: Pick<LabelTemplate, 'dataFilterRules'>
|
||||||
|
): VisibilityRule[] {
|
||||||
|
return resolveVariableFilterRules(template.dataFilterRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按模板数据过滤条件筛选 0-based 行索引 */
|
||||||
|
export function getDataFilteredIndices(
|
||||||
|
rows: RecordRow[],
|
||||||
|
template: Pick<LabelTemplate, 'dataFilterRules'>,
|
||||||
|
variableDefaults?: Record<string, string> | null
|
||||||
|
): number[] {
|
||||||
|
const total = rows.length;
|
||||||
|
if (total <= 0) return [];
|
||||||
|
const rules = getActiveVisibilityRules(resolveDataFilterRules(template));
|
||||||
|
if (rules.length === 0) return Array.from({ length: total }, (_, i) => i);
|
||||||
|
return Array.from({ length: total }, (_, i) => i).filter((i) =>
|
||||||
|
rowMatchesVariableFilterRules(rows[i], i, rules, variableDefaults)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在候选行上应用排版过滤(奇偶/自定义序号,基于原始 1-based 行号) */
|
||||||
|
export function getLayoutRangeIndices(
|
||||||
|
candidateIndices: number[],
|
||||||
|
totalRows: number,
|
||||||
|
range: ExportRangeConfig
|
||||||
|
): number[] {
|
||||||
|
if (candidateIndices.length === 0) return [];
|
||||||
|
switch (range.mode) {
|
||||||
|
case 'odd':
|
||||||
|
return candidateIndices.filter((i) => (i + 1) % 2 === 1);
|
||||||
|
case 'even':
|
||||||
|
return candidateIndices.filter((i) => (i + 1) % 2 === 0);
|
||||||
|
case 'custom': {
|
||||||
|
const parsed = parseCustomExportRange(range.custom ?? '', totalRows);
|
||||||
|
const candidateSet = new Set(candidateIndices);
|
||||||
|
return parsed.filter((i) => candidateSet.has(i));
|
||||||
|
}
|
||||||
|
case 'all':
|
||||||
|
default:
|
||||||
|
return candidateIndices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按数据过滤 + 排版过滤得到最终 0-based 行索引(保持升序) */
|
||||||
|
export function getExportRangeIndices(
|
||||||
|
rows: RecordRow[],
|
||||||
|
range: ExportRangeConfig,
|
||||||
|
template: Pick<LabelTemplate, 'dataFilterRules'>,
|
||||||
|
variableDefaults?: Record<string, string> | null
|
||||||
|
): number[] {
|
||||||
|
const dataFiltered = getDataFilteredIndices(rows, template, variableDefaults);
|
||||||
|
return getLayoutRangeIndices(dataFiltered, rows.length, range);
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_CUT_LINE_CONFIG: CutLineConfig = {
|
export const DEFAULT_CUT_LINE_CONFIG: CutLineConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -247,34 +301,6 @@ export interface PrintableLabel {
|
|||||||
sourceIndex: number;
|
sourceIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按导出范围筛选 0-based 行索引(保持升序) */
|
|
||||||
export function getExportRangeIndices(
|
|
||||||
rows: RecordRow[],
|
|
||||||
range: ExportRangeConfig,
|
|
||||||
variableDefaults?: Record<string, string> | null
|
|
||||||
): number[] {
|
|
||||||
const total = rows.length;
|
|
||||||
if (total <= 0) return [];
|
|
||||||
switch (range.mode) {
|
|
||||||
case 'odd':
|
|
||||||
return Array.from({ length: total }, (_, i) => i).filter((i) => (i + 1) % 2 === 1);
|
|
||||||
case 'even':
|
|
||||||
return Array.from({ length: total }, (_, i) => i).filter((i) => (i + 1) % 2 === 0);
|
|
||||||
case 'custom':
|
|
||||||
return parseCustomExportRange(range.custom ?? '', total);
|
|
||||||
case 'conditional': {
|
|
||||||
const rules = resolveVariableFilterRules(range.filterRules);
|
|
||||||
if (rules.length === 0) return Array.from({ length: total }, (_, i) => i);
|
|
||||||
return Array.from({ length: total }, (_, i) => i).filter((i) =>
|
|
||||||
rowMatchesVariableFilterRules(rows[i], i, rules, variableDefaults)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case 'all':
|
|
||||||
default:
|
|
||||||
return Array.from({ length: total }, (_, i) => i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 解析自定义范围字符串(1-based 序号) */
|
/** 解析自定义范围字符串(1-based 序号) */
|
||||||
export function parseCustomExportRange(input: string, total: number): number[] {
|
export function parseCustomExportRange(input: string, total: number): number[] {
|
||||||
const trimmed = input.trim();
|
const trimmed = input.trim();
|
||||||
@@ -300,13 +326,13 @@ export function parseCustomExportRange(input: string, total: number): number[] {
|
|||||||
export function buildPrintablePages(
|
export function buildPrintablePages(
|
||||||
rows: RecordRow[],
|
rows: RecordRow[],
|
||||||
paper: PaperConfig,
|
paper: PaperConfig,
|
||||||
labelSize: { width: number; height: number },
|
template: Pick<LabelTemplate, 'width' | 'height' | 'dataFilterRules'>,
|
||||||
exportRange: ExportRangeConfig,
|
exportRange: ExportRangeConfig,
|
||||||
variableDefaults?: Record<string, string> | null
|
variableDefaults?: Record<string, string> | null
|
||||||
) {
|
) {
|
||||||
const gridInfo = calculateGrid(paper, labelSize);
|
const gridInfo = calculateGrid(paper, template);
|
||||||
const labelsPerPage = gridInfo.cols * gridInfo.rows;
|
const labelsPerPage = gridInfo.cols * gridInfo.rows;
|
||||||
const indices = getExportRangeIndices(rows, exportRange, variableDefaults);
|
const indices = getExportRangeIndices(rows, exportRange, template, variableDefaults);
|
||||||
const selected: PrintableLabel[] = indices.map((i) => ({ row: rows[i], sourceIndex: i }));
|
const selected: PrintableLabel[] = indices.map((i) => ({ row: rows[i], sourceIndex: i }));
|
||||||
const totalPages = Math.ceil(selected.length / labelsPerPage) || 1;
|
const totalPages = Math.ceil(selected.length / labelsPerPage) || 1;
|
||||||
const pages: (PrintableLabel | null)[][] = [];
|
const pages: (PrintableLabel | null)[][] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user