优化
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>智能标签排版设计与打印生成器</title>
|
<title>智能标签排版设计与打印生成器</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
327
src/App.tsx
327
src/App.tsx
@@ -26,7 +26,6 @@ import { useAppDialog } from './components/AppDialog';
|
|||||||
import {
|
import {
|
||||||
FileDown,
|
FileDown,
|
||||||
Code,
|
Code,
|
||||||
Wand2,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
Plus,
|
Plus,
|
||||||
Download,
|
Download,
|
||||||
@@ -34,6 +33,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import logoUrl from './assets/logo.svg';
|
||||||
|
|
||||||
const TEMPLATE_PREVIEW_MAX_W = 220;
|
const TEMPLATE_PREVIEW_MAX_W = 220;
|
||||||
const TEMPLATE_PREVIEW_MAX_H = 132;
|
const TEMPLATE_PREVIEW_MAX_H = 132;
|
||||||
@@ -72,7 +72,7 @@ export default function App() {
|
|||||||
return parsed.map((t: LabelTemplate) => normalizeTemplate(t));
|
return parsed.map((t: LabelTemplate) => normalizeTemplate(t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export default function App() {
|
|||||||
if (Array.isArray(parsed) && parsed.some((t: LabelTemplate) => t.id === savedId)) {
|
if (Array.isArray(parsed) && parsed.some((t: LabelTemplate) => t.id === savedId)) {
|
||||||
return savedId;
|
return savedId;
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ export default function App() {
|
|||||||
try {
|
try {
|
||||||
const stripped = updatedList.map(stripTemplateForStorage);
|
const stripped = updatedList.map(stripTemplateForStorage);
|
||||||
localStorage.setItem('label_templates_list_v2', JSON.stringify(stripped));
|
localStorage.setItem('label_templates_list_v2', JSON.stringify(stripped));
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Modify active template elements
|
// Modify active template elements
|
||||||
@@ -169,7 +169,7 @@ export default function App() {
|
|||||||
setSelectedElementIds([]);
|
setSelectedElementIds([]);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('selected_label_template_id_v2', id);
|
localStorage.setItem('selected_label_template_id_v2', id);
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
|
|
||||||
setActiveRowIndex(0);
|
setActiveRowIndex(0);
|
||||||
};
|
};
|
||||||
@@ -358,7 +358,7 @@ export default function App() {
|
|||||||
setSelectedTemplateId('');
|
setSelectedTemplateId('');
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem('selected_label_template_id_v2');
|
localStorage.removeItem('selected_label_template_id_v2');
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
setWorkflowStep('template-select');
|
setWorkflowStep('template-select');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,13 +604,12 @@ export default function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`min-h-screen flex flex-col font-sans ${
|
className={`min-h-screen flex flex-col font-sans ${workflowStep === 'template-design' || (workflowStep === 'print-view' && !isMobile)
|
||||||
workflowStep === 'template-design' || (workflowStep === 'print-view' && !isMobile)
|
|
||||||
? 'bg-[#323232] text-[#e8e8e8] h-screen overflow-hidden'
|
? 'bg-[#323232] text-[#e8e8e8] h-screen overflow-hidden'
|
||||||
: workflowStep === 'print-view' && isMobile
|
: workflowStep === 'print-view' && isMobile
|
||||||
? 'bg-slate-100 text-slate-800 h-screen overflow-hidden'
|
? 'bg-slate-100 text-slate-800 h-screen overflow-hidden'
|
||||||
: 'bg-slate-50 text-slate-800'
|
: 'bg-slate-50 text-slate-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/*
|
{/*
|
||||||
=========================================
|
=========================================
|
||||||
@@ -618,61 +617,56 @@ export default function App() {
|
|||||||
=========================================
|
=========================================
|
||||||
*/}
|
*/}
|
||||||
{!(isMobile && workflowStep === 'print-view') && (
|
{!(isMobile && workflowStep === 'print-view') && (
|
||||||
<header
|
<header
|
||||||
className={`no-print bg-slate-950 border-b border-slate-900 shrink-0 shadow flex flex-wrap items-center justify-between gap-4 mobile-template-header ${
|
className={`no-print bg-slate-950 border-b border-slate-900 shrink-0 shadow flex flex-wrap items-center justify-between gap-4 mobile-template-header ${workflowStep === 'template-design' || workflowStep === 'print-view' ? 'px-4 py-2' : 'px-4 py-3 md:px-6 md:py-4'
|
||||||
workflowStep === 'template-design' || workflowStep === 'print-view' ? 'px-4 py-2' : 'px-4 py-3 md:px-6 md:py-4'
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{/* Title logo and slogan description */}
|
||||||
{/* Title logo and slogan description */}
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<img src={logoUrl} alt="" className="w-10 h-10" aria-hidden />
|
||||||
<div className="p-2 md:p-2.5 bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 rounded-xl shadow-inner shrink-0">
|
<div className="min-w-0">
|
||||||
<Wand2 className="w-5 h-5" />
|
<h1 className="text-sm font-bold tracking-wider flex items-center gap-2 text-slate-100 uppercase">
|
||||||
|
<span className="truncate">批量标签排版印刷生成器</span>
|
||||||
|
</h1>
|
||||||
|
{workflowStep !== 'template-design' && workflowStep !== 'print-view' && (
|
||||||
|
<p className="text-[11px] text-slate-400">
|
||||||
|
所见即所得,上传Excel批量排版生成标签
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
|
||||||
<h1 className="text-sm font-bold tracking-wider flex items-center gap-2 text-slate-100 uppercase">
|
|
||||||
<span className="truncate">{isMobile ? '批量标签排版印刷生成器' : '批量标签排版印刷生成器'}</span>
|
|
||||||
<span className="text-[9.5px] bg-indigo-600 text-white font-extrabold px-1.5 py-0.5 rounded shadow-sm tracking-normal shrink-0">PDF</span>
|
|
||||||
</h1>
|
|
||||||
{workflowStep !== 'template-design' && workflowStep !== 'print-view' && !isMobile && (
|
|
||||||
<p className="text-[11px] text-slate-400">
|
|
||||||
数据所见即所得:可视化添加变量、绑定 Excel 多表、快速二元素对齐排料、高保真物理 A4 无损裁切
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{workflowStep === 'template-select' && hasTemplates && !isMobile && (
|
{workflowStep === 'template-select' && hasTemplates && !isMobile && (
|
||||||
<div className="flex flex-wrap items-center gap-2 shrink-0">
|
<div className="flex flex-wrap items-center gap-2 shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowAddTmplForm(true)}
|
onClick={() => setShowAddTmplForm(true)}
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-indigo-600 text-white rounded-xl text-xs font-semibold whitespace-nowrap transition hover:bg-indigo-700 shadow-sm cursor-pointer"
|
className="inline-flex items-center gap-1.5 px-4 py-2 bg-indigo-600 text-white rounded-xl text-xs font-semibold whitespace-nowrap transition hover:bg-indigo-700 shadow-sm cursor-pointer"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4 shrink-0" />
|
<Plus className="w-4 h-4 shrink-0" />
|
||||||
新建模板
|
新建模板
|
||||||
</button>
|
</button>
|
||||||
<label className="inline-flex items-center gap-1.5 px-4 py-2 bg-white/10 hover:bg-white/15 border border-slate-700 text-slate-100 rounded-xl text-xs font-semibold whitespace-nowrap transition shadow-sm cursor-pointer">
|
<label className="inline-flex items-center gap-1.5 px-4 py-2 bg-white/10 hover:bg-white/15 border border-slate-700 text-slate-100 rounded-xl text-xs font-semibold whitespace-nowrap transition shadow-sm cursor-pointer">
|
||||||
<Upload className="w-4 h-4 shrink-0" />
|
<Upload className="w-4 h-4 shrink-0" />
|
||||||
导入 JSON 模板
|
导入 JSON 模板
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".json"
|
accept=".json"
|
||||||
onChange={handleImportTemplate}
|
onChange={handleImportTemplate}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Primary Workspace scroll container */}
|
{/* Primary Workspace scroll container */}
|
||||||
<main
|
<main
|
||||||
className={`no-print flex-1 flex flex-col w-full ${
|
className={`no-print flex-1 flex flex-col w-full ${workflowStep === 'template-design' || workflowStep === 'print-view'
|
||||||
workflowStep === 'template-design' || workflowStep === 'print-view'
|
|
||||||
? 'overflow-hidden min-h-0'
|
? 'overflow-hidden min-h-0'
|
||||||
: 'overflow-auto p-4 md:p-6 max-w-[1720px] mx-auto gap-6'
|
: 'overflow-auto p-4 md:p-6 max-w-[1720px] mx-auto gap-6'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* ========================================================
|
{/* ========================================================
|
||||||
@@ -691,16 +685,16 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center text-center px-6 py-16 md:py-24">
|
<div className="flex flex-col items-center justify-center text-center px-6 py-16 md:py-24">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-indigo-50 text-indigo-500 flex items-center justify-center mb-5 border border-indigo-100">
|
{/* <div className="w-16 h-16 rounded-2xl bg-indigo-50 flex items-center justify-center mb-5 border border-indigo-100">
|
||||||
<Wand2 className="w-8 h-8" />
|
<img src={logoUrl} alt="" className="w-12 h-12" aria-hidden />
|
||||||
</div>
|
</div> */}
|
||||||
<h2 className="text-lg font-bold text-gray-900">还没有标签模板</h2>
|
<h2 className="text-lg font-bold text-gray-900">还没有标签模板</h2>
|
||||||
<p className="text-sm text-gray-500 mt-2 max-w-md leading-relaxed">
|
<p className="text-sm text-gray-500 mt-2 max-w-md leading-relaxed">
|
||||||
{isMobile
|
{isMobile
|
||||||
? '请导入已有模板,或电脑端进行设计。'
|
? '导入已有模板,或在电脑端设计标签模板'
|
||||||
: '创建模板开始可视化设计,或导入已有的 JSON 模板文件。'}
|
: '创建模板开始可视化设计,或导入已有的 JSON 模板文件。'}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-8">
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-3">
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -726,123 +720,121 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{hasTemplates && (
|
{hasTemplates && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{templates.map((tmpl) => {
|
{templates.map((tmpl) => {
|
||||||
const isActive = tmpl.id === selectedTemplateId;
|
const isActive = tmpl.id === selectedTemplateId;
|
||||||
const { displayWidthPx, displayHeightPx } = fitTemplatePreviewSize(
|
const { displayWidthPx, displayHeightPx } = fitTemplatePreviewSize(
|
||||||
tmpl.width,
|
tmpl.width,
|
||||||
tmpl.height
|
tmpl.height
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
|
||||||
key={tmpl.id}
|
|
||||||
onClick={() => handleSelectTemplate(tmpl.id!)}
|
|
||||||
className={`bg-white border rounded-2xl p-5 shadow-xs transition-all flex flex-col cursor-pointer relative overflow-hidden group select-none ${
|
|
||||||
isActive
|
|
||||||
? 'ring-2 ring-indigo-500 border-indigo-500 bg-indigo-50/5'
|
|
||||||
: 'border-gray-200 hover:shadow-md hover:border-indigo-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isActive && (
|
|
||||||
<div
|
|
||||||
className="absolute top-0 right-0 py-1.5 px-3 text-[10px] font-bold rounded-bl-xl text-white"
|
|
||||||
style={{ backgroundColor: '#6366f1' }}
|
|
||||||
>
|
|
||||||
当前选中
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-1 pr-12 mb-3">
|
|
||||||
<span className="text-[10px] font-mono font-extrabold text-indigo-700 bg-indigo-50 px-2 py-0.5 rounded-lg border border-indigo-100">
|
|
||||||
{tmpl.width} × {tmpl.height} mm
|
|
||||||
</span>
|
|
||||||
<h3 className="text-sm font-bold text-gray-900 mt-1 leading-snug">{tmpl.name}</h3>
|
|
||||||
<p className="text-[10px] text-gray-400">{tmpl.elements.length} 个元素</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Preview */}
|
|
||||||
<div
|
<div
|
||||||
className="rounded-xl bg-gray-50 p-3 overflow-hidden flex items-center justify-center"
|
key={tmpl.id}
|
||||||
style={{
|
onClick={() => handleSelectTemplate(tmpl.id!)}
|
||||||
minHeight: `${TEMPLATE_PREVIEW_MAX_H + 24}px`,
|
className={`bg-white border rounded-2xl p-5 shadow-xs transition-all flex flex-col cursor-pointer relative overflow-hidden group select-none ${isActive
|
||||||
}}
|
? 'ring-2 ring-indigo-500 border-indigo-500 bg-indigo-50/5'
|
||||||
|
: 'border-gray-200 hover:shadow-md hover:border-indigo-200'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
|
{isActive && (
|
||||||
|
<div
|
||||||
|
className="absolute top-0 right-0 py-1.5 px-3 text-[10px] font-bold rounded-bl-xl text-white"
|
||||||
|
style={{ backgroundColor: '#6366f1' }}
|
||||||
|
>
|
||||||
|
当前选中
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-1 pr-12 mb-3">
|
||||||
|
<span className="text-[10px] font-mono font-extrabold text-indigo-700 bg-indigo-50 px-2 py-0.5 rounded-lg border border-indigo-100">
|
||||||
|
{tmpl.width} × {tmpl.height} mm
|
||||||
|
</span>
|
||||||
|
<h3 className="text-sm font-bold text-gray-900 mt-1 leading-snug">{tmpl.name}</h3>
|
||||||
|
<p className="text-[10px] text-gray-400">{tmpl.elements.length} 个元素</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
<div
|
<div
|
||||||
className="bg-white shadow-sm shrink-0 overflow-hidden"
|
className="rounded-xl bg-gray-50 p-3 overflow-hidden flex items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
width: `${displayWidthPx}px`,
|
minHeight: `${TEMPLATE_PREVIEW_MAX_H + 24}px`,
|
||||||
height: `${displayHeightPx}px`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CanvasLabelImage
|
<div
|
||||||
widthMm={tmpl.width}
|
className="bg-white shadow-sm shrink-0 overflow-hidden"
|
||||||
heightMm={tmpl.height}
|
style={{
|
||||||
elements={tmpl.elements}
|
width: `${displayWidthPx}px`,
|
||||||
rowData={null}
|
height: `${displayHeightPx}px`,
|
||||||
rowIndex={0}
|
}}
|
||||||
showBorder={true}
|
>
|
||||||
variableDefaults={tmpl.variableDefaults ?? null}
|
<CanvasLabelImage
|
||||||
/>
|
widthMm={tmpl.width}
|
||||||
|
heightMm={tmpl.height}
|
||||||
|
elements={tmpl.elements}
|
||||||
|
rowData={null}
|
||||||
|
rowIndex={0}
|
||||||
|
showBorder={true}
|
||||||
|
variableDefaults={tmpl.variableDefaults ?? null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className={`flex flex-wrap items-center gap-2 mt-4 text-xs ${isMobile ? 'mobile-template-actions' : ''}`}>
|
<div className={`flex flex-wrap items-center gap-2 mt-4 text-xs ${isMobile ? 'mobile-template-actions' : ''}`}>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSelectTemplate(tmpl.id!);
|
||||||
|
setWorkflowStep('template-design');
|
||||||
|
}}
|
||||||
|
className="shrink-0 whitespace-nowrap inline-flex items-center gap-1 px-3 py-1.5 bg-gray-50 text-indigo-700 border border-gray-100 hover:border-indigo-300 hover:bg-indigo-50 rounded-lg text-[11px] font-bold transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Code className="w-3.5 h-3.5 shrink-0" />
|
||||||
|
设计
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSelectTemplate(tmpl.id!);
|
openPrintView(tmpl.id!);
|
||||||
setWorkflowStep('template-design');
|
|
||||||
}}
|
}}
|
||||||
className="shrink-0 whitespace-nowrap inline-flex items-center gap-1 px-3 py-1.5 bg-gray-50 text-indigo-700 border border-gray-100 hover:border-indigo-300 hover:bg-indigo-50 rounded-lg text-[11px] font-bold transition-colors cursor-pointer"
|
className={`shrink-0 whitespace-nowrap inline-flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-[11px] font-bold transition shadow-xs cursor-pointer ${isMobile ? 'mobile-primary-btn flex-1' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Code className="w-3.5 h-3.5 shrink-0" />
|
<FileDown className="w-3.5 h-3.5 shrink-0" />
|
||||||
设计
|
{isMobile ? '上传数据并导出 PDF' : '排版导出'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
{!isMobile && (
|
||||||
<button
|
<>
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
openPrintView(tmpl.id!);
|
e.stopPropagation();
|
||||||
}}
|
handleExportTemplate(tmpl);
|
||||||
className={`shrink-0 whitespace-nowrap inline-flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-[11px] font-bold transition shadow-xs cursor-pointer ${
|
}}
|
||||||
isMobile ? 'mobile-primary-btn flex-1' : ''
|
className="shrink-0 p-1.5 bg-gray-50 text-gray-500 hover:bg-gray-100 border border-gray-100 hover:border-gray-300 rounded-lg inline-flex items-center justify-center transition cursor-pointer"
|
||||||
}`}
|
title="导出 JSON"
|
||||||
>
|
>
|
||||||
<FileDown className="w-3.5 h-3.5 shrink-0" />
|
<Download className="w-3.5 h-3.5" />
|
||||||
{isMobile ? '上传数据并导出 PDF' : '排版导出'}
|
</button>
|
||||||
</button>
|
<button
|
||||||
{!isMobile && (
|
onClick={(e) => {
|
||||||
<>
|
e.stopPropagation();
|
||||||
<button
|
handleDeleteTemplate(tmpl.id!);
|
||||||
onClick={(e) => {
|
}}
|
||||||
e.stopPropagation();
|
className="shrink-0 p-1.5 bg-gray-50 text-rose-500 hover:text-white hover:bg-rose-600 border border-gray-100 rounded-lg inline-flex items-center justify-center transition cursor-pointer"
|
||||||
handleExportTemplate(tmpl);
|
title="删除模板"
|
||||||
}}
|
>
|
||||||
className="shrink-0 p-1.5 bg-gray-50 text-gray-500 hover:bg-gray-100 border border-gray-100 hover:border-gray-300 rounded-lg inline-flex items-center justify-center transition cursor-pointer"
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
title="导出 JSON"
|
</button>
|
||||||
>
|
</>
|
||||||
<Download className="w-3.5 h-3.5" />
|
)}
|
||||||
</button>
|
</div>
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleDeleteTemplate(tmpl.id!);
|
|
||||||
}}
|
|
||||||
className="shrink-0 p-1.5 bg-gray-50 text-rose-500 hover:text-white hover:bg-rose-600 border border-gray-100 rounded-lg inline-flex items-center justify-center transition cursor-pointer"
|
|
||||||
title="删除模板"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -917,7 +909,7 @@ export default function App() {
|
|||||||
selectedElementIds={selectedElementIds}
|
selectedElementIds={selectedElementIds}
|
||||||
onSelectElements={setSelectedElementIds}
|
onSelectElements={setSelectedElementIds}
|
||||||
activeTab="element"
|
activeTab="element"
|
||||||
onChangeTab={() => {}}
|
onChangeTab={() => { }}
|
||||||
paper={paper}
|
paper={paper}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -993,11 +985,10 @@ export default function App() {
|
|||||||
disabled={
|
disabled={
|
||||||
!appliedExportData || appliedLayoutResult.selectedCount === 0 || pdfGenerating
|
!appliedExportData || appliedLayoutResult.selectedCount === 0 || pdfGenerating
|
||||||
}
|
}
|
||||||
className={`inline-flex items-center gap-1.5 px-3 py-1 text-[11px] font-semibold transition cursor-pointer ${
|
className={`inline-flex items-center gap-1.5 px-3 py-1 text-[11px] font-semibold transition cursor-pointer ${appliedExportData && appliedLayoutResult.selectedCount > 0 && !pdfGenerating
|
||||||
appliedExportData && appliedLayoutResult.selectedCount > 0 && !pdfGenerating
|
|
||||||
? 'bg-[#31a8ff] hover:bg-[#2890d8] text-white'
|
? 'bg-[#31a8ff] hover:bg-[#2890d8] text-white'
|
||||||
: 'bg-[#444] text-[#666] cursor-not-allowed'
|
: 'bg-[#444] text-[#666] cursor-not-allowed'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{pdfGenerating ? (
|
{pdfGenerating ? (
|
||||||
<Loader2 className="w-3 h-3 shrink-0 animate-spin" />
|
<Loader2 className="w-3 h-3 shrink-0 animate-spin" />
|
||||||
|
|||||||
1
src/assets/logo.svg
Normal file
1
src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1781266539277" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9515" width="200" height="200"><path d="M690.2528 140.8h-356.608C297.984 140.8 268.8 170.9568 268.8 207.7952V294.4h486.4V207.7952c-0.1024-36.864-29.2864-66.9952-64.9472-66.9952z" fill="#B3DBFF" p-id="9516"></path><path d="M786.6624 268.8H237.2608c-39.168 0-70.8608 31.1296-70.8608 69.6064v296.0128c0 38.4512 31.6928 69.5808 70.8608 69.5808h549.504c39.1424 0 70.8352-31.1296 70.8352-69.5808V338.304c-0.1024-38.3744-31.7952-69.504-70.9376-69.504z" fill="#0084FF" p-id="9517"></path><path d="M268.8 810.2656c0 40.1152 35.4816 72.9344 78.7968 72.9344h328.8064c43.3152 0 78.7968-32.8192 78.7968-72.9344V678.4h-486.4v131.8656z" fill="#B3DBFF" p-id="9518"></path><path d="M204.8 358.4m25.6 0l128 0q25.6 0 25.6 25.6l0 0q0 25.6-25.6 25.6l-128 0q-25.6 0-25.6-25.6l0 0q0-25.6 25.6-25.6Z" fill="#FFFFFF" p-id="9519"></path><path d="M384 716.8m25.6 0l204.8 0q25.6 0 25.6 25.6l0 0q0 25.6-25.6 25.6l-204.8 0q-25.6 0-25.6-25.6l0 0q0-25.6 25.6-25.6Z" fill="#0084FF" p-id="9520"></path><path d="M384 793.6m25.6 0l204.8 0q25.6 0 25.6 25.6l0 0q0 25.6-25.6 25.6l-204.8 0q-25.6 0-25.6-25.6l0 0q0-25.6 25.6-25.6Z" fill="#0084FF" p-id="9521"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -10,14 +10,12 @@ import {
|
|||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { VisibilityRuleDialog } from './VisibilityRuleDialog';
|
import { VisibilityRuleDialog } from './VisibilityRuleDialog';
|
||||||
import { VisibilityRuleSummary } from './VisibilityRuleSummary';
|
import { VisibilityRuleSummary } from './VisibilityRuleSummary';
|
||||||
import { toggleSide, isSideEnabled } from '../elementBox';
|
|
||||||
import {
|
import {
|
||||||
alignToSelectionBounds,
|
alignToSelectionBounds,
|
||||||
alignSelectionToCanvas,
|
alignSelectionToCanvas,
|
||||||
tileSelectedEvenly,
|
tileSelectedEvenly,
|
||||||
type AlignMode,
|
type AlignMode,
|
||||||
} from '../elementAlign';
|
} from '../elementAlign';
|
||||||
import type { ElementSide } from '../types';
|
|
||||||
import { CollapsiblePanelSection } from './CollapsiblePanelSection';
|
import { CollapsiblePanelSection } from './CollapsiblePanelSection';
|
||||||
import { CommitInput } from './CommitInput';
|
import { CommitInput } from './CommitInput';
|
||||||
import {
|
import {
|
||||||
@@ -779,7 +777,64 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text Formatting Controls */}
|
{/* Alignment Tools */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<h5 className="ps-section-title">
|
||||||
|
对齐与分布
|
||||||
|
</h5>
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<AlignHorizontalJustifyStart className="w-3.5 h-3.5" />}
|
||||||
|
label="左对齐"
|
||||||
|
title="靠左对齐 (X=0)"
|
||||||
|
onClick={() => handleElemChange({ ...selectedElem, x: 0 })}
|
||||||
|
/>
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<AlignHorizontalJustifyCenter className="w-3.5 h-3.5" />}
|
||||||
|
label="水平居中"
|
||||||
|
title="水平居中对齐"
|
||||||
|
onClick={() => {
|
||||||
|
const newX = Math.round(((template.width - selectedElem.width) / 2) * 10) / 10;
|
||||||
|
handleElemChange({ ...selectedElem, x: Math.max(0, newX) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<AlignHorizontalJustifyEnd className="w-3.5 h-3.5" />}
|
||||||
|
label="右对齐"
|
||||||
|
title="靠右对齐"
|
||||||
|
onClick={() => {
|
||||||
|
const newX = Math.round((template.width - selectedElem.width) * 10) / 10;
|
||||||
|
handleElemChange({ ...selectedElem, x: Math.max(0, newX) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<AlignVerticalJustifyStart className="w-3.5 h-3.5" />}
|
||||||
|
label="顶对齐"
|
||||||
|
title="靠顶对齐 (Y=0)"
|
||||||
|
onClick={() => handleElemChange({ ...selectedElem, y: 0 })}
|
||||||
|
/>
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<AlignVerticalJustifyCenter className="w-3.5 h-3.5" />}
|
||||||
|
label="垂直居中"
|
||||||
|
title="垂直居中对齐"
|
||||||
|
onClick={() => {
|
||||||
|
const newY = Math.round(((template.height - selectedElem.height) / 2) * 10) / 10;
|
||||||
|
handleElemChange({ ...selectedElem, y: Math.max(0, newY) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AlignToolButton
|
||||||
|
icon={<AlignVerticalJustifyEnd className="w-3.5 h-3.5" />}
|
||||||
|
label="底对齐"
|
||||||
|
title="靠底对齐"
|
||||||
|
onClick={() => {
|
||||||
|
const newY = Math.round((template.height - selectedElem.height) * 10) / 10;
|
||||||
|
handleElemChange({ ...selectedElem, y: Math.max(0, newY) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text Settings */}
|
||||||
{selectedElem.type === 'text' && (
|
{selectedElem.type === 'text' && (
|
||||||
<div className="space-y-3 ps-section-divider">
|
<div className="space-y-3 ps-section-divider">
|
||||||
<h5 className="ps-section-title">
|
<h5 className="ps-section-title">
|
||||||
@@ -1096,7 +1151,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Barcode Formats Options */}
|
{/* Barcode Settings */}
|
||||||
{selectedElem.type === 'barcode' && (
|
{selectedElem.type === 'barcode' && (
|
||||||
<div className="space-y-3 ps-section-divider">
|
<div className="space-y-3 ps-section-divider">
|
||||||
<h5 className="ps-section-title">
|
<h5 className="ps-section-title">
|
||||||
@@ -1134,6 +1189,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Image Settings */}
|
||||||
{selectedElem.type === 'image' && (
|
{selectedElem.type === 'image' && (
|
||||||
<div className="space-y-3 ps-section-divider">
|
<div className="space-y-3 ps-section-divider">
|
||||||
<h5 className="ps-section-title">
|
<h5 className="ps-section-title">
|
||||||
@@ -1367,7 +1423,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
className="ps-range"
|
className="ps-range"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-[#3a3a3a] pt-2.5">
|
<div className="border-t border-[#3a3a3a] pt-2.5 space-y-2">
|
||||||
<label className="ps-field-label">内边距 (mm)</label>
|
<label className="ps-field-label">内边距 (mm)</label>
|
||||||
<PropInput
|
<PropInput
|
||||||
type="number"
|
type="number"
|
||||||
@@ -1383,6 +1439,41 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
}
|
}
|
||||||
className="ps-field font-mono"
|
className="ps-field font-mono"
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
<label className="ps-field-label">各边独立内边距 (mm,留空用统一值)</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
['paddingTop', '上'],
|
||||||
|
['paddingRight', '右'],
|
||||||
|
['paddingBottom', '下'],
|
||||||
|
['paddingLeft', '左'],
|
||||||
|
] as const
|
||||||
|
).map(([key, label]) => (
|
||||||
|
<div key={key} className="flex items-center gap-1">
|
||||||
|
<span className="text-[9px] text-[#777] w-6 shrink-0">{label}</span>
|
||||||
|
<PropInput
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
step="0.5"
|
||||||
|
value={selectedElem[key] ?? ''}
|
||||||
|
onCommit={(val) => {
|
||||||
|
const updated = { ...selectedElem };
|
||||||
|
if (val === '' || val === undefined) {
|
||||||
|
delete updated[key];
|
||||||
|
} else {
|
||||||
|
updated[key] = Math.max(0, Math.min(10, Number(val) || 0));
|
||||||
|
}
|
||||||
|
handleElemChange(updated);
|
||||||
|
}}
|
||||||
|
className="ps-field font-mono text-[10px] flex-1 min-w-0"
|
||||||
|
placeholder="—"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1459,39 +1550,85 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="ps-field-label mb-1">边框边(点击切换)</label>
|
<label className="ps-field-label">各边独立边框宽度 (mm,留空用统一值,0 为不显示)</label>
|
||||||
<div className="grid grid-cols-4 gap-1">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
{(
|
{(
|
||||||
[
|
[
|
||||||
['top', '上'],
|
['borderWidthTop', '上'],
|
||||||
['right', '右'],
|
['borderWidthRight', '右'],
|
||||||
['bottom', '下'],
|
['borderWidthBottom', '下'],
|
||||||
['left', '左'],
|
['borderWidthLeft', '左'],
|
||||||
] as [ElementSide, string][]
|
] as const
|
||||||
).map(([side, label]) => (
|
).map(([key, label]) => (
|
||||||
<button
|
<div key={key} className="flex items-center gap-1">
|
||||||
key={side}
|
<span className="text-[9px] text-[#777] w-6 shrink-0">{label}</span>
|
||||||
type="button"
|
<PropInput
|
||||||
onClick={() =>
|
type="number"
|
||||||
handleElemChange({
|
min="0"
|
||||||
...selectedElem,
|
max="5"
|
||||||
borderSides: toggleSide(selectedElem.borderSides, side),
|
step="0.1"
|
||||||
borderWidth:
|
value={selectedElem[key] ?? ''}
|
||||||
(selectedElem.borderWidth ?? 0) > 0
|
onCommit={(val) => {
|
||||||
? selectedElem.borderWidth
|
const updated = { ...selectedElem };
|
||||||
: 0.3,
|
if (val === '' || val === undefined) {
|
||||||
borderColor: selectedElem.borderColor || '#111827',
|
delete updated[key];
|
||||||
})
|
} else {
|
||||||
}
|
updated[key] = Math.max(0, Math.min(5, Number(val) || 0));
|
||||||
className={`ps-btn text-[10px] justify-center ${
|
}
|
||||||
isSideEnabled(selectedElem.borderSides, side) &&
|
handleElemChange(updated);
|
||||||
(selectedElem.borderWidth ?? 0) > 0
|
}}
|
||||||
? 'active'
|
className="ps-field font-mono text-[10px] flex-1 min-w-0"
|
||||||
: ''
|
placeholder="—"
|
||||||
}`}
|
/>
|
||||||
>
|
</div>
|
||||||
{label}
|
))}
|
||||||
</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="ps-field-label">各边独立边框颜色 (留空用统一值)</label>
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
['borderColorTop', '上'],
|
||||||
|
['borderColorRight', '右'],
|
||||||
|
['borderColorBottom', '下'],
|
||||||
|
['borderColorLeft', '左'],
|
||||||
|
] as const
|
||||||
|
).map(([key, label]) => (
|
||||||
|
<div key={key} className="flex items-center gap-1 min-w-0">
|
||||||
|
<span className="text-[9px] text-[#777] w-6 shrink-0">{label}</span>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={
|
||||||
|
selectedElem[key]?.trim() ||
|
||||||
|
selectedElem.borderColor ||
|
||||||
|
'#111827'
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const updated = { ...selectedElem };
|
||||||
|
updated[key] = e.target.value;
|
||||||
|
handleElemChange(updated);
|
||||||
|
}}
|
||||||
|
className="color-input shrink-0 scale-90"
|
||||||
|
title={`${label}边边框颜色`}
|
||||||
|
/>
|
||||||
|
<PropInput
|
||||||
|
type="text"
|
||||||
|
value={selectedElem[key] ?? ''}
|
||||||
|
onCommit={(val) => {
|
||||||
|
const updated = { ...selectedElem };
|
||||||
|
if (val === '' || val === undefined) {
|
||||||
|
delete updated[key];
|
||||||
|
} else {
|
||||||
|
updated[key] = val;
|
||||||
|
}
|
||||||
|
handleElemChange(updated);
|
||||||
|
}}
|
||||||
|
className="ps-field font-mono text-[10px] flex-1 min-w-0"
|
||||||
|
placeholder="—"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1533,62 +1670,6 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Alignment Tools */}
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<h5 className="ps-section-title">
|
|
||||||
对齐与分布
|
|
||||||
</h5>
|
|
||||||
<div className="grid grid-cols-3 gap-1.5">
|
|
||||||
<AlignToolButton
|
|
||||||
icon={<AlignHorizontalJustifyStart className="w-3.5 h-3.5" />}
|
|
||||||
label="左对齐"
|
|
||||||
title="靠左对齐 (X=0)"
|
|
||||||
onClick={() => handleElemChange({ ...selectedElem, x: 0 })}
|
|
||||||
/>
|
|
||||||
<AlignToolButton
|
|
||||||
icon={<AlignHorizontalJustifyCenter className="w-3.5 h-3.5" />}
|
|
||||||
label="水平居中"
|
|
||||||
title="水平居中对齐"
|
|
||||||
onClick={() => {
|
|
||||||
const newX = Math.round(((template.width - selectedElem.width) / 2) * 10) / 10;
|
|
||||||
handleElemChange({ ...selectedElem, x: Math.max(0, newX) });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AlignToolButton
|
|
||||||
icon={<AlignHorizontalJustifyEnd className="w-3.5 h-3.5" />}
|
|
||||||
label="右对齐"
|
|
||||||
title="靠右对齐"
|
|
||||||
onClick={() => {
|
|
||||||
const newX = Math.round((template.width - selectedElem.width) * 10) / 10;
|
|
||||||
handleElemChange({ ...selectedElem, x: Math.max(0, newX) });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AlignToolButton
|
|
||||||
icon={<AlignVerticalJustifyStart className="w-3.5 h-3.5" />}
|
|
||||||
label="顶对齐"
|
|
||||||
title="靠顶对齐 (Y=0)"
|
|
||||||
onClick={() => handleElemChange({ ...selectedElem, y: 0 })}
|
|
||||||
/>
|
|
||||||
<AlignToolButton
|
|
||||||
icon={<AlignVerticalJustifyCenter className="w-3.5 h-3.5" />}
|
|
||||||
label="垂直居中"
|
|
||||||
title="垂直居中对齐"
|
|
||||||
onClick={() => {
|
|
||||||
const newY = Math.round(((template.height - selectedElem.height) / 2) * 10) / 10;
|
|
||||||
handleElemChange({ ...selectedElem, y: Math.max(0, newY) });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AlignToolButton
|
|
||||||
icon={<AlignVerticalJustifyEnd className="w-3.5 h-3.5" />}
|
|
||||||
label="底对齐"
|
|
||||||
title="靠底对齐"
|
|
||||||
onClick={() => {
|
|
||||||
const newY = Math.round((template.height - selectedElem.height) * 10) / 10;
|
|
||||||
handleElemChange({ ...selectedElem, y: Math.max(0, newY) });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,86 @@ export interface BorderSides {
|
|||||||
left: boolean;
|
left: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaddingMm {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BorderSideStyle {
|
||||||
|
width: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BorderConfig = Record<ElementSide, BorderSideStyle>;
|
||||||
|
|
||||||
|
export function resolvePaddingMm(elem: TemplateElement): PaddingMm {
|
||||||
|
const base = elem.padding ?? 0;
|
||||||
|
const val = (specific?: number) =>
|
||||||
|
specific !== undefined && specific !== null ? specific : base;
|
||||||
|
return {
|
||||||
|
top: val(elem.paddingTop),
|
||||||
|
right: val(elem.paddingRight),
|
||||||
|
bottom: val(elem.paddingBottom),
|
||||||
|
left: val(elem.paddingLeft),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPaddedRectPx(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
w: number,
|
||||||
|
h: number,
|
||||||
|
elem: TemplateElement,
|
||||||
|
scale: number
|
||||||
|
) {
|
||||||
|
const pad = resolvePaddingMm(elem);
|
||||||
|
const top = Math.round(pad.top * scale);
|
||||||
|
const right = Math.round(pad.right * scale);
|
||||||
|
const bottom = Math.round(pad.bottom * scale);
|
||||||
|
const left = Math.round(pad.left * scale);
|
||||||
|
return {
|
||||||
|
x: x + left,
|
||||||
|
y: y + top,
|
||||||
|
w: Math.max(1, w - left - right),
|
||||||
|
h: Math.max(1, h - top - bottom),
|
||||||
|
paddingPx: { top, right, bottom, left },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveBorderConfig(elem: TemplateElement): BorderConfig {
|
||||||
|
const baseWidth = elem.borderWidth ?? 0;
|
||||||
|
const baseColor = elem.borderColor || '#111827';
|
||||||
|
const legacy = elem.borderSides ?? {};
|
||||||
|
|
||||||
|
const resolveSide = (
|
||||||
|
side: ElementSide,
|
||||||
|
widthOverride: number | undefined,
|
||||||
|
colorOverride: string | undefined
|
||||||
|
): BorderSideStyle => {
|
||||||
|
let width: number;
|
||||||
|
if (widthOverride !== undefined && widthOverride !== null) {
|
||||||
|
width = widthOverride;
|
||||||
|
} else if (baseWidth <= 0 || legacy[side] === false) {
|
||||||
|
width = 0;
|
||||||
|
} else {
|
||||||
|
width = baseWidth;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
color: colorOverride?.trim() ? colorOverride : baseColor,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: resolveSide('top', elem.borderWidthTop, elem.borderColorTop),
|
||||||
|
right: resolveSide('right', elem.borderWidthRight, elem.borderColorRight),
|
||||||
|
bottom: resolveSide('bottom', elem.borderWidthBottom, elem.borderColorBottom),
|
||||||
|
left: resolveSide('left', elem.borderWidthLeft, elem.borderColorLeft),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveCornerRadiiPx(
|
export function resolveCornerRadiiPx(
|
||||||
elem: TemplateElement,
|
elem: TemplateElement,
|
||||||
scale: number,
|
scale: number,
|
||||||
@@ -39,16 +119,12 @@ export function resolveCornerRadiiPx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveBorderSides(elem: TemplateElement): BorderSides {
|
export function resolveBorderSides(elem: TemplateElement): BorderSides {
|
||||||
const width = elem.borderWidth ?? 0;
|
const cfg = resolveBorderConfig(elem);
|
||||||
if (width <= 0) {
|
|
||||||
return { top: false, right: false, bottom: false, left: false };
|
|
||||||
}
|
|
||||||
const sides = elem.borderSides ?? {};
|
|
||||||
return {
|
return {
|
||||||
top: sides.top !== false,
|
top: cfg.top.width > 0,
|
||||||
right: sides.right !== false,
|
right: cfg.right.width > 0,
|
||||||
bottom: sides.bottom !== false,
|
bottom: cfg.bottom.width > 0,
|
||||||
left: sides.left !== false,
|
left: cfg.left.width > 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,20 +182,17 @@ export function drawElementBorders(
|
|||||||
elem: TemplateElement,
|
elem: TemplateElement,
|
||||||
scale: number
|
scale: number
|
||||||
) {
|
) {
|
||||||
const widthMm = elem.borderWidth ?? 0;
|
const border = resolveBorderConfig(elem);
|
||||||
if (widthMm <= 0) return;
|
|
||||||
|
|
||||||
const color = elem.borderColor || '#111827';
|
|
||||||
const lw = Math.max(1, widthMm * scale);
|
|
||||||
const { tl, tr, br, bl } = radii;
|
const { tl, tr, br, bl } = radii;
|
||||||
const inset = lw / 2;
|
|
||||||
|
|
||||||
ctx.strokeStyle = color;
|
|
||||||
ctx.lineWidth = lw;
|
|
||||||
ctx.lineCap = 'butt';
|
ctx.lineCap = 'butt';
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
if (sides.top) {
|
if (sides.top) {
|
||||||
|
const lw = Math.max(1, border.top.width * scale);
|
||||||
|
const inset = lw / 2;
|
||||||
|
ctx.strokeStyle = border.top.color;
|
||||||
|
ctx.lineWidth = lw;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
if (sides.left && tl > 0) {
|
if (sides.left && tl > 0) {
|
||||||
ctx.arc(x + tl, y + tl, Math.max(0, tl - inset), Math.PI, 1.5 * Math.PI);
|
ctx.arc(x + tl, y + tl, Math.max(0, tl - inset), Math.PI, 1.5 * Math.PI);
|
||||||
@@ -136,6 +209,10 @@ export function drawElementBorders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sides.right) {
|
if (sides.right) {
|
||||||
|
const lw = Math.max(1, border.right.width * scale);
|
||||||
|
const inset = lw / 2;
|
||||||
|
ctx.strokeStyle = border.right.color;
|
||||||
|
ctx.lineWidth = lw;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
const topStart = sides.top && tr > 0;
|
const topStart = sides.top && tr > 0;
|
||||||
if (topStart) {
|
if (topStart) {
|
||||||
@@ -153,6 +230,10 @@ export function drawElementBorders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sides.bottom) {
|
if (sides.bottom) {
|
||||||
|
const lw = Math.max(1, border.bottom.width * scale);
|
||||||
|
const inset = lw / 2;
|
||||||
|
ctx.strokeStyle = border.bottom.color;
|
||||||
|
ctx.lineWidth = lw;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
if (sides.right && br > 0) {
|
if (sides.right && br > 0) {
|
||||||
ctx.moveTo(x + w - inset, y + h - br);
|
ctx.moveTo(x + w - inset, y + h - br);
|
||||||
@@ -170,6 +251,10 @@ export function drawElementBorders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sides.left) {
|
if (sides.left) {
|
||||||
|
const lw = Math.max(1, border.left.width * scale);
|
||||||
|
const inset = lw / 2;
|
||||||
|
ctx.strokeStyle = border.left.color;
|
||||||
|
ctx.lineWidth = lw;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
const bottomStart = sides.bottom && bl > 0;
|
const bottomStart = sides.bottom && bl > 0;
|
||||||
if (bottomStart) {
|
if (bottomStart) {
|
||||||
@@ -202,20 +287,3 @@ export function drawElementChrome(
|
|||||||
drawElementBorders(ctx, x, y, w, h, radii, sides, elem, scale);
|
drawElementBorders(ctx, x, y, w, h, radii, sides, elem, scale);
|
||||||
return { radii, sides };
|
return { radii, sides };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleSide(
|
|
||||||
current: Partial<Record<ElementSide, boolean>> | undefined,
|
|
||||||
side: ElementSide
|
|
||||||
): Partial<Record<ElementSide, boolean>> {
|
|
||||||
const next = { ...current };
|
|
||||||
const enabled = current?.[side] !== false;
|
|
||||||
next[side] = !enabled;
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isSideEnabled(
|
|
||||||
sides: Partial<Record<ElementSide, boolean>> | undefined,
|
|
||||||
side: ElementSide
|
|
||||||
): boolean {
|
|
||||||
return sides?.[side] !== false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
resolveCornerRadiiPx,
|
resolveCornerRadiiPx,
|
||||||
resolveBorderSides,
|
resolveBorderSides,
|
||||||
|
resolvePaddingMm,
|
||||||
|
getPaddedRectPx,
|
||||||
traceRoundedRect,
|
traceRoundedRect,
|
||||||
drawElementFill,
|
drawElementFill,
|
||||||
drawElementBorders,
|
drawElementBorders,
|
||||||
@@ -213,23 +215,6 @@ function layoutVerticalTextColumns(
|
|||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPaddedRect(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
w: number,
|
|
||||||
h: number,
|
|
||||||
paddingMm: number,
|
|
||||||
scale: number
|
|
||||||
) {
|
|
||||||
const paddingPx = Math.round(paddingMm * scale);
|
|
||||||
return {
|
|
||||||
x: x + paddingPx,
|
|
||||||
y: y + paddingPx,
|
|
||||||
w: Math.max(1, w - 2 * paddingPx),
|
|
||||||
h: Math.max(1, h - 2 * paddingPx),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawImagePlaceholder(
|
function drawImagePlaceholder(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
x: number,
|
x: number,
|
||||||
@@ -343,10 +328,17 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
|
|
||||||
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
|
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
|
||||||
|
|
||||||
const paddingMm = elem.padding || 0;
|
const padPx = (() => {
|
||||||
const paddingPx = Math.round(paddingMm * scale);
|
const pad = resolvePaddingMm(elem);
|
||||||
const writableW = Math.max(1, ew - 2 * paddingPx);
|
return {
|
||||||
const writableH = Math.max(1, eh - 2 * paddingPx);
|
top: Math.round(pad.top * scale),
|
||||||
|
right: Math.round(pad.right * scale),
|
||||||
|
bottom: Math.round(pad.bottom * scale),
|
||||||
|
left: Math.round(pad.left * scale),
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
const writableW = Math.max(1, ew - padPx.left - padPx.right);
|
||||||
|
const writableH = Math.max(1, eh - padPx.top - padPx.bottom);
|
||||||
|
|
||||||
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
|
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
|
||||||
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
|
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
|
||||||
@@ -375,8 +367,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
|
|
||||||
const textScaleX = Math.max(0.1, Math.min(5, elem.textScaleX ?? 1));
|
const textScaleX = Math.max(0.1, Math.min(5, elem.textScaleX ?? 1));
|
||||||
const textScaleY = Math.max(0.1, Math.min(5, elem.textScaleY ?? 1));
|
const textScaleY = Math.max(0.1, Math.min(5, elem.textScaleY ?? 1));
|
||||||
const transformOriginX = ex + paddingPx + writableW / 2;
|
const transformOriginX = ex + padPx.left + writableW / 2;
|
||||||
const transformOriginY = ey + paddingPx + writableH / 2;
|
const transformOriginY = ey + padPx.top + writableH / 2;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
if (textScaleX !== 1 || textScaleY !== 1) {
|
if (textScaleX !== 1 || textScaleY !== 1) {
|
||||||
@@ -393,11 +385,11 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
|
|
||||||
let startX: number;
|
let startX: number;
|
||||||
if (elem.textAlign === 'center') {
|
if (elem.textAlign === 'center') {
|
||||||
startX = ex + paddingPx + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
|
startX = ex + padPx.left + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
|
||||||
} else if (elem.textAlign === 'right') {
|
} else if (elem.textAlign === 'right') {
|
||||||
startX = ex + paddingPx + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
|
startX = ex + padPx.left + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
|
||||||
} else {
|
} else {
|
||||||
startX = ex + paddingPx + columnWidth / 2;
|
startX = ex + padPx.left + columnWidth / 2;
|
||||||
}
|
}
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
@@ -406,18 +398,18 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
const colTextHeight = column.length * lineHeight;
|
const colTextHeight = column.length * lineHeight;
|
||||||
let colStartY: number;
|
let colStartY: number;
|
||||||
if (vAlign === 'top') {
|
if (vAlign === 'top') {
|
||||||
colStartY = ey + paddingPx + lineHeight / 2;
|
colStartY = ey + padPx.top + lineHeight / 2;
|
||||||
} else if (vAlign === 'bottom') {
|
} else if (vAlign === 'bottom') {
|
||||||
colStartY = ey + eh - paddingPx - colTextHeight + lineHeight / 2;
|
colStartY = ey + eh - padPx.bottom - colTextHeight + lineHeight / 2;
|
||||||
} else {
|
} else {
|
||||||
colStartY =
|
colStartY =
|
||||||
ey + paddingPx + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
|
ey + padPx.top + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colX = startX + col * columnWidth;
|
const colX = startX + col * columnWidth;
|
||||||
for (let i = 0; i < column.length; i++) {
|
for (let i = 0; i < column.length; i++) {
|
||||||
const charY = colStartY + i * lineHeight;
|
const charY = colStartY + i * lineHeight;
|
||||||
if (charY - lineHeight / 2 <= ey + eh - paddingPx) {
|
if (charY - lineHeight / 2 <= ey + eh - padPx.bottom) {
|
||||||
ctx.fillText(column[i], colX, charY);
|
ctx.fillText(column[i], colX, charY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,32 +418,32 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
|
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
|
||||||
const totalTextHeight = lines.length * lineHeight;
|
const totalTextHeight = lines.length * lineHeight;
|
||||||
|
|
||||||
let textX = ex + paddingPx;
|
let textX = ex + padPx.left;
|
||||||
let textAlign: CanvasTextAlign = 'left';
|
let textAlign: CanvasTextAlign = 'left';
|
||||||
if (elem.textAlign === 'center') {
|
if (elem.textAlign === 'center') {
|
||||||
textX = ex + paddingPx + writableW / 2;
|
textX = ex + padPx.left + writableW / 2;
|
||||||
textAlign = 'center';
|
textAlign = 'center';
|
||||||
} else if (elem.textAlign === 'right') {
|
} else if (elem.textAlign === 'right') {
|
||||||
textX = ex + paddingPx + (writableW - 2);
|
textX = ex + padPx.left + (writableW - 2);
|
||||||
textAlign = 'right';
|
textAlign = 'right';
|
||||||
} else {
|
} else {
|
||||||
textX = ex + paddingPx + 2;
|
textX = ex + padPx.left + 2;
|
||||||
}
|
}
|
||||||
ctx.textAlign = textAlign;
|
ctx.textAlign = textAlign;
|
||||||
|
|
||||||
let startY: number;
|
let startY: number;
|
||||||
if (vAlign === 'top') {
|
if (vAlign === 'top') {
|
||||||
startY = ey + paddingPx + lineHeight / 2;
|
startY = ey + padPx.top + lineHeight / 2;
|
||||||
} else if (vAlign === 'bottom') {
|
} else if (vAlign === 'bottom') {
|
||||||
startY = ey + eh - paddingPx - totalTextHeight + lineHeight / 2;
|
startY = ey + eh - padPx.bottom - totalTextHeight + lineHeight / 2;
|
||||||
} else {
|
} else {
|
||||||
startY =
|
startY =
|
||||||
ey + paddingPx + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
|
ey + padPx.top + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const lineY = startY + i * lineHeight;
|
const lineY = startY + i * lineHeight;
|
||||||
if (lineY - lineHeight / 2 <= ey + eh - paddingPx) {
|
if (lineY - lineHeight / 2 <= ey + eh - padPx.bottom) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
ctx.fillText(line, textX, lineY);
|
ctx.fillText(line, textX, lineY);
|
||||||
drawTextLineDecorations(ctx, elem, textX, lineY, line, fontSizePx, textAlign);
|
drawTextLineDecorations(ctx, elem, textX, lineY, line, fontSizePx, textAlign);
|
||||||
@@ -468,7 +460,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
} else if (elem.type === 'barcode') {
|
} else if (elem.type === 'barcode') {
|
||||||
try {
|
try {
|
||||||
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
||||||
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
|
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
const barcodeValue = value ? String(value).trim() : mode === 'design' ? 'SAMPLE' : '';
|
const barcodeValue = value ? String(value).trim() : mode === 'design' ? 'SAMPLE' : '';
|
||||||
|
|
||||||
@@ -488,7 +480,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Barcode drawing failed', error);
|
console.warn('Barcode drawing failed', error);
|
||||||
if (mode === 'design') {
|
if (mode === 'design') {
|
||||||
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
|
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
||||||
ctx.fillStyle = '#f3f4f6';
|
ctx.fillStyle = '#f3f4f6';
|
||||||
ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
|
ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
|
||||||
ctx.strokeStyle = '#ef4444';
|
ctx.strokeStyle = '#ef4444';
|
||||||
@@ -503,7 +495,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
} else if (elem.type === 'qrcode') {
|
} else if (elem.type === 'qrcode') {
|
||||||
try {
|
try {
|
||||||
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
||||||
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
|
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
||||||
const qrValue = value ? String(value).trim() : mode === 'design' ? 'SAMPLE QR' : '';
|
const qrValue = value ? String(value).trim() : mode === 'design' ? 'SAMPLE QR' : '';
|
||||||
|
|
||||||
if (qrValue) {
|
if (qrValue) {
|
||||||
@@ -522,7 +514,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('QR code drawing failed', error);
|
console.warn('QR code drawing failed', error);
|
||||||
if (mode === 'design') {
|
if (mode === 'design') {
|
||||||
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
|
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
||||||
ctx.fillStyle = '#f3f4f6';
|
ctx.fillStyle = '#f3f4f6';
|
||||||
ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
|
ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
|
||||||
ctx.strokeStyle = '#ef4444';
|
ctx.strokeStyle = '#ef4444';
|
||||||
@@ -532,7 +524,7 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
}
|
}
|
||||||
} else if (elem.type === 'image') {
|
} else if (elem.type === 'image') {
|
||||||
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
||||||
const inner = getPaddedRect(ex, ey, ew, eh, elem.padding || 0, scale);
|
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
||||||
const src = value ? String(value).trim() : '';
|
const src = value ? String(value).trim() : '';
|
||||||
if (!src) {
|
if (!src) {
|
||||||
if (mode === 'design') {
|
if (mode === 'design') {
|
||||||
|
|||||||
12
src/main.tsx
12
src/main.tsx
@@ -2,8 +2,20 @@ import {StrictMode} from 'react';
|
|||||||
import {createRoot} from 'react-dom/client';
|
import {createRoot} from 'react-dom/client';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import { AppDialogProvider } from './components/AppDialog.tsx';
|
import { AppDialogProvider } from './components/AppDialog.tsx';
|
||||||
|
import logoUrl from './assets/logo.svg';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
const faviconLink =
|
||||||
|
document.querySelector<HTMLLinkElement>("link[rel='icon']") ??
|
||||||
|
(() => {
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'icon';
|
||||||
|
link.type = 'image/svg+xml';
|
||||||
|
document.head.appendChild(link);
|
||||||
|
return link;
|
||||||
|
})();
|
||||||
|
faviconLink.href = logoUrl;
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<AppDialogProvider>
|
<AppDialogProvider>
|
||||||
|
|||||||
16
src/types.ts
16
src/types.ts
@@ -71,6 +71,10 @@ export interface TemplateElement {
|
|||||||
textColor?: string;
|
textColor?: string;
|
||||||
/** 内边距 (mm),所有元素类型均有效 */
|
/** 内边距 (mm),所有元素类型均有效 */
|
||||||
padding?: number;
|
padding?: number;
|
||||||
|
paddingTop?: number;
|
||||||
|
paddingRight?: number;
|
||||||
|
paddingBottom?: number;
|
||||||
|
paddingLeft?: number;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
/** 图片缩放方式,仅 type=image 时有效 */
|
/** 图片缩放方式,仅 type=image 时有效 */
|
||||||
imageFit?: 'contain' | 'cover' | 'fill';
|
imageFit?: 'contain' | 'cover' | 'fill';
|
||||||
@@ -78,10 +82,18 @@ export interface TemplateElement {
|
|||||||
textFormat?: TextFormat;
|
textFormat?: TextFormat;
|
||||||
/** 数值/百分比/货币的小数位数,默认 2 */
|
/** 数值/百分比/货币的小数位数,默认 2 */
|
||||||
decimalPlaces?: number;
|
decimalPlaces?: number;
|
||||||
/** 边框宽度 (mm),0 为无边框 */
|
/** 边框宽度 (mm),0 为无边框;各边留空时沿用此值 */
|
||||||
borderWidth?: number;
|
borderWidth?: number;
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
/** 指定哪些边显示边框,默认四边 */
|
borderWidthTop?: number;
|
||||||
|
borderWidthRight?: number;
|
||||||
|
borderWidthBottom?: number;
|
||||||
|
borderWidthLeft?: number;
|
||||||
|
borderColorTop?: string;
|
||||||
|
borderColorRight?: string;
|
||||||
|
borderColorBottom?: string;
|
||||||
|
borderColorLeft?: string;
|
||||||
|
/** @deprecated 使用各边 borderWidth*(0 为不显示) */
|
||||||
borderSides?: Partial<Record<ElementSide, boolean>>;
|
borderSides?: Partial<Record<ElementSide, boolean>>;
|
||||||
/** 圆角半径 (mm),可被各角覆盖 */
|
/** 圆角半径 (mm),可被各角覆盖 */
|
||||||
borderRadius?: number;
|
borderRadius?: number;
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
Reference in New Issue
Block a user