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
|
||||
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('请输入合法的标签宽度(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 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -241,8 +241,7 @@ type DrawLayerFn = (
|
||||
) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 绘制元素图层:无蒙版时直接绘制;有蒙版时先在离屏画布绘制整层,应用蒙版后再合成到主画布。
|
||||
* 仅对当前元素的 layer 回调内容生效,边框等装饰应在合成后单独绘制。
|
||||
* 绘制元素图层:无蒙版时直接绘制;有蒙版时先在离屏画布绘制整层(背景、内容、边框),应用蒙版后再合成到主画布。
|
||||
*/
|
||||
export async function composeElementLayer(
|
||||
mainCtx: CanvasRenderingContext2D,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user