This commit is contained in:
iqudoo
2026-06-16 23:41:27 +08:00
parent 43900a8b52
commit 3a50e7e33b
7 changed files with 78 additions and 38 deletions

View File

@@ -169,8 +169,8 @@ export default function App() {
// Modal / Inline input values for template creations
const [showAddTmplForm, setShowAddTmplForm] = useState<boolean>(false);
const [newTmplName, setNewTmplName] = useState<string>('');
const [newTmplW, setNewTmplW] = useState<number>(75);
const [newTmplH, setNewTmplH] = useState<number>(45);
const [newTmplW, setNewTmplW] = useState('75');
const [newTmplH, setNewTmplH] = useState('45');
const [editingTemplateId, setEditingTemplateId] = useState<string | null>(null);
const [editTmplName, setEditTmplName] = useState<string>('');
const [templateMenuId, setTemplateMenuId] = useState<string | null>(null);
@@ -405,12 +405,22 @@ export default function App() {
await showAlert('请先输入合法的模板名', { variant: 'warning' });
return;
}
const width = Number(newTmplW);
const height = Number(newTmplH);
if (!newTmplW.trim() || Number.isNaN(width) || width < 10 || width > 150) {
await showAlert('请输入合法的标签宽度10150 mm', { variant: 'warning' });
return;
}
if (!newTmplH.trim() || Number.isNaN(height) || height < 10 || height > 150) {
await showAlert('请输入合法的标签高度10150 mm', { variant: 'warning' });
return;
}
const newId = `custom_tmpl_${Date.now()}`;
const newTmpl: LabelTemplate = {
id: newId,
name: `${newTmplName}`,
width: Math.max(10, Number(newTmplW) || 70),
height: Math.max(10, Number(newTmplH) || 40),
width,
height,
elements: [],
paper: DEFAULT_PAPER_CONFIG,
cutLine: DEFAULT_CUT_LINE_CONFIG,
@@ -427,8 +437,8 @@ export default function App() {
const closeAddTemplateDialog = () => {
setShowAddTmplForm(false);
setNewTmplName('');
setNewTmplW(75);
setNewTmplH(45);
setNewTmplW('75');
setNewTmplH('45');
};
const openEditTemplateDialog = (tmpl: LabelTemplate) => {
@@ -1621,7 +1631,6 @@ export default function App() {
{editingTemplateId && (
<div
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
onClick={closeEditTemplateDialog}
role="presentation"
>
<div
@@ -1669,7 +1678,6 @@ export default function App() {
{showAddTmplForm && (
<div
className="fixed inset-0 z-[9000] flex items-center justify-center bg-slate-950/60 backdrop-blur-sm p-4"
onClick={closeAddTemplateDialog}
role="presentation"
>
<div
@@ -1716,12 +1724,12 @@ export default function App() {
(mm)
</label>
<input
type="number"
type="text"
inputMode="decimal"
required
min="10"
max="150"
placeholder="75"
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"
/>
</div>
@@ -1730,12 +1738,12 @@ export default function App() {
(mm)
</label>
<input
type="number"
type="text"
inputMode="decimal"
required
min="10"
max="150"
placeholder="45"
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"
/>
</div>

View File

@@ -110,7 +110,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
};
return createPortal(
<div className="ps-modal-overlay" onClick={onClose} role="presentation">
<div className="ps-modal-overlay" role="presentation">
<div
className="ps-modal"
onClick={(e) => e.stopPropagation()}
@@ -129,7 +129,7 @@ export const ElementMaskDialog: React.FC<ElementMaskDialogProps> = ({
<form onSubmit={handleSubmit}>
<div className="ps-modal-body space-y-3">
<p className="text-[11px] text-[#888] leading-relaxed">
</p>
<div>

View File

@@ -511,10 +511,10 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
<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>
<p className="text-[10px] text-[#666] leading-relaxed mb-3">
<p className="text-[10px] text-[#666] leading-relaxed">
</p>
{templateVariables.map((v) => {
const inUse = isTemplateVariableInUse(template, v);
@@ -2289,10 +2289,15 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
</div>
<div
className={`ps-layer-item-actions pointer-events-auto ${isNarrowScreen || variant === 'mobile' || isLocked
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100 transition-opacity'
}`}
className={`ps-layer-item-actions pointer-events-auto ${
isNarrowScreen || variant === 'mobile' || isLocked
? ''
: 'ps-layer-item-actions--float'
} ${
isNarrowScreen || variant === 'mobile' || isLocked
? 'opacity-100'
: ''
}`}
>
{!isLocked && (
<>

View File

@@ -124,7 +124,7 @@ export const VisibilityRuleDialog: React.FC<VisibilityRuleDialogProps> = ({
};
return createPortal(
<div className="ps-modal-overlay" onClick={onClose} role="presentation">
<div className="ps-modal-overlay" role="presentation">
<div
className="ps-modal"
onClick={(e) => e.stopPropagation()}

View File

@@ -241,8 +241,7 @@ type DrawLayerFn = (
) => void | Promise<void>;
/**
* 绘制元素图层:无蒙版时直接绘制;有蒙版时先在离屏画布绘制整层,应用蒙版后再合成到主画布。
* 仅对当前元素的 layer 回调内容生效,边框等装饰应在合成后单独绘制。
* 绘制元素图层:无蒙版时直接绘制;有蒙版时先在离屏画布绘制整层(背景、内容、边框),应用蒙版后再合成到主画布。
*/
export async function composeElementLayer(
mainCtx: CanvasRenderingContext2D,

View File

@@ -753,6 +753,7 @@ html.app-dark-shell body {
}
.ps-layer-item {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
@@ -810,6 +811,30 @@ html.app-dark-shell body {
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 {
display: inline-flex;
align-items: center;

View File

@@ -606,9 +606,9 @@ async function drawTableElement(
await composeElementLayer(ctx, elem, ex, ey, ew, eh, scale, async (layerCtx, ox, oy, ow, oh) => {
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
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);
}
@@ -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) => {
drawElementFill(layerCtx, ox, oy, ow, oh, elem, radii);
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') {
try {
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) => {
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 tempCanvas = document.createElement('canvas');
JsBarcode(tempCanvas, barcodeValue, {
@@ -895,9 +897,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
background: GENERATOR_TRANSPARENT_BG,
});
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) {
console.warn('Barcode drawing failed', error);
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) => {
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 tempCanvas = document.createElement('canvas');
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);
drawElementBorders(layerCtx, ox, oy, ow, oh, radii, sides, elem, scale);
});
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
} catch (error) {
console.warn('QR code drawing failed', error);
if (mode === 'design') {
@@ -977,8 +980,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
} else {
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 {
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);