新增打印模块功能,支持图片分析生成原生模板JSON,查询可用打印机,服务端直打功能,优化打印设计器界面,添加打印机选择和快速打印选项,同时更新依赖项以支持PDF处理。
This commit is contained in:
@@ -0,0 +1,679 @@
|
||||
<template>
|
||||
<div class="free-table-element" :data-free-table-id="element.id">
|
||||
<!-- 选中时左上角四向箭头:与画布整体拖动一致,不在此阻止 pointerdown,事件冒泡到 ElementWrapper -->
|
||||
<button
|
||||
v-if="isElementSelected"
|
||||
type="button"
|
||||
class="free-table-move-handle"
|
||||
title="拖动移动整个表格"
|
||||
aria-label="拖动移动整个表格"
|
||||
@click.stop
|
||||
>
|
||||
<svg
|
||||
class="free-table-move-icon"
|
||||
viewBox="0 0 16 16"
|
||||
width="11"
|
||||
height="11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M8 1.5v3M8 11.5v3M1.5 8h3M11.5 8h3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.35"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 2.35 6.15 4.55h3.7L8 2.35zm0 11.3 1.85-2.2H6.15L8 13.65zM2.35 8l2.2 1.85V6.15L2.35 8zm11.3 0-2.2-1.85v3.7L13.65 8z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="free-table-surface" @pointerdown.stop>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col v-for="(cw, ci) in colWidthsMm" :key="`col_${ci}`" :style="{ width: `${cw}mm` }" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr v-for="r in rowCount" :key="`tr_${r - 1}`" :style="{ height: `${rowHeightsMm[r - 1] ?? 6}mm` }">
|
||||
<td
|
||||
v-for="cell in anchorsForRow(r - 1)"
|
||||
:key="`td_${cell.row}_${cell.col}`"
|
||||
class="free-table-cell"
|
||||
:class="{
|
||||
'is-selected': isAnchorSelected(cell),
|
||||
'is-merge-range': isAnchorInMergeRange(cell),
|
||||
}"
|
||||
:rowspan="Math.max(1, Number(cell.rowspan || 1))"
|
||||
:colspan="Math.max(1, Number(cell.colspan || 1))"
|
||||
:data-ft-row="cell.row"
|
||||
:data-ft-col="cell.col"
|
||||
:style="cellStyle(cell)"
|
||||
@pointerdown.stop="handleSelectCell(cell, $event)"
|
||||
@dblclick.stop="handleCellDblClick(cell)"
|
||||
>
|
||||
<span
|
||||
class="cell-move-handle"
|
||||
title="拖动交换单元格内容"
|
||||
@pointerdown.stop.prevent="startCellSwapDrag($event, cell.row, cell.col)"
|
||||
>
|
||||
<span class="cell-move-handle-icon" aria-hidden="true">≡</span>
|
||||
</span>
|
||||
<span v-if="resolveCellContentType(cell) === 'text'" class="cell-body cell-body--text">{{ resolveCellTextForAnchor(cell) }}</span>
|
||||
<span
|
||||
v-else-if="resolveCellContentType(cell) === 'number' || resolveCellContentType(cell) === 'amount'"
|
||||
class="cell-body cell-body--numeric"
|
||||
>
|
||||
{{ formatFreeCellNumeric(cell) }}
|
||||
</span>
|
||||
<img
|
||||
v-else-if="resolveCellContentType(cell) === 'image'"
|
||||
class="table-media-free"
|
||||
alt=""
|
||||
:src="resolveFreeCellImageSrc(cell)"
|
||||
:style="freeCellMediaStyle(cell, 'image')"
|
||||
/>
|
||||
<img
|
||||
v-else-if="resolveCellContentType(cell) === 'qrcode'"
|
||||
class="table-media-free"
|
||||
alt=""
|
||||
:src="resolveFreeCellQrcodeSrc(cell)"
|
||||
:style="freeCellMediaStyle(cell, 'qrcode')"
|
||||
/>
|
||||
<img
|
||||
v-else-if="resolveCellContentType(cell) === 'barcode'"
|
||||
class="table-media-free"
|
||||
alt=""
|
||||
:src="resolveFreeCellBarcodeSrc(cell)"
|
||||
:style="freeCellMediaStyle(cell, 'barcode')"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="isElementSelected" class="free-table-track-layer" aria-hidden="true">
|
||||
<div
|
||||
v-for="(pct, i) in colGripPositionsPct"
|
||||
:key="`cg_${i}`"
|
||||
class="track-grip track-grip--col"
|
||||
:style="{ left: `${pct}%` }"
|
||||
title="拖动调整列宽"
|
||||
@pointerdown.stop.prevent="onColGripPointerDown(i, $event)"
|
||||
/>
|
||||
<div
|
||||
v-for="(pct, i) in rowGripPositionsPct"
|
||||
:key="`rg_${i}`"
|
||||
class="track-grip track-grip--row"
|
||||
:style="{ top: `${pct}%` }"
|
||||
title="拖动调整行高"
|
||||
@pointerdown.stop.prevent="onRowGripPointerDown(i, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import QRCode from 'qrcode';
|
||||
import { getValueByPath } from '../../core/tableBuilder';
|
||||
import { normalizeFreeTableAnchors } from '../../core/freeTableGrid';
|
||||
import { resolveFreeTableCellBorderSides } from '../../core/freeTableBorders';
|
||||
import { lineStyleKeyToCssBorderStyle, resolveFreeTableCellLineStyleKeys } from '../../core/freeTableLineStyles';
|
||||
import { redistributeColEdge, redistributeRowEdge, resolveFreeTableColWidthsMm, resolveFreeTableRowHeightsMm } from '../../core/freeTableTracks';
|
||||
import type { NativeFreeTableCell, NativeFreeTableElement } from '../../core/types';
|
||||
|
||||
const PX_PER_MM = 3.7795275591;
|
||||
|
||||
const qrCodeCache = ref<Record<string, string>>({});
|
||||
const barcodeCache = ref<Record<string, string>>({});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
element: NativeFreeTableElement;
|
||||
previewData: Record<string, any>;
|
||||
selectedCell?: { row: number; col: number } | null;
|
||||
/** 与 selectedCell 配合:Shift 选取的第二角(网格坐标,左上角锚点) */
|
||||
mergeRangeCorner?: { row: number; col: number } | null;
|
||||
isElementSelected?: boolean;
|
||||
/** 画布缩放,用于把指针位移换算为 mm */
|
||||
scale?: number;
|
||||
}>(),
|
||||
{ scale: 1 },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-cell', payload: { row: number; col: number; shiftKey?: boolean }): void;
|
||||
(e: 'swap-cells', payload: { fromRow: number; fromCol: number; toRow: number; toCol: number }): void;
|
||||
/** 双击单元格:打开仅针对本表的单元格编辑弹窗 */
|
||||
(e: 'edit-cell', payload: { row: number; col: number }): void;
|
||||
/** 列宽/行高变更(mm) */
|
||||
(e: 'update-tracks', payload: { colWidths?: number[]; rowHeights?: number[] }): void;
|
||||
}>();
|
||||
|
||||
const rowCount = computed(() => Math.max(1, Number(props.element?.rowCount || 1)));
|
||||
const colCount = computed(() => Math.max(1, Number(props.element?.colCount || 1)));
|
||||
|
||||
const colWidthsMm = computed(() => resolveFreeTableColWidthsMm(props.element));
|
||||
const rowHeightsMm = computed(() => resolveFreeTableRowHeightsMm(props.element));
|
||||
|
||||
const colGripPositionsPct = computed(() => {
|
||||
const w = Math.max(0.01, Number(props.element?.w) || 0.01);
|
||||
let x = 0;
|
||||
const arr = colWidthsMm.value;
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < arr.length - 1; i += 1) {
|
||||
x += arr[i];
|
||||
out.push((x / w) * 100);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
const rowGripPositionsPct = computed(() => {
|
||||
const h = Math.max(0.01, Number(props.element?.h) || 0.01);
|
||||
let y = 0;
|
||||
const arr = rowHeightsMm.value;
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < arr.length - 1; i += 1) {
|
||||
y += arr[i];
|
||||
out.push((y / h) * 100);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
const anchorsNormalized = computed(() =>
|
||||
normalizeFreeTableAnchors(rowCount.value, colCount.value, props.element?.cells || []),
|
||||
);
|
||||
|
||||
const mergeRect = computed(() => {
|
||||
if (!props.selectedCell || !props.mergeRangeCorner) return null;
|
||||
const a = props.selectedCell;
|
||||
const b = props.mergeRangeCorner;
|
||||
return {
|
||||
r0: Math.min(a.row, b.row),
|
||||
r1: Math.max(a.row, b.row),
|
||||
c0: Math.min(a.col, b.col),
|
||||
c1: Math.max(a.col, b.col),
|
||||
};
|
||||
});
|
||||
|
||||
function anchorsForRow(r: number) {
|
||||
return anchorsNormalized.value.filter((c) => c.row === r).sort((a, b) => a.col - b.col);
|
||||
}
|
||||
|
||||
function resolveCellContentType(cell: NativeFreeTableCell) {
|
||||
return String((cell as any)?.contentType || 'text');
|
||||
}
|
||||
|
||||
function resolveCellRawString(cell: NativeFreeTableCell) {
|
||||
const bindField = String(cell?.bindField || '').trim();
|
||||
if (bindField) {
|
||||
const bound = getValueByPath(props.previewData || {}, bindField);
|
||||
if (bound !== undefined && bound !== null && String(bound).trim()) {
|
||||
return String(bound);
|
||||
}
|
||||
}
|
||||
return String(cell?.text || '').trim();
|
||||
}
|
||||
|
||||
function resolveCellTextForAnchor(cell: NativeFreeTableCell) {
|
||||
const t = resolveCellContentType(cell);
|
||||
if (t === 'number' || t === 'amount') {
|
||||
return formatFreeCellNumeric(cell);
|
||||
}
|
||||
const raw = resolveCellRawString(cell);
|
||||
return raw || ' ';
|
||||
}
|
||||
|
||||
function formatFreeCellNumeric(cell: NativeFreeTableCell) {
|
||||
const raw = resolveCellRawString(cell);
|
||||
const numeric = Number(raw);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return raw || ' ';
|
||||
}
|
||||
const decimals = Math.max(0, Math.min(6, Number((cell as any)?.decimalPlaces ?? 2)));
|
||||
const finalValue =
|
||||
(cell as any)?.roundHalfUp === false
|
||||
? Math.trunc(numeric * 10 ** decimals) / 10 ** decimals
|
||||
: Number(numeric.toFixed(decimals));
|
||||
const formatted = finalValue.toLocaleString(undefined, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
});
|
||||
if (resolveCellContentType(cell) === 'amount') {
|
||||
const symbol = (cell as any)?.amountType === 'USD' ? '$' : (cell as any)?.amountType === 'EUR' ? 'EUR ' : '¥';
|
||||
return `${symbol}${formatted}`;
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function resolveFreeCellImageSrc(cell: NativeFreeTableCell) {
|
||||
const v = resolveCellRawString(cell);
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
return `https://via.placeholder.com/180x80.png?text=${encodeURIComponent('Image')}`;
|
||||
}
|
||||
|
||||
function freeCellMediaStyle(cell: NativeFreeTableCell, type: 'image' | 'qrcode' | 'barcode') {
|
||||
const fillCell = (cell as any)?.fillCell !== false;
|
||||
const scale = Math.max(10, Math.min(100, Number((cell as any)?.contentScale || 100)));
|
||||
const percent = `${scale}%`;
|
||||
const base = {
|
||||
display: 'block',
|
||||
margin: '0 auto',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
objectFit: (type === 'image' ? (cell as any)?.imageFit || 'contain' : 'contain') as string,
|
||||
} as Record<string, any>;
|
||||
if (fillCell) {
|
||||
base.width = '100%';
|
||||
base.height = '100%';
|
||||
} else {
|
||||
base.width = percent;
|
||||
base.height = type === 'barcode' ? `${Math.max(20, scale * 0.6)}%` : percent;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function resolveFreeCellQrcodeSrc(cell: NativeFreeTableCell) {
|
||||
const value = String(resolveCellRawString(cell) || 'qrcode_empty');
|
||||
const key = `${(cell as any)?.qrLevel || 'M'}|${(cell as any)?.qrRenderType || 'image/png'}|${value}`;
|
||||
if (qrCodeCache.value[key]) {
|
||||
return qrCodeCache.value[key];
|
||||
}
|
||||
void QRCode.toDataURL(value, {
|
||||
errorCorrectionLevel: (cell as any)?.qrLevel || 'M',
|
||||
margin: 0,
|
||||
type: (cell as any)?.qrRenderType || 'image/png',
|
||||
width: 200,
|
||||
})
|
||||
.then((url) => {
|
||||
qrCodeCache.value = { ...qrCodeCache.value, [key]: url };
|
||||
})
|
||||
.catch(() => {});
|
||||
return '';
|
||||
}
|
||||
|
||||
async function buildBarcodeDataUrl(value: string, format: string) {
|
||||
const mod: any = await import('jsbarcode');
|
||||
const JsBarcode = mod.default || mod;
|
||||
const canvas = document.createElement('canvas');
|
||||
JsBarcode(canvas, value || '00000000', {
|
||||
format: format || 'CODE128',
|
||||
displayValue: false,
|
||||
margin: 0,
|
||||
width: 2,
|
||||
height: 70,
|
||||
});
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
function resolveFreeCellBarcodeSrc(cell: NativeFreeTableCell) {
|
||||
const value = String(resolveCellRawString(cell) || '00000000');
|
||||
const format = String((cell as any)?.barcodeFormat || 'CODE128');
|
||||
const key = `${format}|${value}`;
|
||||
if (barcodeCache.value[key]) {
|
||||
return barcodeCache.value[key];
|
||||
}
|
||||
void buildBarcodeDataUrl(value, format)
|
||||
.then((url) => {
|
||||
barcodeCache.value = { ...barcodeCache.value, [key]: url };
|
||||
})
|
||||
.catch(() => {});
|
||||
return '';
|
||||
}
|
||||
|
||||
function cellStyle(cell: NativeFreeTableCell) {
|
||||
const rs = Math.max(1, Number(cell?.rowspan || 1));
|
||||
const cs = Math.max(1, Number(cell?.colspan || 1));
|
||||
const bw = Math.max(1, Number(props.element?.borderWidth || 1));
|
||||
const bc = props.element?.borderColor || '#d9d9d9';
|
||||
const sides = resolveFreeTableCellBorderSides(
|
||||
props.element,
|
||||
anchorsNormalized.value,
|
||||
cell,
|
||||
cell.row,
|
||||
cell.col,
|
||||
rs,
|
||||
cs,
|
||||
rowCount.value,
|
||||
colCount.value,
|
||||
);
|
||||
const lineKeys = resolveFreeTableCellLineStyleKeys(
|
||||
props.element,
|
||||
cell.row,
|
||||
cell.col,
|
||||
rs,
|
||||
cs,
|
||||
rowCount.value,
|
||||
colCount.value,
|
||||
);
|
||||
const nowrap = (cell as any).autoWrap === false;
|
||||
return {
|
||||
boxSizing: 'border-box',
|
||||
textAlign: cell?.align || 'left',
|
||||
verticalAlign: cell?.verticalAlign || 'middle',
|
||||
fontSize: `${Number(cell?.fontSize || 12)}px`,
|
||||
color: cell?.color || '#111111',
|
||||
backgroundColor: cell?.backgroundColor || '#ffffff',
|
||||
whiteSpace: nowrap ? 'nowrap' : 'pre-wrap',
|
||||
wordBreak: nowrap ? 'normal' : 'break-all',
|
||||
borderTop: sides.top ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.top)} ${bc}` : 'none',
|
||||
borderRight: sides.right ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.right)} ${bc}` : 'none',
|
||||
borderBottom: sides.bottom ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.bottom)} ${bc}` : 'none',
|
||||
borderLeft: sides.left ? `${bw}px ${lineStyleKeyToCssBorderStyle(lineKeys.left)} ${bc}` : 'none',
|
||||
};
|
||||
}
|
||||
|
||||
function handleSelectCell(cell: NativeFreeTableCell, e: PointerEvent) {
|
||||
emit('select-cell', {
|
||||
row: cell.row,
|
||||
col: cell.col,
|
||||
shiftKey: e.shiftKey === true,
|
||||
});
|
||||
}
|
||||
|
||||
function handleCellDblClick(cell: NativeFreeTableCell) {
|
||||
emit('edit-cell', { row: cell.row, col: cell.col });
|
||||
}
|
||||
|
||||
function isAnchorSelected(cell: NativeFreeTableCell) {
|
||||
if (!props.isElementSelected || !props.selectedCell) return false;
|
||||
return props.selectedCell.row === cell.row && props.selectedCell.col === cell.col;
|
||||
}
|
||||
|
||||
function isAnchorInMergeRange(cell: NativeFreeTableCell) {
|
||||
const rect = mergeRect.value;
|
||||
if (!rect) return false;
|
||||
const rs = Math.max(1, Number(cell.rowspan || 1));
|
||||
const cs = Math.max(1, Number(cell.colspan || 1));
|
||||
const a0 = cell.row;
|
||||
const a1 = cell.row + rs - 1;
|
||||
const b0 = cell.col;
|
||||
const b1 = cell.col + cs - 1;
|
||||
return !(a1 < rect.r0 || a0 > rect.r1 || b1 < rect.c0 || b0 > rect.c1);
|
||||
}
|
||||
|
||||
function onColGripPointerDown(edge: number, e: PointerEvent) {
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const el = props.element;
|
||||
const base = [...resolveFreeTableColWidthsMm(el)];
|
||||
const totalW = Math.max(0.01, Number(el.w) || 0.01);
|
||||
const startX = e.clientX;
|
||||
const sc = Math.max(0.2, Number(props.scale) || 1);
|
||||
const target = e.currentTarget as HTMLElement | null;
|
||||
try {
|
||||
target?.setPointerCapture?.(e.pointerId);
|
||||
} catch (_err) {
|
||||
/* 忽略 */
|
||||
}
|
||||
const onMove = (ev: PointerEvent) => {
|
||||
const deltaMm = (ev.clientX - startX) / sc / PX_PER_MM;
|
||||
const next = redistributeColEdge(base, edge, deltaMm, totalW);
|
||||
if (next) {
|
||||
emit('update-tracks', { colWidths: next });
|
||||
}
|
||||
};
|
||||
const onUp = (ev: PointerEvent) => {
|
||||
try {
|
||||
target?.releasePointerCapture?.(ev.pointerId);
|
||||
} catch (_err) {
|
||||
/* 忽略 */
|
||||
}
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
window.removeEventListener('pointercancel', onUp);
|
||||
};
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
window.addEventListener('pointercancel', onUp);
|
||||
}
|
||||
|
||||
function onRowGripPointerDown(edge: number, e: PointerEvent) {
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const el = props.element;
|
||||
const base = [...resolveFreeTableRowHeightsMm(el)];
|
||||
const totalH = Math.max(0.01, Number(el.h) || 0.01);
|
||||
const startY = e.clientY;
|
||||
const sc = Math.max(0.2, Number(props.scale) || 1);
|
||||
const target = e.currentTarget as HTMLElement | null;
|
||||
try {
|
||||
target?.setPointerCapture?.(e.pointerId);
|
||||
} catch (_err) {
|
||||
/* 忽略 */
|
||||
}
|
||||
const onMove = (ev: PointerEvent) => {
|
||||
const deltaMm = (ev.clientY - startY) / sc / PX_PER_MM;
|
||||
const next = redistributeRowEdge(base, edge, deltaMm, totalH);
|
||||
if (next) {
|
||||
emit('update-tracks', { rowHeights: next });
|
||||
}
|
||||
};
|
||||
const onUp = (ev: PointerEvent) => {
|
||||
try {
|
||||
target?.releasePointerCapture?.(ev.pointerId);
|
||||
} catch (_err) {
|
||||
/* 忽略 */
|
||||
}
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
window.removeEventListener('pointercancel', onUp);
|
||||
};
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
window.addEventListener('pointercancel', onUp);
|
||||
}
|
||||
|
||||
function startCellSwapDrag(event: PointerEvent, fromRow: number, fromCol: number) {
|
||||
if (event.button !== 0) return;
|
||||
const startId = props.element.id;
|
||||
const onMove = (_e: PointerEvent) => {};
|
||||
const onUp = (up: PointerEvent) => {
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
const top = document.elementFromPoint(up.clientX, up.clientY) as HTMLElement | null;
|
||||
const host = top?.closest?.('[data-free-table-id]') as HTMLElement | null;
|
||||
if (!host || host.getAttribute('data-free-table-id') !== startId) {
|
||||
return;
|
||||
}
|
||||
const td = top?.closest?.('td[data-ft-row]') as HTMLElement | null;
|
||||
if (!td) return;
|
||||
const toRow = Number(td.getAttribute('data-ft-row'));
|
||||
const toCol = Number(td.getAttribute('data-ft-col'));
|
||||
if (!Number.isFinite(toRow) || !Number.isFinite(toCol)) return;
|
||||
if (fromRow === toRow && fromCol === toCol) return;
|
||||
emit('swap-cells', { fromRow, fromCol, toRow, toCol });
|
||||
};
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.free-table-element {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: 1px dashed #999;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.free-table-move-handle {
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: -1px;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #bfbfbf;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #e8eef8 55%, #dce6f5 100%);
|
||||
color: #1f1f1f;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
line-height: 0;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid #1677ff;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.free-table-move-icon {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.free-table-surface {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
/* 设计器内不显示滚动条,与打印区域一致;略溢出时裁切即可 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.free-table-track-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.track-grip {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
z-index: 6;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.track-grip--col {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 10px;
|
||||
margin-left: -5px;
|
||||
cursor: col-resize;
|
||||
background: rgba(22, 119, 255, 0.14);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.track-grip--row {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
margin-top: -5px;
|
||||
cursor: row-resize;
|
||||
background: rgba(22, 119, 255, 0.14);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.track-grip:hover {
|
||||
background: rgba(22, 119, 255, 0.28);
|
||||
}
|
||||
|
||||
.free-table-surface table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.free-table-surface tbody tr {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.free-table-cell {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
padding: 10px 4px 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.free-table-cell.is-merge-range:not(.is-selected) {
|
||||
box-shadow: inset 0 0 0 1px rgba(250, 140, 22, 0.65);
|
||||
background-color: rgba(255, 247, 230, 0.55);
|
||||
}
|
||||
|
||||
.cell-body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.table-media-free {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cell-move-handle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 1px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 12px;
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
background: #1677ff;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
cursor: grab;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
|
||||
transition: opacity 0.12s ease;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-move-handle-icon {
|
||||
display: block;
|
||||
transform: scaleY(0.85);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.free-table-cell:hover .cell-move-handle,
|
||||
.free-table-cell.is-selected .cell-move-handle {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.free-table-cell.is-selected {
|
||||
box-shadow: inset 0 0 0 2px #1677ff;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user