fix
This commit is contained in:
40
src/App.tsx
40
src/App.tsx
@@ -169,8 +169,8 @@ export default function App() {
|
|||||||
// Modal / Inline input values for template creations
|
// Modal / Inline input values for template creations
|
||||||
const [showAddTmplForm, setShowAddTmplForm] = useState<boolean>(false);
|
const [showAddTmplForm, setShowAddTmplForm] = useState<boolean>(false);
|
||||||
const [newTmplName, setNewTmplName] = useState<string>('');
|
const [newTmplName, setNewTmplName] = useState<string>('');
|
||||||
const [newTmplW, setNewTmplW] = useState<number>(75);
|
const [newTmplW, setNewTmplW] = useState('75');
|
||||||
const [newTmplH, setNewTmplH] = useState<number>(45);
|
const [newTmplH, setNewTmplH] = useState('45');
|
||||||
const [editingTemplateId, setEditingTemplateId] = useState<string | null>(null);
|
const [editingTemplateId, setEditingTemplateId] = useState<string | null>(null);
|
||||||
const [editTmplName, setEditTmplName] = useState<string>('');
|
const [editTmplName, setEditTmplName] = useState<string>('');
|
||||||
const [templateMenuId, setTemplateMenuId] = useState<string | null>(null);
|
const [templateMenuId, setTemplateMenuId] = useState<string | null>(null);
|
||||||
@@ -405,12 +405,22 @@ export default function App() {
|
|||||||
await showAlert('请先输入合法的模板名', { variant: 'warning' });
|
await showAlert('请先输入合法的模板名', { variant: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const width = Number(newTmplW);
|
||||||
|
const height = Number(newTmplH);
|
||||||
|
if (!newTmplW.trim() || Number.isNaN(width) || width < 10 || width > 150) {
|
||||||
|
await showAlert('请输入合法的标签宽度(10–150 mm)', { variant: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newTmplH.trim() || Number.isNaN(height) || height < 10 || height > 150) {
|
||||||
|
await showAlert('请输入合法的标签高度(10–150 mm)', { variant: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newId = `custom_tmpl_${Date.now()}`;
|
const newId = `custom_tmpl_${Date.now()}`;
|
||||||
const newTmpl: LabelTemplate = {
|
const newTmpl: LabelTemplate = {
|
||||||
id: newId,
|
id: newId,
|
||||||
name: `${newTmplName}`,
|
name: `${newTmplName}`,
|
||||||
width: Math.max(10, Number(newTmplW) || 70),
|
width,
|
||||||
height: Math.max(10, Number(newTmplH) || 40),
|
height,
|
||||||
elements: [],
|
elements: [],
|
||||||
paper: DEFAULT_PAPER_CONFIG,
|
paper: DEFAULT_PAPER_CONFIG,
|
||||||
cutLine: DEFAULT_CUT_LINE_CONFIG,
|
cutLine: DEFAULT_CUT_LINE_CONFIG,
|
||||||
@@ -427,8 +437,8 @@ export default function App() {
|
|||||||
const closeAddTemplateDialog = () => {
|
const closeAddTemplateDialog = () => {
|
||||||
setShowAddTmplForm(false);
|
setShowAddTmplForm(false);
|
||||||
setNewTmplName('');
|
setNewTmplName('');
|
||||||
setNewTmplW(75);
|
setNewTmplW('75');
|
||||||
setNewTmplH(45);
|
setNewTmplH('45');
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditTemplateDialog = (tmpl: LabelTemplate) => {
|
const openEditTemplateDialog = (tmpl: LabelTemplate) => {
|
||||||
@@ -1621,7 +1631,6 @@ export default function App() {
|
|||||||
{editingTemplateId && (
|
{editingTemplateId && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
|
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
|
||||||
onClick={closeEditTemplateDialog}
|
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1669,7 +1678,6 @@ export default function App() {
|
|||||||
{showAddTmplForm && (
|
{showAddTmplForm && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
|
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
|
||||||
onClick={closeAddTemplateDialog}
|
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1716,12 +1724,12 @@ export default function App() {
|
|||||||
标签宽度 (mm)
|
标签宽度 (mm)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
required
|
required
|
||||||
min="10"
|
placeholder="75"
|
||||||
max="150"
|
|
||||||
value={newTmplW}
|
value={newTmplW}
|
||||||
onChange={(e) => setNewTmplW(Number(e.target.value))}
|
onChange={(e) => setNewTmplW(e.target.value)}
|
||||||
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm font-mono focus:border-indigo-400 focus:outline-none"
|
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm font-mono focus:border-indigo-400 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1730,12 +1738,12 @@ export default function App() {
|
|||||||
标签高度 (mm)
|
标签高度 (mm)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
required
|
required
|
||||||
min="10"
|
placeholder="45"
|
||||||
max="150"
|
|
||||||
value={newTmplH}
|
value={newTmplH}
|
||||||
onChange={(e) => setNewTmplH(Number(e.target.value))}
|
onChange={(e) => setNewTmplH(e.target.value)}
|
||||||
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm font-mono focus:border-indigo-400 focus:outline-none"
|
className="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-lg text-sm font-mono focus:border-indigo-400 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="ps-modal-overlay" onClick={onClose} role="presentation">
|
<div className="ps-modal-overlay" role="presentation">
|
||||||
<div
|
<div
|
||||||
className="ps-modal"
|
className="ps-modal"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -129,7 +129,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="ps-modal-body space-y-3">
|
<div className="ps-modal-body space-y-3">
|
||||||
<p className="text-[11px] text-[#888] leading-relaxed">
|
<p className="text-[11px] text-[#888] leading-relaxed">
|
||||||
蒙版仅作用于当前图层(背景与内容);先在离屏画布合成,再按蒙版裁剪后绘制到画布。边框不受蒙版影响。
|
蒙版作用于当前图层(背景、内容与边框);先在离屏画布合成,再按蒙版裁剪后绘制到画布。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -511,10 +511,10 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
<p className="text-[10px] text-[#888]">暂无变量</p>
|
<p className="text-[10px] text-[#888]">暂无变量</p>
|
||||||
) : (
|
) : (
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-2 pt-2">
|
||||||
<div className="text-[10px] font-bold text-[#31a8ff]">变量列表</div>
|
<div className="text-[10px] font-bold text-[#31a8ff]">变量列表</div>
|
||||||
<p className="text-[10px] text-[#666] leading-relaxed mb-3">
|
<p className="text-[10px] text-[#666] leading-relaxed">
|
||||||
可编辑变量预览值,未被内容或显示条件引用的变量可删除。
|
未被内容或显示条件引用的变量可删除
|
||||||
</p>
|
</p>
|
||||||
{templateVariables.map((v) => {
|
{templateVariables.map((v) => {
|
||||||
const inUse = isTemplateVariableInUse(template, v);
|
const inUse = isTemplateVariableInUse(template, v);
|
||||||
@@ -2289,10 +2289,15 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`ps-layer-item-actions pointer-events-auto ${isNarrowScreen || variant === 'mobile' || isLocked
|
className={`ps-layer-item-actions pointer-events-auto ${
|
||||||
? 'opacity-100'
|
isNarrowScreen || variant === 'mobile' || isLocked
|
||||||
: 'opacity-0 group-hover:opacity-100 transition-opacity'
|
? ''
|
||||||
}`}
|
: 'ps-layer-item-actions--float'
|
||||||
|
} ${
|
||||||
|
isNarrowScreen || variant === 'mobile' || isLocked
|
||||||
|
? 'opacity-100'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{!isLocked && (
|
{!isLocked && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className="ps-modal-overlay" onClick={onClose} role="presentation">
|
<div className="ps-modal-overlay" role="presentation">
|
||||||
<div
|
<div
|
||||||
className="ps-modal"
|
className="ps-modal"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|||||||
@@ -241,8 +241,7 @@ type DrawLayerFn = (
|
|||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绘制元素图层:无蒙版时直接绘制;有蒙版时先在离屏画布绘制整层,应用蒙版后再合成到主画布。
|
* 绘制元素图层:无蒙版时直接绘制;有蒙版时先在离屏画布绘制整层(背景、内容、边框),应用蒙版后再合成到主画布。
|
||||||
* 仅对当前元素的 layer 回调内容生效,边框等装饰应在合成后单独绘制。
|
|
||||||
*/
|
*/
|
||||||
export async function composeElementLayer(
|
export async function composeElementLayer(
|
||||||
mainCtx: CanvasRenderingContext2D,
|
mainCtx: CanvasRenderingContext2D,
|
||||||
|
|||||||
@@ -753,6 +753,7 @@ html.app-dark-shell body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ps-layer-item {
|
.ps-layer-item {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -810,6 +811,30 @@ html.app-dark-shell body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PC:未 hover 时不占位,悬停时浮于右侧 */
|
||||||
|
.ps-layer-item-actions--float {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
padding-left: 16px;
|
||||||
|
background: linear-gradient(to left, #2d2d2d 180px, transparent);
|
||||||
|
transition: opacity 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps-layer-item:hover .ps-layer-item-actions--float {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
background: linear-gradient(to left, #383838 180px, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ps-layer-item.selected:hover .ps-layer-item-actions--float {
|
||||||
|
background: linear-gradient(to left, #3d4f5f 180px, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.ps-layer-item-actions button {
|
.ps-layer-item-actions button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -606,9 +606,9 @@ async function drawTableElement(
|
|||||||
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
||||||
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
||||||
await drawCellContent(layerCtx, ox, oy);
|
await drawCellContent(layerCtx, ox, oy);
|
||||||
|
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
|
||||||
drawTableGridLines(ctx, elem, ex, ey, scale);
|
drawTableGridLines(ctx, elem, ex, ey, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,9 +871,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
||||||
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
||||||
drawTextContentInBox(layerCtx, elem, ox, oy, ow, oh, String(value), scale);
|
drawTextContentInBox(layerCtx, elem, ox, oy, ow, oh, String(value), scale);
|
||||||
|
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
|
||||||
} else if (elem.type === 'barcode') {
|
} else if (elem.type === 'barcode') {
|
||||||
try {
|
try {
|
||||||
const radii = resolveCornerRadiiPx(elem, scale, ew, eh);
|
const radii = resolveCornerRadiiPx(elem, scale, ew, eh);
|
||||||
@@ -882,7 +881,10 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
|
|
||||||
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
||||||
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
||||||
if (!barcodeValue) return;
|
if (!barcodeValue) {
|
||||||
|
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const inner = getPaddedRectPx(ox, oy, ow, oh, elem, scale);
|
const inner = getPaddedRectPx(ox, oy, ow, oh, elem, scale);
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
JsBarcode(tempCanvas, barcodeValue, {
|
JsBarcode(tempCanvas, barcodeValue, {
|
||||||
@@ -895,9 +897,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
background: GENERATOR_TRANSPARENT_BG,
|
background: GENERATOR_TRANSPARENT_BG,
|
||||||
});
|
});
|
||||||
layerCtx.drawImage(tempCanvas, inner.x, inner.y, inner.w, inner.h);
|
layerCtx.drawImage(tempCanvas, inner.x, inner.y, inner.w, inner.h);
|
||||||
|
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Barcode drawing failed', error);
|
console.warn('Barcode drawing failed', error);
|
||||||
if (mode === 'design') {
|
if (mode === 'design') {
|
||||||
@@ -921,7 +922,10 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
|
|
||||||
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
|
||||||
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
|
||||||
if (!qrValue) return;
|
if (!qrValue) {
|
||||||
|
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const inner = getPaddedRectPx(ox, oy, ow, oh, elem, scale);
|
const inner = getPaddedRectPx(ox, oy, ow, oh, elem, scale);
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
await QRCode.toCanvas(tempCanvas, qrValue, {
|
await QRCode.toCanvas(tempCanvas, qrValue, {
|
||||||
@@ -933,9 +937,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
layerCtx.drawImage(tempCanvas, inner.x, inner.y, inner.w, inner.h);
|
layerCtx.drawImage(tempCanvas, inner.x, inner.y, inner.w, inner.h);
|
||||||
|
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('QR code drawing failed', error);
|
console.warn('QR code drawing failed', error);
|
||||||
if (mode === 'design') {
|
if (mode === 'design') {
|
||||||
@@ -977,8 +980,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
} else {
|
} else {
|
||||||
drawImagePlaceholder(layerCtx, inner.x, inner.y, inner.w, inner.h, '图片');
|
drawImagePlaceholder(layerCtx, inner.x, inner.y, inner.w, inner.h, '图片');
|
||||||
}
|
}
|
||||||
|
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
|
||||||
});
|
});
|
||||||
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
|
||||||
} else {
|
} else {
|
||||||
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
||||||
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
||||||
|
|||||||
Reference in New Issue
Block a user