2026-04-17-23-38-34 - 时间格式自定义下拉组件、表格内图片占位符自适应、打印多页页边距修复
This commit is contained in:
@@ -501,31 +501,52 @@ export default function ReportEditor() {
|
||||
|
||||
const insertImage = () => {
|
||||
editorRef.current?.focus();
|
||||
let width = 200;
|
||||
let height = 200;
|
||||
while (true) {
|
||||
const input = prompt('请输入占位符的最大宽度和高度(px),用*号分隔(如: 100*50)。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
|
||||
if (input === null) return;
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') break;
|
||||
const parts = trimmed.split('*').map(s => s.trim());
|
||||
if (parts.length === 2 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
|
||||
width = parseInt(parts[0]) || 0;
|
||||
height = parseInt(parts[1]) || 0;
|
||||
|
||||
const sel = window.getSelection();
|
||||
let node: Node | null = sel?.anchorNode ?? null;
|
||||
let inTable = false;
|
||||
while (node) {
|
||||
if ((node as Element).nodeName === 'TD' || (node as Element).nodeName === 'TH') {
|
||||
inTable = true;
|
||||
break;
|
||||
}
|
||||
alert('格式错误,请确保使用 * 分隔两个数字,例如 100*50');
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;';
|
||||
if (width > 0) styleStr += `width:${width}px;`;
|
||||
if (height > 0) styleStr += `height:${height}px;`;
|
||||
|
||||
const showShortText = width > 0 && width < 80;
|
||||
const hintText = showShortText ? '插入图片' : '插入/点击放置图片';
|
||||
let width = 200;
|
||||
let height = 200;
|
||||
if (!inTable) {
|
||||
while (true) {
|
||||
const input = prompt('请输入占位符的最大宽度和高度(px),用*号分隔(如: 100*50)。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
|
||||
if (input === null) return;
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') break;
|
||||
const parts = trimmed.split('*').map(s => s.trim());
|
||||
if (parts.length === 2 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
|
||||
width = parseInt(parts[0]) || 0;
|
||||
height = parseInt(parts[1]) || 0;
|
||||
break;
|
||||
}
|
||||
alert('格式错误,请确保使用 * 分隔两个数字,例如 100*50');
|
||||
}
|
||||
}
|
||||
|
||||
const hintText = '插入/点击放置图片';
|
||||
const id = 'ph_' + Date.now();
|
||||
const html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${hintText}</span></span>​`;
|
||||
|
||||
let html: string;
|
||||
if (inTable) {
|
||||
const styleStr = 'display:flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;cursor:pointer;width:100%;height:100%;max-width:200px;max-height:200px;min-height:60px;margin:0 auto;';
|
||||
html = `<div id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;">${hintText}</span></div>`;
|
||||
} else {
|
||||
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;';
|
||||
if (width > 0) styleStr += `width:${width}px;`;
|
||||
if (height > 0) styleStr += `height:${height}px;`;
|
||||
const showShortText = width > 0 && width < 80;
|
||||
const text = showShortText ? '插入图片' : hintText;
|
||||
html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${text}</span></span>​`;
|
||||
}
|
||||
|
||||
execCmd('insertHTML', html);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ export default function TemplateManage() {
|
||||
const [newFieldTimeDefault, setNewFieldTimeDefault] = useState<'current' | 'specific'>('specific');
|
||||
const [newFieldFixedTimeValue, setNewFieldFixedTimeValue] = useState('');
|
||||
const [customTimeFormats, setCustomTimeFormats] = useState<string[]>([]);
|
||||
const [formatDropdownOpen, setFormatDropdownOpen] = useState(false);
|
||||
const [newFormatDropdownOpen, setNewFormatDropdownOpen] = useState(false);
|
||||
const [imageAssets, setImageAssets] = useState<{ id: string; name: string; dataUrl: string }[]>([]);
|
||||
|
||||
const updatePageHeight = () => {
|
||||
@@ -515,31 +517,52 @@ export default function TemplateManage() {
|
||||
const insertImage = () => {
|
||||
editorRef.current?.focus();
|
||||
restoreSelection();
|
||||
let width = 200;
|
||||
let height = 200;
|
||||
while (true) {
|
||||
const input = prompt('请输入占位符的最大宽度和高度(px),用 * 分隔(如: 100*50)。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
|
||||
if (input === null) return;
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') break;
|
||||
const parts = trimmed.split('*').map(s => s.trim());
|
||||
if (parts.length === 2 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
|
||||
width = parseInt(parts[0]) || 0;
|
||||
height = parseInt(parts[1]) || 0;
|
||||
|
||||
const sel = window.getSelection();
|
||||
let node: Node | null = sel?.anchorNode ?? null;
|
||||
let inTable = false;
|
||||
while (node) {
|
||||
if ((node as Element).nodeName === 'TD' || (node as Element).nodeName === 'TH') {
|
||||
inTable = true;
|
||||
break;
|
||||
}
|
||||
alert('格式错误,请确保使用 * 分隔两个数字,例如 100*50');
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;';
|
||||
if (width > 0) styleStr += `width:${width}px;`;
|
||||
if (height > 0) styleStr += `height:${height}px;`;
|
||||
|
||||
const showShortText = width > 0 && width < 80;
|
||||
const hintText = showShortText ? '插图' : '插入/点击放置图片';
|
||||
let width = 200;
|
||||
let height = 200;
|
||||
if (!inTable) {
|
||||
while (true) {
|
||||
const input = prompt('请输入占位符的最大宽度和高度(px),用 * 分隔(如: 100*50)。留空则默认宽高为 200*200。(提示: 正文一行文字高度约为 20 像素左右)', '');
|
||||
if (input === null) return;
|
||||
const trimmed = input.trim();
|
||||
if (trimmed === '') break;
|
||||
const parts = trimmed.split('*').map(s => s.trim());
|
||||
if (parts.length === 2 && /^\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) {
|
||||
width = parseInt(parts[0]) || 0;
|
||||
height = parseInt(parts[1]) || 0;
|
||||
break;
|
||||
}
|
||||
alert('格式错误,请确保使用 * 分隔两个数字,例如 100*50');
|
||||
}
|
||||
}
|
||||
|
||||
const hintText = '插入/点击放置图片';
|
||||
const id = 'ph_' + Date.now();
|
||||
const html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${hintText}</span></span>​`;
|
||||
|
||||
let html: string;
|
||||
if (inTable) {
|
||||
const styleStr = 'display:flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;cursor:pointer;width:100%;height:100%;max-width:200px;max-height:200px;min-height:60px;margin:0 auto;';
|
||||
html = `<div id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;">${hintText}</span></div>`;
|
||||
} else {
|
||||
let styleStr = 'display:inline-flex;align-items:center;justify-content:center;border:1px dashed #cbd5e1;background:#f8fafc;vertical-align:middle;margin:0 4px;cursor:pointer;';
|
||||
if (width > 0) styleStr += `width:${width}px;`;
|
||||
if (height > 0) styleStr += `height:${height}px;`;
|
||||
const showShortText = width > 0 && width < 80;
|
||||
const text = showShortText ? '插图' : hintText;
|
||||
html = `<span id="${id}" class="image-placeholder" data-placeholder="true" contenteditable="false" style="${styleStr}"><span class="delete-btn" contenteditable="false">×</span><span class="placeholder-text" style="color:#94a3b8;font-size:11px;pointer-events:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">${text}</span></span>​`;
|
||||
}
|
||||
|
||||
pushHistory();
|
||||
execCmd('insertHTML', html);
|
||||
};
|
||||
@@ -933,42 +956,61 @@ export default function TemplateManage() {
|
||||
className="w-full px-1.5 py-1 text-xs border border-border rounded"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
list={`edit-format-list-${field.key}`}
|
||||
value={editFieldTimeFormat}
|
||||
onChange={(e) => setEditFieldTimeFormat(e.target.value)}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value.trim();
|
||||
if (val && !customTimeFormats.includes(val)) {
|
||||
const next = [...customTimeFormats, val];
|
||||
setCustomTimeFormats(next);
|
||||
storage.set('customTimeFormats', next);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={editFieldTimeFormat}
|
||||
onChange={(e) => setEditFieldTimeFormat(e.target.value)}
|
||||
onFocus={() => setFormatDropdownOpen(true)}
|
||||
onBlur={() => {
|
||||
setTimeout(() => setFormatDropdownOpen(false), 200);
|
||||
const val = editFieldTimeFormat.trim();
|
||||
if (val && !customTimeFormats.includes(val)) {
|
||||
const next = [...customTimeFormats, val];
|
||||
setCustomTimeFormats(next);
|
||||
storage.set('customTimeFormats', next);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="w-full px-1.5 py-1 text-xs border border-border rounded"
|
||||
placeholder="输入格式,如 YYYY-MM-DD"
|
||||
/>
|
||||
<datalist id={`edit-format-list-${field.key}`}>
|
||||
{customTimeFormats
|
||||
.filter(fmt => {
|
||||
const isDateFormat = /YYYY|MM|DD/.test(fmt);
|
||||
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
|
||||
if (field.type === 'date') return isDateFormat;
|
||||
if (field.type === 'time') return isTimeFormat;
|
||||
return true;
|
||||
})
|
||||
.map(fmt => <option key={fmt} value={fmt} />)}
|
||||
</datalist>
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = editFieldTimeFormat.trim();
|
||||
if (val && !customTimeFormats.includes(val)) {
|
||||
const next = [...customTimeFormats, val];
|
||||
setCustomTimeFormats(next);
|
||||
storage.set('customTimeFormats', next);
|
||||
}
|
||||
setFormatDropdownOpen(false);
|
||||
}
|
||||
}}
|
||||
className="w-full px-1.5 py-1 text-xs border border-border rounded"
|
||||
placeholder="输入格式或下拉选择"
|
||||
/>
|
||||
{formatDropdownOpen && (
|
||||
<div className="absolute z-10 left-0 right-0 top-full mt-1 bg-white border border-border rounded shadow-lg max-h-32 overflow-y-auto">
|
||||
{customTimeFormats
|
||||
.filter(fmt => {
|
||||
const isDateFormat = /YYYY|MM|DD/.test(fmt);
|
||||
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
|
||||
if (field.type === 'date') return isDateFormat;
|
||||
if (field.type === 'time') return isTimeFormat;
|
||||
return true;
|
||||
})
|
||||
.map(fmt => (
|
||||
<div
|
||||
key={fmt}
|
||||
className="px-2 py-1 text-xs hover:bg-slate-100 cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
setEditFieldTimeFormat(fmt);
|
||||
setFormatDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{fmt}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
@@ -1101,42 +1143,61 @@ export default function TemplateManage() {
|
||||
className="w-full px-2 py-1.5 text-xs border border-border rounded"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
list="new-format-list"
|
||||
value={newFieldTimeFormat}
|
||||
onChange={(e) => setNewFieldTimeFormat(e.target.value)}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value.trim();
|
||||
if (val && !customTimeFormats.includes(val)) {
|
||||
const next = [...customTimeFormats, val];
|
||||
setCustomTimeFormats(next);
|
||||
storage.set('customTimeFormats', next);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = (e.target as HTMLInputElement).value.trim();
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={newFieldTimeFormat}
|
||||
onChange={(e) => setNewFieldTimeFormat(e.target.value)}
|
||||
onFocus={() => setNewFormatDropdownOpen(true)}
|
||||
onBlur={() => {
|
||||
setTimeout(() => setNewFormatDropdownOpen(false), 200);
|
||||
const val = newFieldTimeFormat.trim();
|
||||
if (val && !customTimeFormats.includes(val)) {
|
||||
const next = [...customTimeFormats, val];
|
||||
setCustomTimeFormats(next);
|
||||
storage.set('customTimeFormats', next);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="w-full px-2 py-1.5 text-xs border border-border rounded"
|
||||
placeholder="输入格式,如 YYYY-MM-DD"
|
||||
/>
|
||||
<datalist id="new-format-list">
|
||||
{customTimeFormats
|
||||
.filter(fmt => {
|
||||
const isDateFormat = /YYYY|MM|DD/.test(fmt);
|
||||
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
|
||||
if (newFieldForm.type === 'date') return isDateFormat;
|
||||
if (newFieldForm.type === 'time') return isTimeFormat;
|
||||
return true;
|
||||
})
|
||||
.map(fmt => <option key={fmt} value={fmt} />)}
|
||||
</datalist>
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const val = newFieldTimeFormat.trim();
|
||||
if (val && !customTimeFormats.includes(val)) {
|
||||
const next = [...customTimeFormats, val];
|
||||
setCustomTimeFormats(next);
|
||||
storage.set('customTimeFormats', next);
|
||||
}
|
||||
setNewFormatDropdownOpen(false);
|
||||
}
|
||||
}}
|
||||
className="w-full px-2 py-1.5 text-xs border border-border rounded"
|
||||
placeholder="输入格式或下拉选择"
|
||||
/>
|
||||
{newFormatDropdownOpen && (
|
||||
<div className="absolute z-10 left-0 right-0 top-full mt-1 bg-white border border-border rounded shadow-lg max-h-32 overflow-y-auto">
|
||||
{customTimeFormats
|
||||
.filter(fmt => {
|
||||
const isDateFormat = /YYYY|MM|DD/.test(fmt);
|
||||
const isTimeFormat = /HH|hh|mm|A/.test(fmt);
|
||||
if (newFieldForm.type === 'date') return isDateFormat;
|
||||
if (newFieldForm.type === 'time') return isTimeFormat;
|
||||
return true;
|
||||
})
|
||||
.map(fmt => (
|
||||
<div
|
||||
key={fmt}
|
||||
className="px-2 py-1 text-xs hover:bg-slate-100 cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
setNewFieldTimeFormat(fmt);
|
||||
setNewFormatDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
{fmt}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{['单选', '多选'].includes(newFieldForm.category) && (
|
||||
|
||||
@@ -18,10 +18,10 @@ export const printDocument = (htmlContent: string) => {
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page { size: A4; margin: 0; }
|
||||
@page { size: A4; margin: 15mm 10mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; padding: 10mm; font-family: SimSun, "Microsoft YaHei", serif; color: #1E293B; background: white; }
|
||||
.content { width: 190mm; min-height: 277mm; margin: 0 auto; }
|
||||
body { margin: 0; padding: 0; font-family: SimSun, "Microsoft YaHei", serif; color: #1E293B; background: white; }
|
||||
.content { width: 100%; min-height: 277mm; margin: 0 auto; }
|
||||
img { max-width: 100%; height: auto; display: block; margin: 8px auto; }
|
||||
p { margin: 0; padding: 4px 0; line-height: 1.6; }
|
||||
h1 { font-size: 20px; margin: 16px 0 12px; font-weight: 600; text-align: center; }
|
||||
|
||||
Reference in New Issue
Block a user