From e2c6ac4254cb2b25f8062254898c81b098244e2c Mon Sep 17 00:00:00 2001 From: ylturbo <1020311652@qq.com> Date: Thu, 9 Apr 2026 15:24:53 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dcrud=E5=9C=A8table?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E4=B8=8Bspinner=E7=9A=84icon=E5=92=8Ctip?= =?UTF-8?q?=E6=96=87=E5=AD=97=E6=97=A0=E6=B3=95=E9=80=9A=E8=BF=87=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=B1=9E=E6=80=A7className=E5=92=8CspinnerClassName?= =?UTF-8?q?=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/amis-core/src/schema.ts | 15 ++++++++ .../amis-ui/scss/components/_spinner.scss | 7 ++-- packages/amis-ui/src/components/Spinner.tsx | 37 ++++++++++++++----- packages/amis/src/renderers/CRUD.tsx | 8 +++- packages/amis/src/renderers/CRUD2.tsx | 10 +++-- packages/amis/src/renderers/Table/index.tsx | 21 +++++++++-- 6 files changed, 77 insertions(+), 21 deletions(-) diff --git a/packages/amis-core/src/schema.ts b/packages/amis-core/src/schema.ts index 95ca4643d8f..a1a04325397 100644 --- a/packages/amis-core/src/schema.ts +++ b/packages/amis-core/src/schema.ts @@ -1553,9 +1553,24 @@ export interface AMISTrackConfig { * AMIS 加载效果配置 */ export interface AMISSpinnerConfig { + /** + * 是否显示加载状态 + */ + loading?: boolean | AMISSchema; + + /** + * 加载效果配置 + */ loadingConfig?: { root?: string; show?: boolean; + icon?: string; + tip?: string; + tipPlacement?: 'top' | 'right' | 'bottom' | 'left'; + delay?: number; + size?: 'sm' | 'lg' | ''; + className?: string; + spinnerClassName?: string; }; } diff --git a/packages/amis-ui/scss/components/_spinner.scss b/packages/amis-ui/scss/components/_spinner.scss index 3ba70018b84..6738084201f 100644 --- a/packages/amis-ui/scss/components/_spinner.scss +++ b/packages/amis-ui/scss/components/_spinner.scss @@ -23,6 +23,7 @@ flex-direction: column; align-items: center; justify-content: center; + color: var(--Spinner-color); opacity: 0; transition: ease-out opacity var(--animation-duration); @@ -53,14 +54,14 @@ .icon { width: 100%; height: 100%; - color: var(--Spinner-color); + color: inherit; } svg.icon { top: 0; path { - fill: var(--Spinner-color); + fill: currentColor; } } @@ -116,7 +117,7 @@ word-break: keep-all; white-space: nowrap; font-size: var(--spinner-base-fontSize); - color: var(--Spinner-color); + color: inherit; font-weight: var(--spinner-base-fontWeight); } diff --git a/packages/amis-ui/src/components/Spinner.tsx b/packages/amis-ui/src/components/Spinner.tsx index ef1db54fe2c..4ef851d91e0 100644 --- a/packages/amis-ui/src/components/Spinner.tsx +++ b/packages/amis-ui/src/components/Spinner.tsx @@ -43,6 +43,13 @@ export interface SpinnerExtraProps { loadingConfig?: { root?: string; show?: boolean; + icon?: string; + tip?: string; + tipPlacement?: 'top' | 'right' | 'bottom' | 'left'; + delay?: number; + size?: 'sm' | 'lg' | ''; + className?: string; + spinnerClassName?: string; }; } @@ -110,10 +117,10 @@ export class Spinner extends React.Component< show: true, className: '', spinnerClassName: '', - size: '' as '', + size: '' as const, icon: '', tip: '', - tipPlacement: 'bottom' as 'bottom', + tipPlacement: 'bottom' as const, delay: 0, overlay: false, loadingConfig: {}, @@ -199,17 +206,27 @@ export class Spinner extends React.Component< renderBody() { const { classnames: cx, - className, - spinnerClassName, - size = '', + className: propClassName, + spinnerClassName: propSpinnerClassName, overlay, - delay, - icon: iconConfig, - tip, - tipPlacement = '', loadingConfig, - disabled + disabled, + size: propSize, + tip: propTip, + tipPlacement: propTipPlacement, + delay: propDelay, + icon: propIcon } = this.props; + + const size = propSize || loadingConfig?.size || ''; + const tip = propTip || loadingConfig?.tip || ''; + const tipPlacement = propTipPlacement || loadingConfig?.tipPlacement || ''; + const delay = propDelay ?? loadingConfig?.delay ?? 0; + const iconConfig = propIcon || loadingConfig?.icon || ''; + const className = propClassName || loadingConfig?.className || ''; + const spinnerClassName = + propSpinnerClassName || loadingConfig?.spinnerClassName || ''; + // 定义了挂载位置时只能使用默认icon const icon = loadingConfig?.root ? undefined : iconConfig; const isCustomIcon = icon && React.isValidElement(icon); diff --git a/packages/amis/src/renderers/CRUD.tsx b/packages/amis/src/renderers/CRUD.tsx index 690ec713d23..ce9e874e986 100644 --- a/packages/amis/src/renderers/CRUD.tsx +++ b/packages/amis/src/renderers/CRUD.tsx @@ -3194,7 +3194,11 @@ export default class CRUD extends React.Component { headerToolbarRender: this.renderHeaderToolbar, footerToolbarRender: this.renderFooterToolbar, data: store.mergedData, - loading: store.loading, + loading: this.props.loading === false ? false : store.loading, + loadingConfig: { + ...this.props.loadingConfig, + ...(typeof this.props.loading === 'object' ? this.props.loading : {}) + }, offset: store.offset, host: this, filterItemIndex: this.filterItemIndex, @@ -3220,7 +3224,7 @@ export default class CRUD extends React.Component { return (
extends React.Component< onAction: this.handleAction, dispatchEvent: this.dispatchEvent, data: store.mergedData, - loading: store.loading, + loading: this.props.loading === false ? false : store.loading, + loadingConfig: { + ...this.props.loadingConfig, + ...(typeof this.props.loading === 'object' ? this.props.loading : {}) + }, host: this } ); @@ -1749,7 +1753,7 @@ export default class CRUD2 extends React.Component< return (
extends React.Component< translate={__} onRefresh={this.handlePullRefresh} direction={pullRefresh.gestureDirection ?? 'up'} - loading={store.loading} + loading={this.props.loading === false ? false : store.loading} completed={ !store.loading && store.lastPage > 0 && diff --git a/packages/amis/src/renderers/Table/index.tsx b/packages/amis/src/renderers/Table/index.tsx index 48de66eff91..ffdd2ddb0b0 100644 --- a/packages/amis/src/renderers/Table/index.tsx +++ b/packages/amis/src/renderers/Table/index.tsx @@ -497,7 +497,7 @@ export interface TableProps extends RendererProps, SpinnerExtraProps { canAccessSuperData?: boolean; reUseRow?: boolean; itemBadge?: BadgeObject; - loading?: boolean; + loading?: boolean | any; autoFillHeight?: boolean | AutoFillHeightObject; } @@ -2959,7 +2959,13 @@ export default class Table< translate={translate} dispatchEvent={dispatchEvent} onEvent={onEvent} - loading={store.loading} // store 的同步较慢,所以统一用 store 来下发,否则会出现 props 和 store 变化触发子节点两次 re-rerender + loading={this.props.loading === false ? false : store.loading} + loadingConfig={{ + ...loadingConfig, + ...(typeof this.props.loading === 'object' + ? this.props.loading + : {}) + }} > {renderItemActions({ store, @@ -2969,7 +2975,16 @@ export default class Table< })} - + ); } From 7beedc22da20b009ac46e5daf33a2a644d2e3531 Mon Sep 17 00:00:00 2001 From: ylturbo <1020311652@qq.com> Date: Mon, 20 Apr 2026 14:18:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E5=AF=BC=E5=87=BAexcel=E6=94=AF?= =?UTF-8?q?=E6=8C=81images=E5=88=97=EF=BC=8C=E5=B9=B6=E9=80=82=E5=BA=94?= =?UTF-8?q?=E8=A1=8C=E9=AB=98=E5=92=8C=E5=88=97=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../amis/src/renderers/Table/exportExcel.ts | 169 +++++++++++++----- 1 file changed, 126 insertions(+), 43 deletions(-) diff --git a/packages/amis/src/renderers/Table/exportExcel.ts b/packages/amis/src/renderers/Table/exportExcel.ts index da04aa894db..1a7dbaa8536 100644 --- a/packages/amis/src/renderers/Table/exportExcel.ts +++ b/packages/amis/src/renderers/Table/exportExcel.ts @@ -402,6 +402,8 @@ export async function exportExcel( } // 用于 mapping source 的情况 const remoteMappingCache: any = {}; + // 用于记录列宽(用于展宽图片列) + const maxColumnWidths: Record = {}; // 数据从第二行开始 let rowIndex = 1; if (toolbar.rowSlice) { @@ -415,6 +417,7 @@ export async function exportExcel( const rowData = createObject(data, row.data); rowIndex += 1; const sheetRow = worksheet.getRow(rowIndex); + let maxRowHeight = 0; let columIndex = 0; for (const column of filteredColumns) { columIndex += 1; @@ -442,53 +445,121 @@ export async function exportExcel( const type = (column as AMISSchema).type || 'plain'; // TODO: 这里很多组件都是拷贝对应渲染的逻辑实现的,导致每种都得实现一遍 - if ((type === 'image' || (type as any) === 'static-image') && value) { - try { - const imageData = await toDataURL(value); - const imageDimensions = await getImageDimensions(imageData); - let imageWidth = imageDimensions.width; - let imageHeight = imageDimensions.height; - // 限制一下图片高宽 - const imageMaxSize = 100; - if (imageWidth > imageHeight) { - if (imageWidth > imageMaxSize) { - imageHeight = (imageMaxSize * imageHeight) / imageWidth; - imageWidth = imageMaxSize; - } + if ( + (type === 'image' || + (type as any) === 'static-image' || + type === 'images' || + (type as any) === 'static-images') && + value + ) { + let list: Array = []; + if (type === 'images' || (type as any) === 'static-images') { + const delimiter = column.pristine.delimiter || ','; + if (typeof value === 'string') { + list = value.split(delimiter); + } else if (Array.isArray(value)) { + list = value; } else { - if (imageHeight > imageMaxSize) { - imageWidth = (imageMaxSize * imageWidth) / imageHeight; - imageHeight = imageMaxSize; - } - } - const imageMatch = imageData.match(/data:image\/(.*);/); - let imageExt = 'png'; - if (imageMatch) { - imageExt = imageMatch[1]; + list = [value]; } - // 目前 excel 只支持这些格式,所以其它格式直接输出 url - if (imageExt != 'png' && imageExt != 'jpeg' && imageExt != 'gif') { - sheetRow.getCell(columIndex).value = value; - continue; + } else { + list = [value]; + } + + const srcKey = column.pristine.src || 'image'; + const gap = 5; // 图片间距(像素) + + // 第一遍:收集所有图片数据和尺寸 + const imageItems: Array<{ + imageId: number; + imageWidth: number; + imageHeight: number; + linkURL: string; + }> = []; + for (const item of list) { + const src = + typeof item === 'string' + ? item + : (item && (item[srcKey] || item.src)) || item; + if (!src) continue; + + try { + const imageData = await toDataURL(src); + const imageDimensions = await getImageDimensions(imageData); + let imageWidth = imageDimensions.width; + let imageHeight = imageDimensions.height; + // 限制一下图片高宽 + const imageMaxSize = 100; + if (imageWidth > imageHeight) { + if (imageWidth > imageMaxSize) { + imageHeight = (imageMaxSize * imageHeight) / imageWidth; + imageWidth = imageMaxSize; + } + } else { + if (imageHeight > imageMaxSize) { + imageWidth = (imageMaxSize * imageWidth) / imageHeight; + imageHeight = imageMaxSize; + } + } + const imageMatch = imageData.match(/data:image\/(.*);/); + let imageExt = 'png'; + if (imageMatch) { + imageExt = imageMatch[1]; + if (imageExt === 'jpg') { + imageExt = 'jpeg'; + } + } + // 目前 excel 只支持这些格式,所以其它格式直接输出 url + if (imageExt != 'png' && imageExt != 'jpeg' && imageExt != 'gif') { + sheetRow.getCell(columIndex).value = src; + continue; + } + const imageId = workbook.addImage({ + base64: imageData, + extension: imageExt as any + }); + imageItems.push({ + imageId, + imageWidth, + imageHeight, + linkURL: getAbsoluteUrl(src) + }); + maxRowHeight = Math.max(maxRowHeight, imageHeight * 0.75); + } catch (e) { + console.warn(e); + sheetRow.getCell(columIndex).value = src; } - const imageId = workbook.addImage({ - base64: imageData, - extension: imageExt - }); - const linkURL = getAbsoluteUrl(value); - worksheet.addImage(imageId, { - // 这里坐标位置是从 0 开始的,所以要减一 - tl: {col: columIndex - 1, row: rowIndex - 1}, - ext: { - width: imageWidth, - height: imageHeight + } + + if (imageItems.length === 0) continue; + + // 计算总像素宽度(用于列宽自适应) + const totalPixelWidth = + imageItems.reduce((sum, img) => sum + img.imageWidth, 0) + + Math.max(0, imageItems.length - 1) * gap; + + // Excel 列宽单位约等于 7 像素,更新最大列宽记录 + maxColumnWidths[columIndex] = Math.max( + maxColumnWidths[columIndex] || 0, + totalPixelWidth / 7 + ); + + // 使用 nativeColOff(EMU 单位,1 像素 = 9525 EMU)直接写入 XML + // 这种方式完全绕过 exceljs 内部的 col 宽度依赖计算,定位最为准确 + let xOffset = 0; + for (const img of imageItems) { + worksheet.addImage(img.imageId, { + tl: { + nativeCol: columIndex - 1, + nativeColOff: Math.round(xOffset * 9525), + nativeRow: rowIndex - 1, + nativeRowOff: 0 }, - hyperlinks: { - tooltip: linkURL - } - }); - } catch (e) { - console.warn(e); + ext: {width: img.imageWidth, height: img.imageHeight}, + editAs: 'oneCell', + hyperlinks: {tooltip: img.linkURL} + } as any); + xOffset += img.imageWidth + gap; } } else if (type == 'link' || (type as any) === 'static-link') { const href = column.pristine.href; @@ -620,8 +691,20 @@ export async function exportExcel( sheetRow.getCell(columIndex).numFmt = '0'; } } + + if (maxRowHeight) { + sheetRow.height = maxRowHeight; + } } + // 应用列宽 + Object.keys(maxColumnWidths).forEach(colIndex => { + const width = maxColumnWidths[parseInt(colIndex, 10)]; + if (width > 0) { + worksheet.getColumn(parseInt(colIndex, 10)).width = width; + } + }); + // 后置总结行 renderSummary(worksheet, data, affixRow, rowIndex);