680 lines
21 KiB
Vue
680 lines
21 KiB
Vue
<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>
|