2295 lines
92 KiB
Vue
2295 lines
92 KiB
Vue
<template>
|
||
<div ref="propertiesPanelRootRef" class="properties-panel">
|
||
<div class="properties-panel__header">
|
||
<span class="properties-panel__title">设计栏</span>
|
||
<div class="properties-panel__header-actions">
|
||
<button type="button" class="properties-panel__icon-btn" title="全部折叠" aria-label="全部折叠" @click="collapseAllSections">
|
||
<svg class="properties-panel__icon-svg" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
||
<path fill="currentColor" d="M4 5h16v2H4V5zm0 6h16v2H4v-2zm0 6h9v2H4v-2z" />
|
||
</svg>
|
||
</button>
|
||
<button type="button" class="properties-panel__icon-btn" title="全部展开" aria-label="全部展开" @click="expandAllSections">
|
||
<svg class="properties-panel__icon-svg" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
||
<path fill="currentColor" d="M4 5h16v2H4V5zm0 6h16v2H4v-2zm0 6h16v2H4v-2z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="properties-panel__body">
|
||
<template v-if="selectedElement">
|
||
<details class="section-card" open>
|
||
<summary class="panel-title">元素属性</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-input :value="selectedElement.type" addon-before="类型" disabled />
|
||
<div
|
||
v-if="selectedElement.type !== 'table' && selectedElement.type !== 'detailTable' && selectedElement.type !== 'freeTable'"
|
||
class="bind-param-compact"
|
||
>
|
||
<span class="bind-param-compact__addon">绑定参数</span>
|
||
<a-select
|
||
:value="resolveParamBindSelectValue(selectedElement.bindField)"
|
||
:options="bindingParamOptions"
|
||
allow-clear
|
||
show-search
|
||
option-filter-prop="label"
|
||
placeholder="请先在左侧「参数」页维护"
|
||
class="bind-param-compact__select"
|
||
@update:value="handleElementBindParamChange"
|
||
/>
|
||
</div>
|
||
<a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.x" addon-before="X(mm)" style="width: 100%" @update:value="updateRect('x', $event)" />
|
||
<a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.y" addon-before="Y(mm)" style="width: 100%" @update:value="updateRect('y', $event)" />
|
||
<a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.w" addon-before="宽(mm)" style="width: 100%" :min="6" @update:value="updateRect('w', $event)" />
|
||
<a-input-number v-if="!isReportBand(selectedElement.type)" :value="selectedElement.h" addon-before="高(mm)" style="width: 100%" :min="6" @update:value="updateRect('h', $event)" />
|
||
<a-input-number
|
||
:value="selectedElement.style?.fontSize || 12"
|
||
addon-before="字体(px)"
|
||
:min="8"
|
||
:max="72"
|
||
style="width: 100%"
|
||
@update:value="updateStyle('fontSize', $event)"
|
||
/>
|
||
<template v-if="showPlainTextStyleColors">
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="selectedElement.style?.color || '#111111'"
|
||
addon-before="文字色"
|
||
placeholder="#111111 / rgb(...)"
|
||
@update:value="updateStyle('color', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择文字颜色"
|
||
:value="toColorHex(String(selectedElement.style?.color || '#111111'))"
|
||
@input="handleStyleColorInput('color', $event, '#111111')"
|
||
/>
|
||
</div>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="String(selectedElement.style?.backgroundColor ?? 'transparent')"
|
||
addon-before="背景色"
|
||
placeholder="transparent / #ffffff"
|
||
@update:value="updateStyle('backgroundColor', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择背景颜色"
|
||
:value="toColorHex(plainTextBackgroundForPicker)"
|
||
@input="handlePlainTextBackgroundColorInput($event)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</a-space>
|
||
</details>
|
||
<template v-if="isText(selectedElement.type)">
|
||
<a-divider />
|
||
<a-input :value="(selectedElement as any).text" addon-before="内容" @update:value="updateField('text', $event)" />
|
||
</template>
|
||
<template v-if="isReportBand(selectedElement.type)">
|
||
<a-divider />
|
||
<div class="panel-subtitle">布局</div>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-input-number :value="selectedElement.h" addon-before="高度" :min="4" :max="200" style="width: 100%" @update:value="updateRect('h', $event)" />
|
||
</a-space>
|
||
<div class="panel-subtitle" style="margin-top: 8px">外观</div>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="selectedElement.style?.backgroundColor || '#ffffff'"
|
||
addon-before="背景色"
|
||
placeholder="#ffffff / rgb(...)"
|
||
@update:value="updateStyle('backgroundColor', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择背景颜色"
|
||
:value="toColorHex(selectedElement.style?.backgroundColor || '#ffffff')"
|
||
@input="handleStyleColorInput('backgroundColor', $event, '#ffffff')"
|
||
/>
|
||
</div>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="selectedElement.style?.color || '#111111'"
|
||
addon-before="文字色"
|
||
placeholder="#111111 / rgb(...)"
|
||
@update:value="updateStyle('color', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择文字颜色"
|
||
:value="toColorHex(String(selectedElement.style?.color || '#111111'))"
|
||
@input="handleStyleColorInput('color', $event, '#111111')"
|
||
/>
|
||
</div>
|
||
<div class="panel-subtitle" style="margin-top: 8px">数据</div>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-input :value="(selectedElement as any).bookmarkText || ''" addon-before="书签文本" @update:value="updateField('bookmarkText', $event)" />
|
||
</a-space>
|
||
<div class="panel-subtitle" style="margin-top: 8px">行为</div>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-switch :checked="(selectedElement as any).keepTogether !== false" checked-children="保持同页" un-checked-children="允许拆分" @update:checked="updateField('keepTogether', $event)" />
|
||
<a-switch :checked="(selectedElement as any).centerWithDetail !== false" checked-children="跟随明细居中" un-checked-children="不居中" @update:checked="updateField('centerWithDetail', $event)" />
|
||
<a-select
|
||
:value="(selectedElement as any).refreshPage || 'none'"
|
||
:options="REFRESH_PAGE_OPTIONS"
|
||
style="width: 100%"
|
||
@update:value="updateField('refreshPage', $event)"
|
||
/>
|
||
<a-switch :checked="(selectedElement as any).visible !== false" checked-children="可见" un-checked-children="隐藏" @update:checked="updateField('visible', $event)" />
|
||
<a-switch :checked="(selectedElement as any).stretch === true" checked-children="可伸展" un-checked-children="不可伸展" @update:checked="updateField('stretch', $event)" />
|
||
<a-switch :checked="(selectedElement as any).shrink === true" checked-children="可收缩" un-checked-children="不可收缩" @update:checked="updateField('shrink', $event)" />
|
||
<a-switch :checked="(selectedElement as any).printRepeated === true" checked-children="每页重复打印" un-checked-children="不重复" @update:checked="updateField('printRepeated', $event)" />
|
||
<a-switch
|
||
v-if="isReportFooter(selectedElement.type)"
|
||
:checked="(selectedElement as any).printAtPageBottom === true"
|
||
checked-children="打印在页底"
|
||
un-checked-children="常规位置"
|
||
@update:checked="updateField('printAtPageBottom', $event)"
|
||
/>
|
||
<a-switch
|
||
v-if="isReportFooter(selectedElement.type)"
|
||
:checked="(selectedElement as any).removeBlankWhenNoData === true"
|
||
checked-children="空白行排除"
|
||
un-checked-children="保留空白"
|
||
@update:checked="updateField('removeBlankWhenNoData', $event)"
|
||
/>
|
||
</a-space>
|
||
</template>
|
||
<template v-if="selectedElement.type === 'image'">
|
||
<a-divider />
|
||
<a-input :value="(selectedElement as any).src" addon-before="图片URL" @update:value="updateField('src', $event)" />
|
||
</template>
|
||
<template v-if="selectedElement.type === 'qrcode' || selectedElement.type === 'barcode'">
|
||
<a-divider />
|
||
<a-input :value="(selectedElement as any).value" addon-before="编码值" @update:value="updateField('value', $event)" />
|
||
</template>
|
||
<template v-if="selectedElement.type === 'table' || selectedElement.type === 'detailTable'">
|
||
<a-divider />
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">表格配置</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<div class="bind-param-compact">
|
||
<span class="bind-param-compact__addon">数据源</span>
|
||
<a-select
|
||
:value="resolveTableSourceSelectValue((selectedElement as any).source)"
|
||
:options="detailTableSourceOptions"
|
||
allow-clear
|
||
show-search
|
||
option-filter-prop="label"
|
||
placeholder="请选择左侧「字段」页登记的明细数据源"
|
||
class="bind-param-compact__select"
|
||
@update:value="handleTableSourceChange"
|
||
/>
|
||
</div>
|
||
<a-select
|
||
:value="(selectedElement as any).tableHeightMode || 'autoPage'"
|
||
style="width: 100%"
|
||
:options="TABLE_HEIGHT_MODE_OPTIONS"
|
||
@update:value="updateField('tableHeightMode', $event)"
|
||
/>
|
||
<a-input-number
|
||
v-if="((selectedElement as any).tableHeightMode || 'autoPage') === 'fixedRows'"
|
||
:value="(selectedElement as any).fixedRows || 5"
|
||
addon-before="分页行数"
|
||
:min="1"
|
||
:max="500"
|
||
style="width: 100%"
|
||
@update:value="updateField('fixedRows', Number($event || 5))"
|
||
/>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">分组合并</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-select
|
||
mode="multiple"
|
||
:value="(selectedElement as any).mergeColumnKeys || []"
|
||
:options="mergeColumnOptions"
|
||
placeholder="选择分组列(顺序决定优先级)"
|
||
style="width: 100%"
|
||
@update:value="updateMergeColumns($event)"
|
||
/>
|
||
<a-switch
|
||
:checked="(selectedElement as any).strictGrouping !== false"
|
||
checked-children="强制分组"
|
||
un-checked-children="宽松分组"
|
||
@update:checked="updateField('strictGrouping', $event)"
|
||
/>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">表头</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-switch
|
||
:checked="(selectedElement as any).showHeader"
|
||
checked-children="显示表头"
|
||
un-checked-children="隐藏表头"
|
||
@update:checked="updateField('showHeader', $event)"
|
||
/>
|
||
<a-input-number :value="(selectedElement as any).headerHeight" addon-before="表头高(mm)" style="width: 100%" @update:value="updateField('headerHeight', $event)" />
|
||
<a-input-number :value="(selectedElement as any).headerFontSize || 12" addon-before="表头字号(px)" :min="8" :max="72" style="width: 100%" @update:value="updateField('headerFontSize', Number($event || 12))" />
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="(selectedElement as any).headerBgColor || '#f5f5f5'"
|
||
addon-before="表头背景"
|
||
placeholder="#f5f5f5 / rgb(...)"
|
||
@update:value="updateField('headerBgColor', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择表头背景色"
|
||
:value="toColorHex((selectedElement as any).headerBgColor || '#f5f5f5')"
|
||
@input="handleColorInput('headerBgColor', $event, '#f5f5f5')"
|
||
/>
|
||
</div>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="(selectedElement as any).headerTextColor || '#111111'"
|
||
addon-before="表头文字"
|
||
placeholder="#111111 / rgb(...)"
|
||
@update:value="updateField('headerTextColor', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择表头文字色"
|
||
:value="toColorHex((selectedElement as any).headerTextColor || '#111111')"
|
||
@input="handleColorInput('headerTextColor', $event, '#111111')"
|
||
/>
|
||
</div>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">表体</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-input-number :value="(selectedElement as any).rowHeight" addon-before="表体行高(mm)" style="width: 100%" @update:value="updateField('rowHeight', $event)" />
|
||
<a-input-number :value="(selectedElement as any).bodyFontSize || 12" addon-before="表体字号(px)" :min="8" :max="72" style="width: 100%" @update:value="updateField('bodyFontSize', Number($event || 12))" />
|
||
<a-button type="primary" block class="column-width-entry-btn" @click="columnWidthModalOpen = true">打开绑定字段列宽设置</a-button>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">底部</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-switch
|
||
:checked="(selectedElement as any).footerShowTotal !== false"
|
||
checked-children="显示底部合计"
|
||
un-checked-children="隐藏底部合计"
|
||
@update:checked="updateField('footerShowTotal', $event)"
|
||
/>
|
||
<a-select
|
||
:value="(selectedElement as any).footerTotalMode || 'overall'"
|
||
style="width: 100%"
|
||
:options="FOOTER_TOTAL_MODE_OPTIONS"
|
||
@update:value="updateField('footerTotalMode', $event)"
|
||
/>
|
||
<a-select
|
||
:value="resolveFooterLabelColumnKey()"
|
||
style="width: 100%"
|
||
:options="footerLabelColumnOptions"
|
||
@update:value="updateField('footerLabelColumnKey', $event)"
|
||
/>
|
||
<a-input
|
||
:value="(selectedElement as any).footerLabelText || '合计'"
|
||
addon-before="底部文字"
|
||
@update:value="updateField('footerLabelText', $event)"
|
||
/>
|
||
<a-switch
|
||
:checked="(selectedElement as any).footerLabelCenter !== false"
|
||
checked-children="底部居中"
|
||
un-checked-children="底部左对齐"
|
||
@update:checked="updateField('footerLabelCenter', $event)"
|
||
/>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">多级表头</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<a-switch
|
||
:checked="(selectedElement as any).enableMultiHeader === true"
|
||
checked-children="已开启"
|
||
un-checked-children="未开启"
|
||
@update:checked="handleMultiHeaderToggle($event)"
|
||
/>
|
||
<a-button type="primary" ghost :disabled="(selectedElement as any).enableMultiHeader !== true" @click="headerConfigModalOpen = true">
|
||
打开多级表头设置
|
||
</a-button>
|
||
</a-space>
|
||
</details>
|
||
<a-modal
|
||
v-model:open="headerConfigModalOpen"
|
||
title="多级表头设置"
|
||
:width="900"
|
||
:footer="null"
|
||
destroy-on-close
|
||
>
|
||
<TableHeaderConfigEditor
|
||
:row-count="resolveHeaderConfigRowCount()"
|
||
:col-count="tableColumns.length || 1"
|
||
:column-titles="tableColumns.map((item) => item?.title || item?.key || '')"
|
||
:value="(selectedElement as any).headerConfig"
|
||
@update:value="handleHeaderConfigChange($event)"
|
||
/>
|
||
</a-modal>
|
||
<a-modal
|
||
v-model:open="columnWidthModalOpen"
|
||
title="绑定字段列宽设置"
|
||
:width="760"
|
||
:footer="null"
|
||
destroy-on-close
|
||
>
|
||
<div class="column-width-modal">
|
||
<div class="column-width-modal-tip">按绑定字段分别设置列宽,修改后会实时同步到画布。</div>
|
||
<div class="column-width-list">
|
||
<div v-for="(col, idx) in tableColumns" :key="`width_${col.key}`" class="column-width-item">
|
||
<div class="column-width-item-head">
|
||
<span class="column-width-index">列{{ idx + 1 }}</span>
|
||
<span class="column-width-title">{{ col.title || col.key }}</span>
|
||
</div>
|
||
<a-input :value="col.bindField || col.field || col.key" addon-before="字段" disabled />
|
||
<a-input-number
|
||
:value="col.width"
|
||
addon-before="列宽"
|
||
addon-after="mm"
|
||
:min="10"
|
||
:max="500"
|
||
style="width: 100%"
|
||
@update:value="updateTableColumn(idx, 'width', Number($event || 10))"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="column-width-modal-footer">
|
||
<a-button @click="columnWidthModalOpen = false">关闭</a-button>
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">当前列属性(双击表头选择列)</summary>
|
||
<div v-if="selectedTableColumnInfo" class="table-column-list">
|
||
<div class="table-column-item">
|
||
<a-space direction="vertical" style="width: 100%" size="small">
|
||
<a-input
|
||
:value="selectedTableColumnInfo.col.title"
|
||
addon-before="列标题"
|
||
@update:value="updateSelectedTableColumn('title', $event)"
|
||
/>
|
||
<div class="bind-param-compact">
|
||
<span class="bind-param-compact__addon">绑定字段</span>
|
||
<a-select
|
||
:value="resolveDetailFieldBindSelectValue(selectedTableColumnInfo.col.bindField)"
|
||
:options="detailFieldBindOptionsForColumn"
|
||
allow-clear
|
||
show-search
|
||
option-filter-prop="label"
|
||
placeholder="请选择当前数据源下的字段"
|
||
class="bind-param-compact__select"
|
||
@update:value="handleTableColumnBindDetailFieldChange"
|
||
/>
|
||
</div>
|
||
<a-input-number
|
||
:value="selectedTableColumnInfo.col.width"
|
||
addon-before="列宽"
|
||
:min="10"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedTableColumn('width', Number($event || 10))"
|
||
/>
|
||
<a-select
|
||
:value="selectedTableColumnInfo.col.contentType || 'text'"
|
||
style="width: 100%"
|
||
:options="TABLE_COLUMN_CONTENT_TYPE_OPTIONS"
|
||
@update:value="handleColumnContentTypeChange($event)"
|
||
/>
|
||
<template v-if="(selectedTableColumnInfo.col.contentType || 'text') === 'text'">
|
||
<a-select
|
||
:value="selectedTableColumnInfo.col.fontFamily || ''"
|
||
style="width: 100%"
|
||
:options="TABLE_FONT_OPTIONS"
|
||
@update:value="updateSelectedTableColumn('fontFamily', $event)"
|
||
/>
|
||
<a-switch
|
||
:checked="!!selectedTableColumnInfo.col.useCustomFontSize"
|
||
checked-children="单列字号"
|
||
un-checked-children="跟随表体字号"
|
||
@update:checked="updateSelectedTableColumn('useCustomFontSize', $event)"
|
||
/>
|
||
<a-input-number
|
||
:value="selectedTableColumnInfo.col.fontSize || 12"
|
||
addon-before="字体大小"
|
||
:min="8"
|
||
:max="72"
|
||
style="width: 100%"
|
||
:disabled="!selectedTableColumnInfo.col.useCustomFontSize"
|
||
@update:value="updateSelectedTableColumn('fontSize', Number($event || 12))"
|
||
/>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="selectedTableColumnInfo.col.fontColor || '#111111'"
|
||
addon-before="字体颜色"
|
||
placeholder="#111111 / rgb(...)"
|
||
@update:value="updateSelectedTableColumn('fontColor', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择字体颜色"
|
||
:value="toColorHex(selectedTableColumnInfo.col.fontColor || '#111111')"
|
||
@input="handleColumnColorInput($event)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'image'">
|
||
<a-select
|
||
:value="selectedTableColumnInfo.col.imageFit || 'contain'"
|
||
style="width: 100%"
|
||
:options="IMAGE_FIT_OPTIONS"
|
||
@update:value="updateSelectedTableColumn('imageFit', $event)"
|
||
/>
|
||
<a-input-number
|
||
:value="selectedTableColumnInfo.col.contentScale || 100"
|
||
addon-before="图片缩放(%)"
|
||
:min="10"
|
||
:max="100"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedTableColumn('contentScale', Number($event || 100))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedTableColumnInfo.col.fillCell !== false"
|
||
checked-children="填满单元格"
|
||
un-checked-children="按缩放显示"
|
||
@update:checked="updateSelectedTableColumn('fillCell', $event)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'qrcode'">
|
||
<a-select
|
||
:value="selectedTableColumnInfo.col.qrLevel || 'M'"
|
||
style="width: 100%"
|
||
:options="QRCODE_LEVEL_OPTIONS"
|
||
@update:value="updateSelectedTableColumn('qrLevel', $event)"
|
||
/>
|
||
<a-select
|
||
:value="selectedTableColumnInfo.col.qrRenderType || 'image/png'"
|
||
style="width: 100%"
|
||
:options="QRCODE_RENDER_OPTIONS"
|
||
@update:value="updateSelectedTableColumn('qrRenderType', $event)"
|
||
/>
|
||
<a-input-number
|
||
:value="selectedTableColumnInfo.col.contentScale || 100"
|
||
addon-before="二维码缩放(%)"
|
||
:min="10"
|
||
:max="100"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedTableColumn('contentScale', Number($event || 100))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedTableColumnInfo.col.fillCell !== false"
|
||
checked-children="填满单元格"
|
||
un-checked-children="按缩放显示"
|
||
@update:checked="updateSelectedTableColumn('fillCell', $event)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'barcode'">
|
||
<a-select
|
||
:value="selectedTableColumnInfo.col.barcodeFormat || 'CODE128'"
|
||
style="width: 100%"
|
||
:options="BARCODE_FORMAT_OPTIONS"
|
||
@update:value="updateSelectedTableColumn('barcodeFormat', $event)"
|
||
/>
|
||
<a-input-number
|
||
:value="selectedTableColumnInfo.col.contentScale || 100"
|
||
addon-before="条码缩放(%)"
|
||
:min="10"
|
||
:max="100"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedTableColumn('contentScale', Number($event || 100))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedTableColumnInfo.col.fillCell !== false"
|
||
checked-children="填满单元格"
|
||
un-checked-children="按缩放显示"
|
||
@update:checked="updateSelectedTableColumn('fillCell', $event)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="(selectedTableColumnInfo.col.contentType || 'text') === 'number' || (selectedTableColumnInfo.col.contentType || 'text') === 'amount'">
|
||
<a-input-number
|
||
:value="selectedTableColumnInfo.col.decimalPlaces ?? 2"
|
||
addon-before="小数位数"
|
||
:min="0"
|
||
:max="6"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedTableColumn('decimalPlaces', Number($event ?? 2))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedTableColumnInfo.col.roundHalfUp !== false"
|
||
checked-children="四舍五入"
|
||
un-checked-children="截断"
|
||
@update:checked="updateSelectedTableColumn('roundHalfUp', $event)"
|
||
/>
|
||
<a-select
|
||
v-if="(selectedTableColumnInfo.col.contentType || 'text') === 'amount'"
|
||
:value="selectedTableColumnInfo.col.amountType || 'CNY'"
|
||
style="width: 100%"
|
||
:options="AMOUNT_TYPE_OPTIONS"
|
||
@update:value="updateSelectedTableColumn('amountType', $event)"
|
||
/>
|
||
<a-switch
|
||
:checked="!!selectedTableColumnInfo.col.enableFooterTotal"
|
||
checked-children="参与底部合计"
|
||
un-checked-children="不参与合计"
|
||
@update:checked="updateSelectedTableColumn('enableFooterTotal', $event)"
|
||
/>
|
||
</template>
|
||
<a-select
|
||
:value="selectedTableColumnInfo.col.align || 'left'"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedTableColumn('align', $event)"
|
||
>
|
||
<a-select-option value="left">左对齐</a-select-option>
|
||
<a-select-option value="center">居中</a-select-option>
|
||
<a-select-option value="right">右对齐</a-select-option>
|
||
</a-select>
|
||
<a-space v-if="(selectedTableColumnInfo.col.contentType || 'text') === 'text'">
|
||
<a-switch
|
||
:checked="selectedTableColumnInfo.col.autoWrap !== false"
|
||
checked-children="自动换行"
|
||
un-checked-children="不换行"
|
||
@update:checked="updateSelectedTableColumn('autoWrap', $event)"
|
||
/>
|
||
<a-switch
|
||
:checked="!!selectedTableColumnInfo.col.autoFitFont"
|
||
checked-children="自动适配字号"
|
||
un-checked-children="固定字号"
|
||
@update:checked="updateSelectedTableColumn('autoFitFont', $event)"
|
||
/>
|
||
</a-space>
|
||
<a-button danger size="small" @click="removeSelectedTableColumn">删除当前列</a-button>
|
||
</a-space>
|
||
</div>
|
||
<a-button block size="small" type="dashed" @click="addTableColumn">新增列</a-button>
|
||
</div>
|
||
<a-empty v-else description="请先在画布中双击表格表头选择列" />
|
||
</details>
|
||
</template>
|
||
<template v-if="selectedElement.type === 'freeTable'">
|
||
<a-divider />
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">自由表格</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<div class="free-table-merge-tip">合并:先点击起始格,再按住 Shift 点击结束格,然后点「合并选中区域」。</div>
|
||
<div class="free-table-dim-row">
|
||
<div class="free-table-dim-item">
|
||
<span class="free-table-dim-label">行数</span>
|
||
<a-input-number
|
||
:value="(selectedElement as any).rowCount || 1"
|
||
:min="1"
|
||
:max="100"
|
||
size="small"
|
||
disabled
|
||
/>
|
||
</div>
|
||
<div class="free-table-dim-item">
|
||
<span class="free-table-dim-label">列数</span>
|
||
<a-input-number
|
||
:value="(selectedElement as any).colCount || 1"
|
||
:min="1"
|
||
:max="50"
|
||
size="small"
|
||
disabled
|
||
/>
|
||
</div>
|
||
</div>
|
||
<a-space>
|
||
<a-button size="small" @click="addFreeTableRow">新增行</a-button>
|
||
<a-button size="small" @click="removeFreeTableRow">删除行</a-button>
|
||
<a-button size="small" @click="addFreeTableCol">新增列</a-button>
|
||
<a-button size="small" @click="removeFreeTableCol">删除列</a-button>
|
||
</a-space>
|
||
<div class="free-table-track-head">单元格合并</div>
|
||
<a-button type="primary" size="small" block :disabled="!canMergeFreeTableSelection" @click="mergeFreeTableSelection">合并选中区域</a-button>
|
||
<a-button size="small" block :disabled="!canSplitFreeTableMerged" @click="splitFreeTableMerged">拆分当前合并</a-button>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">表格样式</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<div class="free-table-merge-tip">外边框控制整张表最外一圈;内边框控制行间横线、列间竖线。选中单元格后可在下方单独隐藏该格某一侧边框。</div>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="(selectedElement as any).borderColor || '#d9d9d9'"
|
||
addon-before="边框色"
|
||
placeholder="#d9d9d9"
|
||
@update:value="updateField('borderColor', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择边框颜色"
|
||
:value="toColorHex((selectedElement as any).borderColor || '#d9d9d9')"
|
||
@input="handleColorInput('borderColor', $event, '#d9d9d9')"
|
||
/>
|
||
</div>
|
||
<a-input-number
|
||
:value="(selectedElement as any).borderWidth || 1"
|
||
addon-before="边框宽(px)"
|
||
:min="1"
|
||
:max="6"
|
||
style="width: 100%"
|
||
@update:value="updateField('borderWidth', Number($event || 1))"
|
||
/>
|
||
<div class="bind-param-compact">
|
||
<span class="bind-param-compact__addon">外框线型</span>
|
||
<a-select
|
||
:value="(selectedElement as any).outerBorderLineStyle || 'solid'"
|
||
class="bind-param-compact__select"
|
||
@update:value="updateField('outerBorderLineStyle', $event)"
|
||
>
|
||
<a-select-option
|
||
v-for="opt in FREE_TABLE_LINE_STYLE_OPTIONS"
|
||
:key="`ft_os_${opt.value}`"
|
||
:value="opt.value"
|
||
:label="opt.label"
|
||
>
|
||
<div class="free-table-line-style-option">
|
||
<span :class="['free-table-line-preview', `free-table-line-preview--${opt.value}`]" />
|
||
<span>{{ opt.label }}</span>
|
||
</div>
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="bind-param-compact">
|
||
<span class="bind-param-compact__addon">横线线型</span>
|
||
<a-select
|
||
:value="(selectedElement as any).innerBorderHorizontalLineStyle || 'solid'"
|
||
class="bind-param-compact__select"
|
||
@update:value="updateField('innerBorderHorizontalLineStyle', $event)"
|
||
>
|
||
<a-select-option
|
||
v-for="opt in FREE_TABLE_LINE_STYLE_OPTIONS"
|
||
:key="`ft_hs_${opt.value}`"
|
||
:value="opt.value"
|
||
:label="opt.label"
|
||
>
|
||
<div class="free-table-line-style-option">
|
||
<span :class="['free-table-line-preview', `free-table-line-preview--${opt.value}`]" />
|
||
<span>{{ opt.label }}</span>
|
||
</div>
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="bind-param-compact">
|
||
<span class="bind-param-compact__addon">竖线线型</span>
|
||
<a-select
|
||
:value="(selectedElement as any).innerBorderVerticalLineStyle || 'solid'"
|
||
class="bind-param-compact__select"
|
||
@update:value="updateField('innerBorderVerticalLineStyle', $event)"
|
||
>
|
||
<a-select-option
|
||
v-for="opt in FREE_TABLE_LINE_STYLE_OPTIONS"
|
||
:key="`ft_vs_${opt.value}`"
|
||
:value="opt.value"
|
||
:label="opt.label"
|
||
>
|
||
<div class="free-table-line-style-option">
|
||
<span :class="['free-table-line-preview', `free-table-line-preview--${opt.value}`]" />
|
||
<span>{{ opt.label }}</span>
|
||
</div>
|
||
</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
<div class="free-table-track-head">外边框(显示)</div>
|
||
<div class="free-table-border-switch-grid">
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">上</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="(selectedElement as any).outerBorder?.top !== false"
|
||
checked-children="开"
|
||
un-checked-children="关"
|
||
@update:checked="patchFreeTableOuterSide('top', $event)"
|
||
/>
|
||
</div>
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">右</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="(selectedElement as any).outerBorder?.right !== false"
|
||
checked-children="开"
|
||
un-checked-children="关"
|
||
@update:checked="patchFreeTableOuterSide('right', $event)"
|
||
/>
|
||
</div>
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">下</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="(selectedElement as any).outerBorder?.bottom !== false"
|
||
checked-children="开"
|
||
un-checked-children="关"
|
||
@update:checked="patchFreeTableOuterSide('bottom', $event)"
|
||
/>
|
||
</div>
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">左</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="(selectedElement as any).outerBorder?.left !== false"
|
||
checked-children="开"
|
||
un-checked-children="关"
|
||
@update:checked="patchFreeTableOuterSide('left', $event)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="free-table-track-head">内边框(显示)</div>
|
||
<a-space wrap>
|
||
<a-switch
|
||
:checked="(selectedElement as any).innerBorder?.horizontal !== false"
|
||
checked-children="行间横线"
|
||
un-checked-children="行间横线关"
|
||
@update:checked="patchFreeTableInnerSide('horizontal', $event)"
|
||
/>
|
||
<a-switch
|
||
:checked="(selectedElement as any).innerBorder?.vertical !== false"
|
||
checked-children="列间竖线"
|
||
un-checked-children="列间竖线关"
|
||
@update:checked="patchFreeTableInnerSide('vertical', $event)"
|
||
/>
|
||
</a-space>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">表格设置</summary>
|
||
<a-space direction="vertical" style="width: 100%">
|
||
<div class="free-table-merge-tip">
|
||
列宽总和等于元素宽度、行高总和等于元素高度(单位 mm)。输入某一列/行时会与相邻列/行自动补偿;也可在画布中拖动蓝色分隔线调整。
|
||
</div>
|
||
<a-space wrap>
|
||
<a-button size="small" @click="evenFreeTableColWidths">均分列宽</a-button>
|
||
<a-button size="small" @click="evenFreeTableRowHeights">均分行高</a-button>
|
||
</a-space>
|
||
<div class="free-table-track-head">列宽(mm)</div>
|
||
<div v-for="ci in freeTableColIndices" :key="`ft_cw_${ci}`" class="free-table-track-row">
|
||
<span class="free-table-track-label">列{{ ci + 1 }}</span>
|
||
<a-input-number
|
||
size="small"
|
||
:min="MIN_FREE_TABLE_TRACK_MM"
|
||
:max="freeTableMaxColWidthMm"
|
||
:value="freeTableColWidthsResolved[ci]"
|
||
style="width: 160px"
|
||
@update:value="handleFreeTableColWidthChange(ci, $event)"
|
||
/>
|
||
</div>
|
||
<div class="free-table-track-head">行高(mm)</div>
|
||
<div v-for="ri in freeTableRowIndices" :key="`ft_rh_${ri}`" class="free-table-track-row">
|
||
<span class="free-table-track-label">行{{ ri + 1 }}</span>
|
||
<a-input-number
|
||
size="small"
|
||
:min="MIN_FREE_TABLE_TRACK_MM"
|
||
:max="freeTableMaxRowHeightMm"
|
||
:value="freeTableRowHeightsResolved[ri]"
|
||
style="width: 160px"
|
||
@update:value="handleFreeTableRowHeightChange(ri, $event)"
|
||
/>
|
||
</div>
|
||
</a-space>
|
||
</details>
|
||
<details class="section-card" open>
|
||
<summary class="panel-subtitle">当前单元格(点击画布单元格后编辑)</summary>
|
||
<a-space v-if="selectedFreeTableCellInfo" direction="vertical" style="width: 100%">
|
||
<a-input :value="`R${selectedFreeTableCellInfo.row + 1} C${selectedFreeTableCellInfo.col + 1}`" addon-before="位置" disabled />
|
||
<a-input
|
||
:value="selectedFreeTableCellInfo.cell.text || ''"
|
||
addon-before="文本"
|
||
@update:value="updateSelectedFreeTableCell('text', $event)"
|
||
/>
|
||
<div class="bind-param-compact">
|
||
<span class="bind-param-compact__addon">绑定参数</span>
|
||
<a-select
|
||
:value="resolveParamBindSelectValue(selectedFreeTableCellInfo.cell.bindField)"
|
||
:options="bindingParamOptions"
|
||
allow-clear
|
||
show-search
|
||
option-filter-prop="label"
|
||
placeholder="请先在左侧「参数」页维护"
|
||
class="bind-param-compact__select"
|
||
@update:value="handleFreeTableCellBindParamChange"
|
||
/>
|
||
</div>
|
||
<a-select
|
||
:value="selectedFreeTableCellInfo.cell.align || 'left'"
|
||
:options="FREE_TABLE_ALIGN_OPTIONS"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedFreeTableCell('align', $event)"
|
||
/>
|
||
<a-select
|
||
:value="selectedFreeTableCellInfo.cell.verticalAlign || 'middle'"
|
||
:options="FREE_TABLE_VALIGN_OPTIONS"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedFreeTableCell('verticalAlign', $event)"
|
||
/>
|
||
<div class="bind-param-compact">
|
||
<span class="bind-param-compact__addon">数据类型</span>
|
||
<a-select
|
||
:value="selectedFreeTableCellInfo.cell.contentType || 'text'"
|
||
:options="TABLE_COLUMN_CONTENT_TYPE_OPTIONS"
|
||
class="bind-param-compact__select"
|
||
@update:value="updateSelectedFreeTableCell('contentType', $event)"
|
||
/>
|
||
</div>
|
||
<template v-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'image'">
|
||
<a-select
|
||
:value="selectedFreeTableCellInfo.cell.imageFit || 'contain'"
|
||
style="width: 100%"
|
||
:options="IMAGE_FIT_OPTIONS"
|
||
@update:value="updateSelectedFreeTableCell('imageFit', $event)"
|
||
/>
|
||
<a-input-number
|
||
:value="selectedFreeTableCellInfo.cell.contentScale ?? 100"
|
||
addon-before="图片缩放(%)"
|
||
:min="10"
|
||
:max="100"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedFreeTableCell('contentScale', Number($event ?? 100))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedFreeTableCellInfo.cell.fillCell !== false"
|
||
checked-children="填满单元格"
|
||
un-checked-children="按缩放显示"
|
||
@update:checked="updateSelectedFreeTableCell('fillCell', $event)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'qrcode'">
|
||
<a-select
|
||
:value="selectedFreeTableCellInfo.cell.qrLevel || 'M'"
|
||
style="width: 100%"
|
||
:options="QRCODE_LEVEL_OPTIONS"
|
||
@update:value="updateSelectedFreeTableCell('qrLevel', $event)"
|
||
/>
|
||
<a-select
|
||
:value="selectedFreeTableCellInfo.cell.qrRenderType || 'image/png'"
|
||
style="width: 100%"
|
||
:options="QRCODE_RENDER_OPTIONS"
|
||
@update:value="updateSelectedFreeTableCell('qrRenderType', $event)"
|
||
/>
|
||
<a-input-number
|
||
:value="selectedFreeTableCellInfo.cell.contentScale ?? 100"
|
||
addon-before="二维码缩放(%)"
|
||
:min="10"
|
||
:max="100"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedFreeTableCell('contentScale', Number($event ?? 100))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedFreeTableCellInfo.cell.fillCell !== false"
|
||
checked-children="填满单元格"
|
||
un-checked-children="按缩放显示"
|
||
@update:checked="updateSelectedFreeTableCell('fillCell', $event)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'barcode'">
|
||
<a-select
|
||
:value="selectedFreeTableCellInfo.cell.barcodeFormat || 'CODE128'"
|
||
style="width: 100%"
|
||
:options="BARCODE_FORMAT_OPTIONS"
|
||
@update:value="updateSelectedFreeTableCell('barcodeFormat', $event)"
|
||
/>
|
||
<a-input-number
|
||
:value="selectedFreeTableCellInfo.cell.contentScale ?? 100"
|
||
addon-before="条码缩放(%)"
|
||
:min="10"
|
||
:max="100"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedFreeTableCell('contentScale', Number($event ?? 100))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedFreeTableCellInfo.cell.fillCell !== false"
|
||
checked-children="填满单元格"
|
||
un-checked-children="按缩放显示"
|
||
@update:checked="updateSelectedFreeTableCell('fillCell', $event)"
|
||
/>
|
||
</template>
|
||
<template v-else-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'number' || (selectedFreeTableCellInfo.cell.contentType || 'text') === 'amount'">
|
||
<a-input-number
|
||
:value="selectedFreeTableCellInfo.cell.decimalPlaces ?? 2"
|
||
addon-before="小数位数"
|
||
:min="0"
|
||
:max="6"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedFreeTableCell('decimalPlaces', Number($event ?? 2))"
|
||
/>
|
||
<a-switch
|
||
:checked="selectedFreeTableCellInfo.cell.roundHalfUp !== false"
|
||
checked-children="四舍五入"
|
||
un-checked-children="截断"
|
||
@update:checked="updateSelectedFreeTableCell('roundHalfUp', $event)"
|
||
/>
|
||
<a-select
|
||
v-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'amount'"
|
||
:value="selectedFreeTableCellInfo.cell.amountType || 'CNY'"
|
||
style="width: 100%"
|
||
:options="AMOUNT_TYPE_OPTIONS"
|
||
@update:value="updateSelectedFreeTableCell('amountType', $event)"
|
||
/>
|
||
</template>
|
||
<a-space v-if="(selectedFreeTableCellInfo.cell.contentType || 'text') === 'text'">
|
||
<a-switch
|
||
:checked="selectedFreeTableCellInfo.cell.autoWrap !== false"
|
||
checked-children="自动换行"
|
||
un-checked-children="不换行"
|
||
@update:checked="updateSelectedFreeTableCell('autoWrap', $event)"
|
||
/>
|
||
<a-switch
|
||
:checked="!!selectedFreeTableCellInfo.cell.autoFitFont"
|
||
checked-children="自动适配字号"
|
||
un-checked-children="固定字号"
|
||
@update:checked="updateSelectedFreeTableCell('autoFitFont', $event)"
|
||
/>
|
||
</a-space>
|
||
<a-input-number
|
||
:value="selectedFreeTableCellInfo.cell.fontSize || 12"
|
||
addon-before="字号(px)"
|
||
:min="8"
|
||
:max="72"
|
||
style="width: 100%"
|
||
@update:value="updateSelectedFreeTableCell('fontSize', Number($event || 12))"
|
||
/>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="selectedFreeTableCellInfo.cell.color || '#111111'"
|
||
addon-before="文字色"
|
||
placeholder="#111111"
|
||
@update:value="updateSelectedFreeTableCell('color', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择文字颜色"
|
||
:value="toColorHex(selectedFreeTableCellInfo.cell.color || '#111111')"
|
||
@input="handleFreeTableCellColorInput('color', $event, '#111111')"
|
||
/>
|
||
</div>
|
||
<div class="color-input-row">
|
||
<a-input
|
||
:value="selectedFreeTableCellInfo.cell.backgroundColor || '#ffffff'"
|
||
addon-before="背景色"
|
||
placeholder="#ffffff"
|
||
@update:value="updateSelectedFreeTableCell('backgroundColor', $event)"
|
||
/>
|
||
<input
|
||
type="color"
|
||
class="native-color-picker-trigger"
|
||
title="选择背景颜色"
|
||
:value="toColorHex(selectedFreeTableCellInfo.cell.backgroundColor || '#ffffff')"
|
||
@input="handleFreeTableCellColorInput('backgroundColor', $event, '#ffffff')"
|
||
/>
|
||
</div>
|
||
<a-divider plain orientation="left" style="margin: 8px 0">单元格边线(隐藏)</a-divider>
|
||
<div class="free-table-merge-tip">以下为当前锚点格(含合并区域)单独隐藏某侧边框,与「表格样式」中的外框/内线叠加生效。</div>
|
||
<div class="free-table-border-switch-grid">
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">隐上</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="!!selectedFreeTableCellInfo.cell.hideBorderTop"
|
||
checked-children="隐"
|
||
un-checked-children="显"
|
||
@update:checked="updateSelectedFreeTableCell('hideBorderTop', $event)"
|
||
/>
|
||
</div>
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">隐右</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="!!selectedFreeTableCellInfo.cell.hideBorderRight"
|
||
checked-children="隐"
|
||
un-checked-children="显"
|
||
@update:checked="updateSelectedFreeTableCell('hideBorderRight', $event)"
|
||
/>
|
||
</div>
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">隐下</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="!!selectedFreeTableCellInfo.cell.hideBorderBottom"
|
||
checked-children="隐"
|
||
un-checked-children="显"
|
||
@update:checked="updateSelectedFreeTableCell('hideBorderBottom', $event)"
|
||
/>
|
||
</div>
|
||
<div class="free-table-border-switch-cell">
|
||
<span class="free-table-border-switch-label">隐左</span>
|
||
<a-switch
|
||
size="small"
|
||
:checked="!!selectedFreeTableCellInfo.cell.hideBorderLeft"
|
||
checked-children="隐"
|
||
un-checked-children="显"
|
||
@update:checked="updateSelectedFreeTableCell('hideBorderLeft', $event)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</a-space>
|
||
<a-empty v-else description="请先在画布中点击自由表格的单元格" />
|
||
</details>
|
||
</template>
|
||
</template>
|
||
<template v-else>
|
||
<a-empty description="请先选中元素" />
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, ref } from 'vue';
|
||
import { useMessage } from '/@/hooks/web/useMessage';
|
||
import { TABLE_FONT_OPTIONS } from '../core/fontOptions';
|
||
import TableHeaderConfigEditor from './TableHeaderConfigEditor.vue';
|
||
import type {
|
||
NativeDataBindingDetailTable,
|
||
NativeDataBindingParam,
|
||
NativeElement,
|
||
NativeTemplateSchema,
|
||
} from '../core/types';
|
||
import {
|
||
canMergeFreeTableRegion,
|
||
clipAnchorsToGrid,
|
||
getFreeTableOwnerAt,
|
||
mergeFreeTableRegion,
|
||
normalizeFreeTableAnchors,
|
||
splitFreeTableAt,
|
||
} from '../core/freeTableGrid';
|
||
import { FREE_TABLE_LINE_STYLE_OPTIONS } from '../core/freeTableLineStyles';
|
||
import {
|
||
MIN_FREE_TABLE_TRACK_MM,
|
||
buildEvenColWidths,
|
||
buildEvenRowHeights,
|
||
colWidthsAfterAddCol,
|
||
colWidthsAfterRemoveCol,
|
||
resolveFreeTableColWidthsMm,
|
||
resolveFreeTableRowHeightsMm,
|
||
rowHeightsAfterAddRow,
|
||
rowHeightsAfterRemoveRow,
|
||
setColWidthAt,
|
||
setRowHeightAt,
|
||
} from '../core/freeTableTracks';
|
||
|
||
const { createMessage } = useMessage();
|
||
|
||
const propertiesPanelRootRef = ref<HTMLElement | null>(null);
|
||
|
||
function getSectionDetails(): HTMLDetailsElement[] {
|
||
const root = propertiesPanelRootRef.value;
|
||
if (!root) return [];
|
||
return Array.from(root.querySelectorAll('details.section-card')) as HTMLDetailsElement[];
|
||
}
|
||
|
||
/** 折叠所有功能分区(details.section-card) */
|
||
function collapseAllSections() {
|
||
getSectionDetails().forEach((el) => {
|
||
el.open = false;
|
||
});
|
||
}
|
||
|
||
/** 展开所有功能分区 */
|
||
function expandAllSections() {
|
||
getSectionDetails().forEach((el) => {
|
||
el.open = true;
|
||
});
|
||
}
|
||
|
||
const props = defineProps<{
|
||
schema: NativeTemplateSchema;
|
||
selectedElement?: NativeElement;
|
||
selectedTableColumnKey?: string;
|
||
selectedFreeTableCell?: { row: number; col: number } | null;
|
||
/** 画布 Shift 选取的矩形(含首尾),用于合并 */
|
||
selectedFreeTableMergeRect?: { r0: number; r1: number; c0: number; c1: number } | null;
|
||
}>();
|
||
|
||
/** 标题/正文等:在「元素属性」中显示 style 文字色与背景色 */
|
||
const showPlainTextStyleColors = computed(() => {
|
||
const el = props.selectedElement;
|
||
if (!el) return false;
|
||
return ['title', 'subtitle', 'text', 'date', 'pageNo'].includes(el.type);
|
||
});
|
||
|
||
/** 透明/无色时取色器用白色占位 */
|
||
const plainTextBackgroundForPicker = computed(() => {
|
||
const raw = String(props.selectedElement?.style?.backgroundColor ?? '').trim().toLowerCase();
|
||
if (!raw || raw === 'transparent' || raw === 'none' || raw === 'rgba(0,0,0,0)' || raw === 'rgba(0, 0, 0, 0)') {
|
||
return '#ffffff';
|
||
}
|
||
return raw;
|
||
});
|
||
|
||
/** 左侧「参数」页维护的键,供下拉绑定 */
|
||
const bindingParamOptions = computed(() =>
|
||
(props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => ({
|
||
value: p.key,
|
||
label: p.label ? `${p.key}(${p.label})` : p.key,
|
||
})),
|
||
);
|
||
|
||
/** 左侧「字段」页登记的明细数据源,供普通/明细表格 source 下拉 */
|
||
const detailTableSourceOptions = computed(() => {
|
||
const tables = (props.schema.dataBinding?.detailTables ?? []) as NativeDataBindingDetailTable[];
|
||
const opts = tables.map((t) => ({
|
||
value: t.tableKey,
|
||
label: t.label ? `${t.tableKey}(${t.label})` : t.tableKey,
|
||
}));
|
||
const el = props.selectedElement;
|
||
if (!el || (el.type !== 'table' && el.type !== 'detailTable')) return opts;
|
||
const cur = String((el as any).source || '').trim();
|
||
if (!cur || opts.some((o) => o.value === cur)) return opts;
|
||
return [{ value: cur, label: `${cur}(未登记,请迁移或到「字段」页登记)` }, ...opts];
|
||
});
|
||
|
||
/** 当前表格元素 source 对应的明细字段列表 */
|
||
const detailFieldOptionsForCurrentSource = computed(() => {
|
||
const el = props.selectedElement;
|
||
if (!el || (el.type !== 'table' && el.type !== 'detailTable')) return [];
|
||
const src = String((el as any).source || '').trim();
|
||
const dt = (props.schema.dataBinding?.detailTables ?? []).find((t) => t.tableKey === src) as
|
||
| NativeDataBindingDetailTable
|
||
| undefined;
|
||
const fields = dt?.fields ?? [];
|
||
return fields.map((f) => ({
|
||
value: f.key,
|
||
label: f.label ? `${f.key}(${f.label})` : f.key,
|
||
}));
|
||
});
|
||
|
||
function resolveParamBindSelectValue(raw: string | undefined | null) {
|
||
const k = String(raw || '').trim();
|
||
if (!k) return undefined;
|
||
const keys = new Set((props.schema.dataBinding?.params ?? []).map((p: NativeDataBindingParam) => p.key));
|
||
return keys.has(k) ? k : undefined;
|
||
}
|
||
|
||
/** 表格数据源:已登记则正常;未登记的旧模板仍回显当前键 */
|
||
function resolveTableSourceSelectValue(raw: string | undefined | null) {
|
||
const k = String(raw || '').trim();
|
||
return k || undefined;
|
||
}
|
||
|
||
/** 列绑定字段:空为 undefined 便于 allow-clear */
|
||
function resolveDetailFieldBindSelectValue(raw: string | undefined | null) {
|
||
const k = String(raw || '').trim();
|
||
return k || undefined;
|
||
}
|
||
|
||
/** 参数显示名:有 label 用 label,否则用键 */
|
||
function resolveParamDisplayName(paramKey: string) {
|
||
const k = String(paramKey || '').trim();
|
||
if (!k) return '';
|
||
const p = (props.schema.dataBinding?.params ?? []).find((x: NativeDataBindingParam) => x.key === k);
|
||
if (p?.label && String(p.label).trim()) return String(p.label).trim();
|
||
return k;
|
||
}
|
||
|
||
/** 元素绑定参数变更:标题/文本/日期类自动把「文本」设为参数显示名 */
|
||
function handleElementBindParamChange(value: string | undefined) {
|
||
if (!props.selectedElement) return;
|
||
const v = value ?? '';
|
||
const t = props.selectedElement.type;
|
||
const patch: Record<string, unknown> = { bindField: v };
|
||
if (v && ['title', 'subtitle', 'text', 'date'].includes(t)) {
|
||
patch.text = resolveParamDisplayName(v);
|
||
}
|
||
emit('update-element', { id: props.selectedElement.id, patch: patch as any });
|
||
}
|
||
|
||
/** 明细字段显示名:有 label 用 label,否则用字段键 */
|
||
function resolveDetailFieldDisplayName(tableKey: string, fieldKey: string) {
|
||
const tk = String(tableKey || '').trim();
|
||
const fk = String(fieldKey || '').trim();
|
||
if (!tk || !fk) return '';
|
||
const dt = (props.schema.dataBinding?.detailTables ?? []).find((t) => t.tableKey === tk);
|
||
const f = dt?.fields?.find((x) => x.key === fk);
|
||
if (f?.label && String(f.label).trim()) return String(f.label).trim();
|
||
return fk;
|
||
}
|
||
|
||
/** 表格列绑定明细字段:同步列标题为字段显示名(或字段键) */
|
||
function handleTableColumnBindDetailFieldChange(value: string | undefined) {
|
||
const v = value ?? '';
|
||
if (!props.selectedElement || (props.selectedElement.type !== 'table' && props.selectedElement.type !== 'detailTable')) {
|
||
return;
|
||
}
|
||
if (!selectedTableColumnInfo.value) return;
|
||
const src = String((props.selectedElement as any).source || '').trim();
|
||
const idx = selectedTableColumnInfo.value.index;
|
||
const columns = tableColumns.value.map((item) => ({ ...item }));
|
||
const target = columns[idx];
|
||
if (!target) return;
|
||
target.bindField = v;
|
||
target.field = v;
|
||
if (v) {
|
||
const display = resolveDetailFieldDisplayName(src, v);
|
||
target.title = display || v;
|
||
}
|
||
emit('update-element', { id: props.selectedElement.id, patch: { columns } as any });
|
||
}
|
||
|
||
/** 切换数据源:清空不在新数据源中的列绑定 */
|
||
function handleTableSourceChange(value: string | undefined) {
|
||
if (!props.selectedElement || (props.selectedElement.type !== 'table' && props.selectedElement.type !== 'detailTable')) {
|
||
return;
|
||
}
|
||
const v = String(value || '').trim();
|
||
const fieldKeys = new Set(
|
||
(props.schema.dataBinding?.detailTables ?? []).find((t) => t.tableKey === v)?.fields?.map((f) => f.key) ?? [],
|
||
);
|
||
const columns = tableColumns.value.map((item) => {
|
||
const c = { ...item };
|
||
const bf = String(c.bindField || '').trim();
|
||
if (bf && !fieldKeys.has(bf)) {
|
||
c.bindField = '';
|
||
c.field = '';
|
||
}
|
||
return c;
|
||
});
|
||
emit('update-element', { id: props.selectedElement.id, patch: { source: v || undefined, columns } as any });
|
||
}
|
||
|
||
/** 自由表格单元格绑定参数:同步单元格文本为参数显示名(单次提交避免状态不同步) */
|
||
function handleFreeTableCellBindParamChange(value: string | undefined) {
|
||
const v = value ?? '';
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
const info = selectedFreeTableCellInfo.value;
|
||
if (!info) return;
|
||
const rowCount = Math.max(1, Number((props.selectedElement as any).rowCount || 1));
|
||
const colCount = Math.max(1, Number((props.selectedElement as any).colCount || 1));
|
||
const anchors = normalizeFreeTableAnchors(rowCount, colCount, (props.selectedElement as any).cells || []);
|
||
const display = v ? resolveParamDisplayName(v) : undefined;
|
||
const next = anchors.map((item: any) => {
|
||
if (item.row === info.row && item.col === info.col) {
|
||
const row: any = { ...item, bindField: v };
|
||
if (v && display) row.text = display;
|
||
return row;
|
||
}
|
||
return { ...item };
|
||
});
|
||
updateFreeTableElement({ cells: next });
|
||
}
|
||
|
||
const emit = defineEmits(['update-element']);
|
||
const headerConfigModalOpen = ref(false);
|
||
const columnWidthModalOpen = ref(false);
|
||
const TABLE_COLUMN_CONTENT_TYPE_OPTIONS = [
|
||
{ label: '文本', value: 'text' },
|
||
{ label: '数字', value: 'number' },
|
||
{ label: '金额', value: 'amount' },
|
||
{ label: '图片', value: 'image' },
|
||
{ label: '二维码', value: 'qrcode' },
|
||
{ label: '条形码', value: 'barcode' },
|
||
];
|
||
const AMOUNT_TYPE_OPTIONS = [
|
||
{ label: '人民币(CNY)', value: 'CNY' },
|
||
{ label: '美元(USD)', value: 'USD' },
|
||
{ label: '欧元(EUR)', value: 'EUR' },
|
||
];
|
||
const TABLE_HEIGHT_MODE_OPTIONS = [
|
||
{ label: '按数据自动分页', value: 'autoPage' },
|
||
{ label: '固定行数分页', value: 'fixedRows' },
|
||
];
|
||
const FOOTER_TOTAL_MODE_OPTIONS = [
|
||
{ label: '合计模式:总合计', value: 'overall' },
|
||
{ label: '合计模式:按页合计', value: 'page' },
|
||
];
|
||
const IMAGE_FIT_OPTIONS = [
|
||
{ label: '拉伸填充', value: 'fill' },
|
||
{ label: '等比包含', value: 'contain' },
|
||
{ label: '等比裁切', value: 'cover' },
|
||
];
|
||
const QRCODE_LEVEL_OPTIONS = [
|
||
{ label: 'L(低容错)', value: 'L' },
|
||
{ label: 'M(中容错)', value: 'M' },
|
||
{ label: 'Q(高容错)', value: 'Q' },
|
||
{ label: 'H(最高容错)', value: 'H' },
|
||
];
|
||
const QRCODE_RENDER_OPTIONS = [
|
||
{ label: 'PNG', value: 'image/png' },
|
||
{ label: 'JPEG', value: 'image/jpeg' },
|
||
{ label: 'WEBP', value: 'image/webp' },
|
||
];
|
||
const BARCODE_FORMAT_OPTIONS = [
|
||
{ label: 'CODE128', value: 'CODE128' },
|
||
{ label: 'CODE39', value: 'CODE39' },
|
||
{ label: 'EAN13', value: 'EAN13' },
|
||
{ label: 'EAN8', value: 'EAN8' },
|
||
{ label: 'ITF14', value: 'ITF14' },
|
||
{ label: 'MSI', value: 'MSI' },
|
||
{ label: 'pharmacode', value: 'pharmacode' },
|
||
];
|
||
const REFRESH_PAGE_OPTIONS = [
|
||
{ label: '刷新页:不应用', value: 'none' },
|
||
{ label: '刷新页:每次', value: 'always' },
|
||
{ label: '刷新页:溢出时', value: 'onOverflow' },
|
||
];
|
||
const FREE_TABLE_ALIGN_OPTIONS = [
|
||
{ label: '单元格对齐:左', value: 'left' },
|
||
{ label: '单元格对齐:中', value: 'center' },
|
||
{ label: '单元格对齐:右', value: 'right' },
|
||
];
|
||
const FREE_TABLE_VALIGN_OPTIONS = [
|
||
{ label: '垂直对齐:上', value: 'top' },
|
||
{ label: '垂直对齐:中', value: 'middle' },
|
||
{ label: '垂直对齐:下', value: 'bottom' },
|
||
];
|
||
|
||
function updateRect(key: string, value: any) {
|
||
if (!props.selectedElement) return;
|
||
emit('update-element', { id: props.selectedElement.id, patch: { [key]: Number(value || 0) } as any });
|
||
}
|
||
|
||
function updateStyle(key: string, value: any) {
|
||
if (!props.selectedElement) return;
|
||
emit('update-element', {
|
||
id: props.selectedElement.id,
|
||
patch: {
|
||
style: {
|
||
...(props.selectedElement.style || {}),
|
||
[key]: value,
|
||
},
|
||
} as any,
|
||
});
|
||
}
|
||
|
||
function updateField(key: string, value: any) {
|
||
if (!props.selectedElement) return;
|
||
emit('update-element', { id: props.selectedElement.id, patch: { [key]: value } as any });
|
||
}
|
||
|
||
const tableColumns = computed<any[]>(() => {
|
||
if (!props.selectedElement || (props.selectedElement.type !== 'table' && props.selectedElement.type !== 'detailTable')) {
|
||
return [];
|
||
}
|
||
return Array.isArray((props.selectedElement as any).columns) ? (props.selectedElement as any).columns : [];
|
||
});
|
||
|
||
const selectedTableColumnInfo = computed<{ index: number; col: any } | null>(() => {
|
||
if (!tableColumns.value.length || !props.selectedTableColumnKey) {
|
||
return null;
|
||
}
|
||
const index = tableColumns.value.findIndex((item) => item?.key === props.selectedTableColumnKey);
|
||
if (index < 0) {
|
||
return null;
|
||
}
|
||
return { index, col: tableColumns.value[index] };
|
||
});
|
||
|
||
/** 当前列绑定字段下拉:含未登记的旧值,避免下拉空白 */
|
||
const detailFieldBindOptionsForColumn = computed(() => {
|
||
const base = detailFieldOptionsForCurrentSource.value;
|
||
const col = selectedTableColumnInfo.value?.col;
|
||
const bf = String(col?.bindField || col?.field || '').trim();
|
||
if (!bf || base.some((o) => o.value === bf)) return base;
|
||
return [{ value: bf, label: `${bf}(未登记)` }, ...base];
|
||
});
|
||
|
||
const selectedFreeTableCellInfo = computed<{ row: number; col: number; cell: any } | null>(() => {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
|
||
return null;
|
||
}
|
||
const row = Number(props.selectedFreeTableCell?.row ?? -1);
|
||
const col = Number(props.selectedFreeTableCell?.col ?? -1);
|
||
if (row < 0 || col < 0) {
|
||
return null;
|
||
}
|
||
const rowCount = Math.max(1, Number((props.selectedElement as any).rowCount || 1));
|
||
const colCount = Math.max(1, Number((props.selectedElement as any).colCount || 1));
|
||
if (row >= rowCount || col >= colCount) {
|
||
return null;
|
||
}
|
||
const anchors = normalizeFreeTableAnchors(rowCount, colCount, (props.selectedElement as any).cells || []);
|
||
const owner = getFreeTableOwnerAt(anchors, row, col);
|
||
return { row: owner.row, col: owner.col, cell: owner };
|
||
});
|
||
|
||
const canMergeFreeTableSelection = computed(() => {
|
||
const rect = props.selectedFreeTableMergeRect;
|
||
if (!rect || !props.selectedElement || props.selectedElement.type !== 'freeTable') {
|
||
return false;
|
||
}
|
||
if (rect.r0 === rect.r1 && rect.c0 === rect.c1) {
|
||
return false;
|
||
}
|
||
const el = props.selectedElement as any;
|
||
const rowCount = Math.max(1, Number(el.rowCount || 1));
|
||
const colCount = Math.max(1, Number(el.colCount || 1));
|
||
const anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
|
||
return canMergeFreeTableRegion(anchors, rowCount, colCount, rect.r0, rect.c0, rect.r1, rect.c1);
|
||
});
|
||
|
||
const canSplitFreeTableMerged = computed(() => {
|
||
const info = selectedFreeTableCellInfo.value;
|
||
if (!info) return false;
|
||
const rs = Math.max(1, Number(info.cell.rowspan || 1));
|
||
const cs = Math.max(1, Number(info.cell.colspan || 1));
|
||
return rs > 1 || cs > 1;
|
||
});
|
||
|
||
const freeTableTrackEl = computed(() => {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
|
||
return null;
|
||
}
|
||
return props.selectedElement as any;
|
||
});
|
||
|
||
const freeTableColIndices = computed(() => {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return [];
|
||
}
|
||
const n = Math.max(1, Number(el.colCount || 1));
|
||
return Array.from({ length: n }, (_, i) => i);
|
||
});
|
||
|
||
const freeTableRowIndices = computed(() => {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return [];
|
||
}
|
||
const n = Math.max(1, Number(el.rowCount || 1));
|
||
return Array.from({ length: n }, (_, i) => i);
|
||
});
|
||
|
||
const freeTableColWidthsResolved = computed(() => {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return [];
|
||
}
|
||
return resolveFreeTableColWidthsMm(el);
|
||
});
|
||
|
||
const freeTableRowHeightsResolved = computed(() => {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return [];
|
||
}
|
||
return resolveFreeTableRowHeightsMm(el);
|
||
});
|
||
|
||
const freeTableMaxColWidthMm = computed(() => {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return 200;
|
||
}
|
||
const n = Math.max(1, Number(el.colCount || 1));
|
||
const w = Math.max(0.01, Number(el.w) || 0.01);
|
||
return Math.max(MIN_FREE_TABLE_TRACK_MM, w - MIN_FREE_TABLE_TRACK_MM * Math.max(0, n - 1));
|
||
});
|
||
|
||
const freeTableMaxRowHeightMm = computed(() => {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return 200;
|
||
}
|
||
const n = Math.max(1, Number(el.rowCount || 1));
|
||
const h = Math.max(0.01, Number(el.h) || 0.01);
|
||
return Math.max(MIN_FREE_TABLE_TRACK_MM, h - MIN_FREE_TABLE_TRACK_MM * Math.max(0, n - 1));
|
||
});
|
||
|
||
const footerLabelColumnOptions = computed(() => {
|
||
const options = tableColumns.value.map((item) => ({
|
||
label: `标签列:${item?.title || item?.key}`,
|
||
value: item?.key,
|
||
}));
|
||
return options;
|
||
});
|
||
const mergeColumnOptions = computed(() => {
|
||
return tableColumns.value.map((item) => ({
|
||
label: `${item?.title || item?.key}(${item?.bindField || item?.field || item?.key})`,
|
||
value: item?.key,
|
||
}));
|
||
});
|
||
|
||
function updateTableColumn(index: number, key: string, value: any) {
|
||
if (!props.selectedElement) return;
|
||
const columns = tableColumns.value.map((item) => ({ ...item }));
|
||
const target = columns[index];
|
||
if (!target) return;
|
||
target[key] = value;
|
||
if (key === 'bindField' && !target.field) {
|
||
target.field = value;
|
||
}
|
||
if (key === 'fontSize') {
|
||
target.useCustomFontSize = true;
|
||
}
|
||
emit('update-element', { id: props.selectedElement.id, patch: { columns } as any });
|
||
}
|
||
|
||
function updateMergeColumns(values: string[]) {
|
||
if (!props.selectedElement) return;
|
||
const selected = Array.isArray(values) ? values.map((item) => String(item || '')).filter(Boolean) : [];
|
||
const validKeys = new Set(tableColumns.value.map((item) => String(item?.key || '')));
|
||
const next = selected.filter((item) => validKeys.has(item));
|
||
updateField('mergeColumnKeys', next);
|
||
}
|
||
|
||
function addTableColumn() {
|
||
if (!props.selectedElement) return;
|
||
const columns = tableColumns.value.map((item) => ({ ...item }));
|
||
const nextIndex = columns.length + 1;
|
||
columns.push({
|
||
key: `col_${Date.now()}_${nextIndex}`,
|
||
title: `列${nextIndex}`,
|
||
field: `field${nextIndex}`,
|
||
bindField: `field${nextIndex}`,
|
||
width: 30,
|
||
align: 'left',
|
||
contentType: 'text',
|
||
fontFamily: '',
|
||
fontSize: 12,
|
||
useCustomFontSize: false,
|
||
fontColor: '#111111',
|
||
autoFitFont: false,
|
||
autoWrap: true,
|
||
fillCell: true,
|
||
contentScale: 100,
|
||
imageFit: 'contain',
|
||
qrLevel: 'M',
|
||
qrRenderType: 'image/png',
|
||
barcodeFormat: 'CODE128',
|
||
decimalPlaces: 2,
|
||
roundHalfUp: true,
|
||
amountType: 'CNY',
|
||
enableFooterTotal: false,
|
||
});
|
||
emit('update-element', { id: props.selectedElement.id, patch: { columns, headerConfig: undefined } as any });
|
||
}
|
||
|
||
function removeTableColumn(index: number) {
|
||
if (!props.selectedElement) return;
|
||
const columns = tableColumns.value.map((item) => ({ ...item })).filter((_, idx) => idx !== index);
|
||
const validKeys = new Set(columns.map((item) => String(item?.key || '')));
|
||
const mergeColumnKeys = Array.isArray((props.selectedElement as any).mergeColumnKeys)
|
||
? (props.selectedElement as any).mergeColumnKeys.filter((item: string) => validKeys.has(String(item || '')))
|
||
: [];
|
||
emit('update-element', { id: props.selectedElement.id, patch: { columns, headerConfig: undefined, mergeColumnKeys } as any });
|
||
}
|
||
|
||
function updateSelectedTableColumn(key: string, value: any) {
|
||
if (!selectedTableColumnInfo.value) return;
|
||
updateTableColumn(selectedTableColumnInfo.value.index, key, value);
|
||
}
|
||
|
||
function updateFreeTableElement(nextPatch: any) {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
emit('update-element', { id: props.selectedElement.id, patch: nextPatch as any });
|
||
}
|
||
|
||
function patchFreeTableOuterSide(side: 'top' | 'right' | 'bottom' | 'left', checked: boolean) {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
|
||
return;
|
||
}
|
||
const el = props.selectedElement as any;
|
||
updateField('outerBorder', { ...(el.outerBorder || {}), [side]: checked });
|
||
}
|
||
|
||
function patchFreeTableInnerSide(key: 'horizontal' | 'vertical', checked: boolean) {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') {
|
||
return;
|
||
}
|
||
const el = props.selectedElement as any;
|
||
updateField('innerBorder', { ...(el.innerBorder || {}), [key]: checked });
|
||
}
|
||
|
||
function handleFreeTableColWidthChange(index: number, val: number | string | null) {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return;
|
||
}
|
||
const v = Number(val);
|
||
if (!Number.isFinite(v)) {
|
||
return;
|
||
}
|
||
const cur = resolveFreeTableColWidthsMm(el);
|
||
const next = setColWidthAt(cur, index, v, Math.max(0.01, Number(el.w) || 0.01));
|
||
if (!next) {
|
||
createMessage.warning(`无法满足该列宽(单列最小约 ${MIN_FREE_TABLE_TRACK_MM}mm)`);
|
||
return;
|
||
}
|
||
updateFreeTableElement({ colWidths: next });
|
||
}
|
||
|
||
function handleFreeTableRowHeightChange(index: number, val: number | string | null) {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return;
|
||
}
|
||
const v = Number(val);
|
||
if (!Number.isFinite(v)) {
|
||
return;
|
||
}
|
||
const cur = resolveFreeTableRowHeightsMm(el);
|
||
const next = setRowHeightAt(cur, index, v, Math.max(0.01, Number(el.h) || 0.01));
|
||
if (!next) {
|
||
createMessage.warning(`无法满足该行高(单行最小约 ${MIN_FREE_TABLE_TRACK_MM}mm)`);
|
||
return;
|
||
}
|
||
updateFreeTableElement({ rowHeights: next });
|
||
}
|
||
|
||
function evenFreeTableColWidths() {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return;
|
||
}
|
||
const cc = Math.max(1, Number(el.colCount || 1));
|
||
const w = Math.max(0.01, Number(el.w) || 0.01);
|
||
updateFreeTableElement({ colWidths: buildEvenColWidths(cc, w) });
|
||
}
|
||
|
||
function evenFreeTableRowHeights() {
|
||
const el = freeTableTrackEl.value;
|
||
if (!el) {
|
||
return;
|
||
}
|
||
const rc = Math.max(1, Number(el.rowCount || 1));
|
||
const h = Math.max(0.01, Number(el.h) || 0.01);
|
||
updateFreeTableElement({ rowHeights: buildEvenRowHeights(rc, h) });
|
||
}
|
||
|
||
/** 行/列变化后:裁剪越界锚点并填满网格 */
|
||
function rebuildFreeTableCells(rowCount: number, colCount: number, baseCells: any[] = []) {
|
||
const clipped = clipAnchorsToGrid(rowCount, colCount, baseCells);
|
||
return normalizeFreeTableAnchors(rowCount, colCount, clipped);
|
||
}
|
||
|
||
function mergeFreeTableSelection() {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
const rect = props.selectedFreeTableMergeRect;
|
||
if (!rect) return;
|
||
const el = props.selectedElement as any;
|
||
const rowCount = Math.max(1, Number(el.rowCount || 1));
|
||
const colCount = Math.max(1, Number(el.colCount || 1));
|
||
let anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
|
||
if (!canMergeFreeTableRegion(anchors, rowCount, colCount, rect.r0, rect.c0, rect.r1, rect.c1)) {
|
||
createMessage.warning('所选区域无法合并(需均为未合并的 1×1 单元格,且为完整矩形)');
|
||
return;
|
||
}
|
||
anchors = mergeFreeTableRegion(anchors, rowCount, colCount, rect.r0, rect.c0, rect.r1, rect.c1);
|
||
updateFreeTableElement({ cells: anchors });
|
||
createMessage.success('已合并单元格');
|
||
}
|
||
|
||
function splitFreeTableMerged() {
|
||
const info = selectedFreeTableCellInfo.value;
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable' || !info) return;
|
||
const el = props.selectedElement as any;
|
||
const rowCount = Math.max(1, Number(el.rowCount || 1));
|
||
const colCount = Math.max(1, Number(el.colCount || 1));
|
||
const anchors = normalizeFreeTableAnchors(rowCount, colCount, el.cells || []);
|
||
const next = splitFreeTableAt(anchors, rowCount, colCount, info.row, info.col);
|
||
updateFreeTableElement({ cells: next });
|
||
createMessage.success('已拆分合并单元格');
|
||
}
|
||
|
||
function addFreeTableRow() {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
const el = props.selectedElement as any;
|
||
const rowCount = Math.max(1, Number(el.rowCount || 1)) + 1;
|
||
const colCount = Math.max(1, Number(el.colCount || 1));
|
||
const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
|
||
const prevRh = resolveFreeTableRowHeightsMm(el);
|
||
const nextRh = rowHeightsAfterAddRow(prevRh, Math.max(0.01, Number(el.h) || 0.01));
|
||
updateFreeTableElement({ rowCount, cells, rowHeights: nextRh });
|
||
}
|
||
|
||
function removeFreeTableRow() {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
const el = props.selectedElement as any;
|
||
const prevRowCount = Math.max(1, Number(el.rowCount || 1));
|
||
if (prevRowCount <= 1) return;
|
||
const rowCount = prevRowCount - 1;
|
||
const colCount = Math.max(1, Number(el.colCount || 1));
|
||
const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
|
||
const prevRh = resolveFreeTableRowHeightsMm(el);
|
||
const nextRh = rowHeightsAfterRemoveRow(prevRh, prevRowCount - 1, Math.max(0.01, Number(el.h) || 0.01));
|
||
updateFreeTableElement({ rowCount, cells, rowHeights: nextRh });
|
||
}
|
||
|
||
function addFreeTableCol() {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
const el = props.selectedElement as any;
|
||
const rowCount = Math.max(1, Number(el.rowCount || 1));
|
||
const colCount = Math.max(1, Number(el.colCount || 1)) + 1;
|
||
const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
|
||
const prevCw = resolveFreeTableColWidthsMm(el);
|
||
const nextCw = colWidthsAfterAddCol(prevCw, Math.max(0.01, Number(el.w) || 0.01));
|
||
updateFreeTableElement({ colCount, cells, colWidths: nextCw });
|
||
}
|
||
|
||
function removeFreeTableCol() {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
const el = props.selectedElement as any;
|
||
const rowCount = Math.max(1, Number(el.rowCount || 1));
|
||
const prevColCount = Math.max(1, Number(el.colCount || 1));
|
||
if (prevColCount <= 1) return;
|
||
const colCount = prevColCount - 1;
|
||
const cells = rebuildFreeTableCells(rowCount, colCount, el.cells || []);
|
||
const prevCw = resolveFreeTableColWidthsMm(el);
|
||
const nextCw = colWidthsAfterRemoveCol(prevCw, prevColCount - 1, Math.max(0.01, Number(el.w) || 0.01));
|
||
updateFreeTableElement({ colCount, cells, colWidths: nextCw });
|
||
}
|
||
|
||
function updateSelectedFreeTableCell(key: string, value: any) {
|
||
if (!props.selectedElement || props.selectedElement.type !== 'freeTable') return;
|
||
const info = selectedFreeTableCellInfo.value;
|
||
if (!info) return;
|
||
const rowCount = Math.max(1, Number((props.selectedElement as any).rowCount || 1));
|
||
const colCount = Math.max(1, Number((props.selectedElement as any).colCount || 1));
|
||
const anchors = normalizeFreeTableAnchors(rowCount, colCount, (props.selectedElement as any).cells || []);
|
||
const next = anchors.map((item: any) => {
|
||
if (item.row === info.row && item.col === info.col) {
|
||
const row = { ...item };
|
||
if (String(key).startsWith('hideBorder')) {
|
||
if (value) {
|
||
row[key] = true;
|
||
} else {
|
||
delete row[key];
|
||
}
|
||
} else {
|
||
row[key] = value;
|
||
}
|
||
return row;
|
||
}
|
||
return { ...item };
|
||
});
|
||
updateFreeTableElement({ cells: next });
|
||
}
|
||
|
||
function removeSelectedTableColumn() {
|
||
if (!selectedTableColumnInfo.value) return;
|
||
removeTableColumn(selectedTableColumnInfo.value.index);
|
||
}
|
||
|
||
function resolveFooterLabelColumnKey() {
|
||
const key = String((props.selectedElement as any)?.footerLabelColumnKey || '');
|
||
if (key) {
|
||
return key;
|
||
}
|
||
return tableColumns.value?.[0]?.key || '';
|
||
}
|
||
|
||
function resolveHeaderConfigRowCount() {
|
||
const count = Number((props.selectedElement as any)?.headerConfig?.rowCount || 1);
|
||
if (Number.isFinite(count) && count > 0) {
|
||
return count;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
function handleHeaderConfigChange(nextHeaderConfig: any) {
|
||
if (!props.selectedElement) return;
|
||
const nextColumns = syncColumnTitlesByHeaderConfig(tableColumns.value, nextHeaderConfig);
|
||
emit('update-element', {
|
||
id: props.selectedElement.id,
|
||
patch: {
|
||
headerConfig: nextHeaderConfig,
|
||
columns: nextColumns,
|
||
} as any,
|
||
});
|
||
}
|
||
|
||
function handleMultiHeaderToggle(checked: boolean) {
|
||
updateField('enableMultiHeader', checked === true);
|
||
if (checked !== true) {
|
||
headerConfigModalOpen.value = false;
|
||
}
|
||
}
|
||
|
||
function syncColumnTitlesByHeaderConfig(columns: any[], headerConfig: any) {
|
||
const colCount = columns.length;
|
||
const rowCount = Math.max(1, Number(headerConfig?.rowCount || 1));
|
||
const owner: any[][] = Array.from({ length: rowCount }, () => Array.from({ length: colCount }, () => null));
|
||
const cells = Array.isArray(headerConfig?.cells) ? headerConfig.cells : [];
|
||
cells.forEach((item: any) => {
|
||
const row = Math.max(0, Number(item?.row || 0));
|
||
const col = Math.max(0, Number(item?.col || 0));
|
||
const rowspan = Math.max(1, Number(item?.rowspan || 1));
|
||
const colspan = Math.max(1, Number(item?.colspan || 1));
|
||
if (row >= rowCount || col >= colCount || owner[row][col]) return;
|
||
const maxRow = Math.min(rowCount, row + rowspan);
|
||
const maxCol = Math.min(colCount, col + colspan);
|
||
for (let r = row; r < maxRow; r += 1) {
|
||
for (let c = col; c < maxCol; c += 1) {
|
||
if (owner[r][c]) return;
|
||
}
|
||
}
|
||
const next = { ...item, row, col, rowspan: maxRow - row, colspan: maxCol - col };
|
||
for (let r = row; r < maxRow; r += 1) {
|
||
for (let c = col; c < maxCol; c += 1) {
|
||
owner[r][c] = next;
|
||
}
|
||
}
|
||
});
|
||
return columns.map((col, index) => {
|
||
const bottomCell = owner[rowCount - 1]?.[index];
|
||
const nextTitle =
|
||
bottomCell && Number(bottomCell?.row ?? rowCount - 1) < rowCount - 1
|
||
? String(col?.title || `列${index + 1}`)
|
||
: String(bottomCell?.title || col?.title || `列${index + 1}`);
|
||
return { ...col, title: nextTitle };
|
||
});
|
||
}
|
||
|
||
function handleColumnContentTypeChange(value: string) {
|
||
const type = String(value || 'text');
|
||
updateSelectedTableColumn('contentType', type);
|
||
if (type === 'text') {
|
||
updateSelectedTableColumn('autoWrap', true);
|
||
updateSelectedTableColumn('autoFitFont', false);
|
||
return;
|
||
}
|
||
if (type === 'number' || type === 'amount') {
|
||
updateSelectedTableColumn('decimalPlaces', 2);
|
||
updateSelectedTableColumn('roundHalfUp', true);
|
||
updateSelectedTableColumn('amountType', 'CNY');
|
||
updateSelectedTableColumn('enableFooterTotal', false);
|
||
return;
|
||
}
|
||
if (type === 'image') {
|
||
updateSelectedTableColumn('fillCell', true);
|
||
updateSelectedTableColumn('contentScale', 100);
|
||
updateSelectedTableColumn('imageFit', 'contain');
|
||
return;
|
||
}
|
||
if (type === 'qrcode') {
|
||
updateSelectedTableColumn('fillCell', true);
|
||
updateSelectedTableColumn('contentScale', 100);
|
||
updateSelectedTableColumn('qrLevel', 'M');
|
||
updateSelectedTableColumn('qrRenderType', 'image/png');
|
||
return;
|
||
}
|
||
if (type === 'barcode') {
|
||
updateSelectedTableColumn('fillCell', true);
|
||
updateSelectedTableColumn('contentScale', 100);
|
||
updateSelectedTableColumn('barcodeFormat', 'CODE128');
|
||
}
|
||
}
|
||
|
||
function isText(type: string) {
|
||
return ['title', 'subtitle', 'text', 'date', 'pageNo', 'reportHeader', 'reportFooter'].includes(type);
|
||
}
|
||
|
||
function isReportBand(type: string) {
|
||
return type === 'reportHeader' || type === 'reportFooter';
|
||
}
|
||
|
||
function isReportFooter(type: string) {
|
||
return type === 'reportFooter';
|
||
}
|
||
|
||
function toColorHex(value: string) {
|
||
if (!value) return '#ffffff';
|
||
const color = String(value).trim();
|
||
const lower = color.toLowerCase();
|
||
if (lower === 'transparent' || lower === 'none' || lower === 'rgba(0,0,0,0)' || lower === 'rgba(0, 0, 0, 0)') {
|
||
return '#ffffff';
|
||
}
|
||
if (/^#([0-9a-fA-F]{6})$/.test(color)) return color;
|
||
if (/^#([0-9a-fA-F]{3})$/.test(color)) {
|
||
const short = color.slice(1);
|
||
return `#${short[0]}${short[0]}${short[1]}${short[1]}${short[2]}${short[2]}`;
|
||
}
|
||
return '#000000';
|
||
}
|
||
|
||
function handleColorInput(key: string, event: Event, fallback: string) {
|
||
const value = (event?.target as HTMLInputElement | null)?.value || fallback;
|
||
updateField(key, value);
|
||
}
|
||
|
||
function handleColumnColorInput(event: Event) {
|
||
const value = (event?.target as HTMLInputElement | null)?.value || '#111111';
|
||
updateSelectedTableColumn('fontColor', value);
|
||
}
|
||
|
||
function handleStyleColorInput(key: string, event: Event, fallback: string) {
|
||
const value = (event?.target as HTMLInputElement | null)?.value || fallback;
|
||
updateStyle(key, value);
|
||
}
|
||
|
||
function handlePlainTextBackgroundColorInput(event: Event) {
|
||
const value = (event?.target as HTMLInputElement | null)?.value || '#ffffff';
|
||
updateStyle('backgroundColor', value);
|
||
}
|
||
|
||
function handleFreeTableCellColorInput(key: 'color' | 'backgroundColor', event: Event, fallback: string) {
|
||
const value = (event?.target as HTMLInputElement | null)?.value || fallback;
|
||
updateSelectedFreeTableCell(key, value);
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.properties-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 130px);
|
||
padding: 0;
|
||
overflow: hidden;
|
||
border-left: 1px solid #f0f0f0;
|
||
background: #fff;
|
||
|
||
.properties-panel__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
background: #fafafa;
|
||
min-height: 44px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.properties-panel__title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.properties-panel__header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.properties-panel__icon-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
margin: 0;
|
||
border: 1px solid transparent;
|
||
border-radius: 6px;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
line-height: 1;
|
||
|
||
&:hover {
|
||
color: #1677ff;
|
||
background: rgba(22, 119, 255, 0.08);
|
||
border-color: rgba(22, 119, 255, 0.2);
|
||
}
|
||
}
|
||
|
||
.properties-panel__icon-svg {
|
||
display: block;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.properties-panel__body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
padding: 12px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
margin-bottom: 10px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.panel-subtitle {
|
||
margin-bottom: 6px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.section-card {
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
margin-bottom: 8px;
|
||
background: #fff;
|
||
|
||
> summary {
|
||
list-style: none;
|
||
}
|
||
|
||
> summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.table-column-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.table-column-item {
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
background: #fafafa;
|
||
}
|
||
}
|
||
|
||
.color-input-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
:deep(.ant-input-group-wrapper) {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
/** 原生取色器:与文本框并排展示 */
|
||
.native-color-picker-trigger {
|
||
flex: 0 0 28px;
|
||
width: 28px;
|
||
height: 28px;
|
||
padding: 0;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
}
|
||
|
||
.column-width-entry-btn {
|
||
margin-top: 4px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.column-width-modal {
|
||
.column-width-modal-tip {
|
||
margin-bottom: 10px;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
background: #f6ffed;
|
||
border: 1px solid #b7eb8f;
|
||
color: #389e0d;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.column-width-list {
|
||
max-height: 420px;
|
||
overflow: auto;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 10px;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.column-width-item {
|
||
border: 1px solid #f0f0f0;
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
background: #fafafa;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.column-width-item-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.column-width-index {
|
||
display: inline-block;
|
||
min-width: 48px;
|
||
text-align: center;
|
||
border-radius: 10px;
|
||
padding: 0 8px;
|
||
line-height: 20px;
|
||
font-size: 12px;
|
||
color: #1677ff;
|
||
background: #e6f4ff;
|
||
}
|
||
|
||
.column-width-title {
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.column-width-modal-footer {
|
||
margin-top: 12px;
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.free-table-merge-tip {
|
||
font-size: 12px;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
padding: 6px 8px;
|
||
background: #fafafa;
|
||
border-radius: 6px;
|
||
border: 1px dashed #d9d9d9;
|
||
}
|
||
|
||
/* 行数/列数并排:避免 addon 组合框在窄侧栏溢出重叠 */
|
||
.free-table-dim-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
gap: 8px;
|
||
width: 100%;
|
||
}
|
||
|
||
.free-table-dim-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.free-table-dim-label {
|
||
flex: 0 0 auto;
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.free-table-dim-item :deep(.ant-input-number) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.free-table-track-head {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.75);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.free-table-track-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.free-table-track-label {
|
||
flex: 0 0 52px;
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
}
|
||
|
||
.free-table-border-switch-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 8px 6px;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.free-table-border-switch-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.free-table-border-switch-label {
|
||
font-size: 11px;
|
||
color: rgba(0, 0, 0, 0.55);
|
||
}
|
||
|
||
/** 自由表格线型下拉:与画笔样式类似,用线条预览 */
|
||
.free-table-line-style-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-height: 26px;
|
||
}
|
||
|
||
.free-table-line-preview {
|
||
flex: 0 0 56px;
|
||
height: 0;
|
||
border-top: 2px solid rgba(0, 0, 0, 0.82);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.free-table-line-preview--solid {
|
||
border-top-style: solid;
|
||
}
|
||
|
||
.free-table-line-preview--dashed {
|
||
border-top-style: dashed;
|
||
}
|
||
|
||
.free-table-line-preview--dotted {
|
||
border-top-style: dotted;
|
||
}
|
||
|
||
.free-table-line-preview--dash_dot {
|
||
border-top: none;
|
||
height: 2px;
|
||
background: repeating-linear-gradient(
|
||
90deg,
|
||
rgba(0, 0, 0, 0.82) 0 5px,
|
||
transparent 5px 7px,
|
||
rgba(0, 0, 0, 0.82) 7px 8px,
|
||
transparent 8px 14px
|
||
);
|
||
}
|
||
|
||
.free-table-line-preview--double_dash_dot {
|
||
border-top: none;
|
||
height: 2px;
|
||
background: repeating-linear-gradient(
|
||
90deg,
|
||
rgba(0, 0, 0, 0.82) 0 5px,
|
||
transparent 5px 7px,
|
||
rgba(0, 0, 0, 0.82) 7px 8px,
|
||
transparent 8px 9px,
|
||
rgba(0, 0, 0, 0.82) 9px 10px,
|
||
transparent 10px 16px
|
||
);
|
||
}
|
||
|
||
.bind-param-compact {
|
||
display: flex;
|
||
width: 100%;
|
||
align-items: stretch;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
|
||
.bind-param-compact__addon {
|
||
flex: 0 0 80px;
|
||
max-width: 88px;
|
||
padding: 0 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
background: #fafafa;
|
||
border-right: 1px solid #d9d9d9;
|
||
font-size: 14px;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
line-height: 32px;
|
||
}
|
||
|
||
.bind-param-compact__select {
|
||
flex: 1;
|
||
min-width: 0;
|
||
|
||
:deep(.ant-select-selector) {
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|