From 1411b5960610c1e5dd55bdfa4b1fd7764f13645e Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Wed, 21 May 2025 20:10:34 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=90=9C=E7=B4=A2=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E6=96=B0=E5=A2=9E=E9=AB=98=E7=BA=A7=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 + src/control/form/form/form.tsx | 15 ++- .../advance-search/advance-search.scss | 28 ++++ .../advance-search/advance-search.tsx | 124 ++++++++++++++++++ src/control/form/search-form/search-form.tsx | 31 ++++- src/locale/en/index.ts | 1 + src/locale/zh-CN/index.ts | 1 + .../searchform-buttons.controller.ts | 11 ++ .../searchform-buttons.scss | 7 +- .../searchform-buttons/searchform-buttons.tsx | 13 +- 10 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 src/control/form/search-form/advance-search/advance-search.scss create mode 100644 src/control/form/search-form/advance-search/advance-search.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acbcfcd1..6507a533e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [Unreleased] +### Added + +- 搜索表单新增高级搜索功能 + ## [0.7.40-alpha.19] - 2025-05-20 ### Added diff --git a/src/control/form/form/form.tsx b/src/control/form/form/form.tsx index e1a53cfc8..cbde3be12 100644 --- a/src/control/form/form/form.tsx +++ b/src/control/form/form/form.tsx @@ -5,7 +5,7 @@ import { ScriptFactory, } from '@ibiz-template/runtime'; import { useNamespace } from '@ibiz-template/vue3-util'; -import { IDEFormDetail, IDEFormItem } from '@ibiz/model-core'; +import { IDEFormDetail, IDESearchForm } from '@ibiz/model-core'; import { defineComponent, h, @@ -67,10 +67,17 @@ export const FormControl = defineComponent({ */ const renderByDetailType = ( detail: IDEFormDetail, + isRoot: boolean = false, ): VNode | VNode[] | undefined => { - if ((detail as IDEFormItem).hidden) { + const { hidden, userTag } = detail as IParams; + // 启用高级搜索时默认只显示常驻项 + if ( + hidden || + (!isRoot && + (c.model as IDESearchForm).enableAdvanceSearch && + userTag !== 'permanent') + ) return; - } const detailId = detail.id!; // 有插槽走插槽 @@ -160,7 +167,7 @@ export const FormControl = defineComponent({ return ( {this.c.model.deformPages?.map(page => { - return this.renderByDetailType(page); + return this.renderByDetailType(page, true); })} ); diff --git a/src/control/form/search-form/advance-search/advance-search.scss b/src/control/form/search-form/advance-search/advance-search.scss new file mode 100644 index 000000000..d37ee3235 --- /dev/null +++ b/src/control/form/search-form/advance-search/advance-search.scss @@ -0,0 +1,28 @@ +@include b(advance-search) { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: getCssVar('spacing', 'base'); + overflow: auto; + @include e('header') { + display: flex; + align-items: center; + height: getCssVar('height-control', 'large'); + padding: 0 0 getCssVar('spacing', 'base'); + overflow: hidden; + font-size: getCssVar('font-size', 'header-4'); + white-space: nowrap; + cursor: move; + } + @include e('search-form') { + width: 100%; + height: calc(100% - #{getCssVar('height-control', 'large')} * 2); + overflow: auto; + } + @include e('footer') { + display: flex; + justify-content: flex-end; + padding: getCssVar('spacing', 'base') 0 0 0; + } +} \ No newline at end of file diff --git a/src/control/form/search-form/advance-search/advance-search.tsx b/src/control/form/search-form/advance-search/advance-search.tsx new file mode 100644 index 000000000..34d976311 --- /dev/null +++ b/src/control/form/search-form/advance-search/advance-search.tsx @@ -0,0 +1,124 @@ +import { h, VNode, PropType, defineComponent, resolveComponent } from 'vue'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import { IDEFormDetail } from '@ibiz/model-core'; +import { + IModal, + ScriptFactory, + SearchFormController, + findChildFormDetails, +} from '@ibiz-template/runtime'; +import './advance-search.scss'; + +export const AdvanceSearch = defineComponent({ + name: 'IBizAdvanceSearch', + props: { + controller: { + type: Object as PropType, + required: true, + }, + modal: { type: Object as PropType }, + }, + setup(props) { + const ns = useNamespace('advance-search'); + const c = props.controller; + + /** + * 绘制成员的attrs + * @param {IDEFormDetail} model + * @return {*} {IParams} + */ + const renderAttrs = (model: IDEFormDetail): IParams => { + const attrs: IParams = {}; + model.controlAttributes?.forEach(item => { + if (item.attrName && item.attrValue) { + attrs[item.attrName!] = ScriptFactory.execSingleLine( + item.attrValue!, + { + ...c.getEventArgs(), + data: c.data, + }, + ); + } + }); + return attrs; + }; + + /** + * 按照类型绘制表单成员 + * @param {IDEFormDetail} detail + * @return {*} {(VNode | VNode[] | undefined)} + */ + const renderByDetailType = ( + detail: IDEFormDetail, + ): VNode | VNode[] | undefined => { + const { hidden, userTag } = detail as IParams; + // 根据用户标记判断表单项是否为常驻项,必须要启用高级搜索 + if (hidden || userTag === 'permanent') return; + const detailId = detail.id!; + // 子插槽 + const childSlots: IData = {}; + const childDetails = findChildFormDetails(detail); + if (childDetails.length) { + // 容器成员绘制子成员 + childSlots.default = (): (VNode[] | VNode | undefined)[] => + childDetails.map(child => { + return renderByDetailType(child); + }); + } + // 根据适配器绘制表单成员 + const provider = c.providers[detailId]; + if (!provider) { + return ( +
+ {ibiz.i18n.t('control.form.noSupportDetailType', { + detailType: detail.detailType, + })} +
+ ); + } + const component = resolveComponent(provider.component) as string; + return h( + component, + { + modelData: detail, + controller: c.details[detailId], + key: detail.id, + attrs: renderAttrs(detail), + }, + childSlots, + ); + }; + + return { + ns, + renderByDetailType, + }; + }, + render() { + return ( +
+
+ {ibiz.i18n.t('app.advanceSearch')} +
+
+ + {this.controller.model.deformPages?.map(page => { + return this.renderByDetailType(page); + })} + +
+
+ this.controller.search()}> + {ibiz.i18n.t('app.search')} + + this.controller.reset()}> + {ibiz.i18n.t('app.reset')} + +
+
+ ); + }, +}); diff --git a/src/control/form/search-form/search-form.tsx b/src/control/form/search-form/search-form.tsx index b12584d4b..d50bc9b43 100644 --- a/src/control/form/search-form/search-form.tsx +++ b/src/control/form/search-form/search-form.tsx @@ -1,7 +1,13 @@ -import { IControlProvider, SearchFormController } from '@ibiz-template/runtime'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + IControlProvider, + IModal, + SearchFormController, +} from '@ibiz-template/runtime'; import { useControlController, useNamespace } from '@ibiz-template/vue3-util'; import { IDESearchForm } from '@ibiz/model-core'; -import { defineComponent, PropType, reactive, watch } from 'vue'; +import { defineComponent, h, PropType, reactive, watch } from 'vue'; +import { AdvanceSearch } from './advance-search/advance-search'; import './search-form.scss'; export const SearchFormControl = defineComponent({ @@ -78,6 +84,27 @@ export const SearchFormControl = defineComponent({ }); }); + const openAdvanceSearch = async () => { + const overlay = ibiz.overlay.createModal( + (modal: IModal) => { + return h(AdvanceSearch, { + controller: c, + modal, + }); + }, + {}, + { + width: '800px', + height: 'auto', + closeOnClickModal: false, + } as any, + ); + overlay.present(); + await overlay.onWillDismiss(); + }; + + c.evt.on('openAdvanceSearch', () => openAdvanceSearch()); + return { c, ns }; }, diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 3f050a131..c73f71f1c 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -25,6 +25,7 @@ export default { newlyBuild: 'Newly build', reset: 'Reset', search: 'Search', + advanceSearch: 'Advance Search', rememberMe: 'Remember me', retract: 'Retract', pleaseEnterAccount: 'Please enter account number', diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 7033fafa8..36a375276 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -25,6 +25,7 @@ export default { newlyBuild: '新建', reset: '重置', search: '搜索', + advanceSearch: '高级搜索', rememberMe: '记住我', retract: '收起', pleaseEnterAccount: '请输入账号', diff --git a/src/panel-component/searchform-buttons/searchform-buttons.controller.ts b/src/panel-component/searchform-buttons/searchform-buttons.controller.ts index d819d36d6..816536e97 100644 --- a/src/panel-component/searchform-buttons/searchform-buttons.controller.ts +++ b/src/panel-component/searchform-buttons/searchform-buttons.controller.ts @@ -26,6 +26,17 @@ export class SearchFormButtonsController extends PanelItemController { return this.searchFrom.model.searchButtonStyle || 'DEFAULT'; } + /** + * 高级搜索 + * + * @readonly + * @type {boolean} + * @memberof SearchFormButtonsController + */ + get advanceSearch(): boolean { + return !!this.searchFrom.model.enableAdvanceSearch; + } + /** * @description 保存的过滤条件 * @exposedoc diff --git a/src/panel-component/searchform-buttons/searchform-buttons.scss b/src/panel-component/searchform-buttons/searchform-buttons.scss index 370bf8501..225831d46 100644 --- a/src/panel-component/searchform-buttons/searchform-buttons.scss +++ b/src/panel-component/searchform-buttons/searchform-buttons.scss @@ -5,11 +5,16 @@ $searchform-buttons: ( @include b(searchform-buttons) { @include set-component-css-var('searchform-buttons', $searchform-buttons); + display: flex; + gap: getCssVar(spacing, tight); + align-items: center; height: getCssVar(searchform-buttons, height); margin-top: getCssVar(spacing, tight); margin-left: getCssVar(spacing, tight); - + .el-button+.el-button { + margin-left: 0; + } } diff --git a/src/panel-component/searchform-buttons/searchform-buttons.tsx b/src/panel-component/searchform-buttons/searchform-buttons.tsx index 194c569cf..c5dfd2312 100644 --- a/src/panel-component/searchform-buttons/searchform-buttons.tsx +++ b/src/panel-component/searchform-buttons/searchform-buttons.tsx @@ -41,6 +41,7 @@ export const SearchFormButtons = defineComponent({ } c.onSearchButtonClick(); }; + const onResetButtonClick = () => { c.onResetButtonClick(); }; @@ -58,12 +59,17 @@ export const SearchFormButtons = defineComponent({ }); }; + const onAdvanceSearch = () => { + c.searchFrom.evt.emit('openAdvanceSearch', undefined); + }; + return { - ns, c, + ns, onSearchButtonClick, onResetButtonClick, saveFilterConfirm, + onAdvanceSearch, }; }, render() { @@ -137,6 +143,11 @@ export const SearchFormButtons = defineComponent({ }} )} + {this.controller.advanceSearch && ( + + {ibiz.i18n.t('app.advanceSearch')} + + )} ); }, -- Gitee From 9809540e245d1e15c8a5873542dbceecd837f671 Mon Sep 17 00:00:00 2001 From: ShineKOT <1917095344@qq.com> Date: Wed, 21 May 2025 20:12:20 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E5=88=97=E9=99=84=E4=BB=B6=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 + .../attachment-column/attachment-column.scss | 39 ++++ .../attachment-column/attachment-column.tsx | 144 ++++++++++++ .../attachment-column/file-util.ts | 205 ++++++++++++++++++ .../grid-field-column/grid-field-column.tsx | 12 + .../grid-column/grid-field-column/index.ts | 6 +- src/control/grid/grid/grid-control.util.ts | 113 +++++++++- src/control/grid/grid/grid.tsx | 22 +- 8 files changed, 529 insertions(+), 14 deletions(-) create mode 100644 src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.scss create mode 100644 src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.tsx create mode 100644 src/control/grid/grid-column/grid-field-column/attachment-column/file-util.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6507a533e..f4fd6443f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### Added - 搜索表单新增高级搜索功能 +- 表格新增tab切换换行能力 +- 新增表格列附件查看 ## [0.7.40-alpha.19] - 2025-05-20 diff --git a/src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.scss b/src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.scss new file mode 100644 index 000000000..69c3b910e --- /dev/null +++ b/src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.scss @@ -0,0 +1,39 @@ +@include b(attachment-column) { + display: flex; + flex-wrap: wrap; + gap: getCssVar('spacing', 'extra-tight'); + width: 100%; + @include e(file) { + display: flex; + align-items: center; + max-width: 100%; + min-height: getCssVar('height-control', 'default'); + max-height: getCssVar('height-control', 'default'); + color: getCssVar('color', 'primary'); + @include m(img) { + width: auto; + max-width: getCssVar('height-control', 'default'); + max-height: getCssVar('height-control', 'default'); + cursor: zoom-in; + } + } + @include e(image-preview) { + width: 100%; + height: 100%; + padding: getCssVar('spacing', 'base'); + @include m(container) { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + + img { + width: 100%; + max-width: 100%; + max-height: 100%; + padding: getCssVar('spacing', 'loose'); + } + } + } +} diff --git a/src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.tsx b/src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.tsx new file mode 100644 index 000000000..de9dfd7de --- /dev/null +++ b/src/control/grid/grid-column/grid-field-column/attachment-column/attachment-column.tsx @@ -0,0 +1,144 @@ +import { PropType, defineComponent, ref } from 'vue'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import { ControlVO, GridFieldColumnController } from '@ibiz-template/runtime'; +import { getFileType, useFilesParse } from './file-util'; +import './attachment-column.scss'; + +export const AttachmentColumn = defineComponent({ + name: 'IBizAttachmentColumn', + props: { + value: { + type: [String, Array], + required: true, + }, + data: { + type: Object as PropType, + required: true, + }, + controller: { + type: Object as PropType, + required: true, + }, + }, + setup(props) { + const ns = useNamespace('attachment-column'); + const { downloadUrl, files, onDownload } = useFilesParse( + props, + props.controller, + ); + const loading = ref(true); + + const onLoad = (): void => { + loading.value = false; + }; + + const renderImagePreview = (file: IData): JSX.Element => { + return ( +
+
+ -- +
+
+ ); + }; + + /** 处理图片预览 */ + const handleImagePreview = async (file: IData): Promise => { + const overlay = ibiz.overlay.createModal( + renderImagePreview(file), + undefined, + { + modalClass: ns.e('image-preview'), + width: 700, + height: 'auto', + }, + ); + overlay.present(); + await overlay.onWillDismiss(); + }; + + /** 处理PDF预览 */ + const handlePDFPreview = async (file: IData): Promise => { + const url = file.url || downloadUrl.value.replace('%fileId%', file.id); + // 适配 url 拉起的文件没有 Content-Type 的情况。没有 Content-Type , link.click()会直接调用下载逻辑 + const response = await ibiz.net.request(url, { + method: 'get', + responseType: 'blob', + baseURL: '', // 已经有baseURL了,这里无需再写 + }); + if (response.data) { + const blob = new Blob([response.data as Blob], { + type: 'application/pdf', + }); + // 创建对象 URL + const objectURL = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = objectURL; + link.target = '_blank'; + link.style.cursor = 'pointer'; + link.textContent = file.name || ''; + const handleClick = function (): void { + link.removeEventListener('click', handleClick); + document.body.removeChild(link); + }; + link.addEventListener('click', handleClick); + document.body.appendChild(link); + link.click(); + } + }; + + const handleFileClick = (file: IData): void => { + const fileName = file.name; + const fileExtension = fileName.split('.').pop()?.toLowerCase() ?? ''; + switch (getFileType(fileExtension)) { + case 'image': + // 打开图片预览 + handleImagePreview(file); + break; + case 'pdf': + // 打开pdf预览 + handlePDFPreview(file); + break; + case 'other': + default: + // 直接下载文件 + onDownload(file); + break; + } + }; + + return { + ns, + files, + handleFileClick, + }; + }, + render() { + return ( +
+ {this.files.map(file => { + const fileName = file.name; + const fileExtension = fileName.split('.').pop()?.toLowerCase() ?? ''; + const fileType = getFileType(fileExtension); + return ( +
this.handleFileClick(file)} + > + {fileType === 'image' ? ( + + ) : ( + file.name + )} +
+ ); + })} +
+ ); + }, +}); diff --git a/src/control/grid/grid-column/grid-field-column/attachment-column/file-util.ts b/src/control/grid/grid-column/grid-field-column/attachment-column/file-util.ts new file mode 100644 index 000000000..8ea90e576 --- /dev/null +++ b/src/control/grid/grid-column/grid-field-column/attachment-column/file-util.ts @@ -0,0 +1,205 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Ref, ref, watch } from 'vue'; +import { isString } from 'lodash-es'; +import { GridFieldColumnController } from '@ibiz-template/runtime'; + +/** 文件总类型定义 */ +type FileType = 'image' | 'pdf' | 'other'; + +/** + * 获取文件类型 + */ +export const getFileType = (extension: string): FileType => { + switch (extension) { + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': + case 'bmp': + case 'svg': + case 'webp': + case 'jfif': + // 图片类型 + return 'image'; + case 'pdf': + // pdf 类型 + return 'pdf'; + default: + // 其它文件类型 + return 'other'; + } +}; + +/** + * 文件内容解析的适配逻辑 + * + * @export + * @param {IParams} props + * @param {UploadEditorController} c + * @returns {*} + */ +export function useFilesParse( + props: IParams, + c: GridFieldColumnController, +): { + uploadUrl: Ref; + downloadUrl: Ref; + files: Ref< + { + id: string; + name: string; + url?: string | undefined; + base64?: string | undefined; + }[] + >; + onDownload: (file: IData) => void; +} { + // 文件列表 + const files: Ref< + { + id: string; + name: string; + url?: string; + base64?: string; + }[] + > = ref([]); + + // 上传文件路径 + const uploadUrl: Ref = ref(''); + + // 下载文件路径 + const downloadUrl: Ref = ref(''); + + // svg图片Blob路径存储 + const svgBlob: Map = new Map(); + + // 获取svg以data:image开头的预览路径 + const fetchSVGAsBase64 = async (url: string): Promise => { + try { + // 发起请求获取 SVG 文件 + const response = await fetch(url); + + // 检查响应状态 + if (!response.ok) { + throw new Error(`Failed to fetch SVG: ${response.status}`); + } + + // 读取响应体为 Blob + const blob = await response.blob(); + + // 将 Blob 转换为 base-64 编码的字符串 + const reader = new FileReader(); + return new Promise((resolve, reject) => { + reader.onloadend = (): void => { + const base64String = reader.result as string; + // 添加正确的 MIME 类型前缀 + if (base64String) { + const dataURL = base64String.replace( + 'data:application/octet-stream;base64', + 'data:image/svg+xml;base64', + ); + resolve(dataURL); + } + }; + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (error) { + ibiz.log.error((error as IData)?.message); + throw error; + } + }; + + // 计算svg的预览路径 + const calcSvgPreview = (file: IData): void => { + const blob = svgBlob.get(file.id); + if (blob) { + Object.assign(file, { base64: blob }); + } else { + fetchSVGAsBase64(file.url).then(base64String => { + Object.assign(file, { base64: base64String }); + }); + } + }; + + // 下载文件 + const onDownload = (file: IData): void => { + const url = file.url || downloadUrl.value.replace('%fileId%', file.id); + ibiz.util.file.fileDownload(url, file.name); + }; + + // 值响应式变更 + watch( + () => props.value, + newVal => { + // eslint-disable-next-line no-nested-ternary + files.value = !newVal + ? [] + : isString(newVal) + ? JSON.parse(newVal) + : newVal; + }, + { immediate: true }, + ); + + // data响应式变更基础路径 + watch( + () => props.data, + newVal => { + if (newVal) { + const urls = ibiz.util.file.calcFileUpDownUrl( + c.context, + c.params, + newVal, + {}, + ); + uploadUrl.value = urls.uploadUrl; + downloadUrl.value = urls.downloadUrl; + } + }, + { immediate: true, deep: true }, + ); + + watch( + files, + newVal => { + // 变更后且下载基础路径存在时解析 + if (newVal?.length && downloadUrl.value) { + newVal.forEach((file: IData) => { + Object.assign(file, { + url: file.url || downloadUrl.value.replace('%fileId%', file.id), + }); + if (file.name.split('.').pop() === 'svg') { + calcSvgPreview(file); + } + }); + } + }, + { immediate: true }, + ); + + watch( + downloadUrl, + newVal => { + // 变更后且下载基础路径存在时解析 + if (newVal && files.value.length) { + files.value.forEach((file: IData) => { + Object.assign(file, { + url: downloadUrl.value.replace('%fileId%', file.id), + }); + if (file.name.split('.').pop() === 'svg') { + calcSvgPreview(file); + } + }); + } + }, + { immediate: true }, + ); + + return { + files, + uploadUrl, + onDownload, + downloadUrl, + }; +} diff --git a/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx b/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx index 0535e3c35..08b08d981 100644 --- a/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx +++ b/src/control/grid/grid-column/grid-field-column/grid-field-column.tsx @@ -27,6 +27,9 @@ export const GridFieldColumn = defineComponent({ const ns = useNamespace('grid-field-column'); const zIndex = props.controller.grid.state.zIndex; + + const columnType = props.controller.model.userParam?.columnType; + /** * 单元格点击事件 * @@ -195,6 +198,7 @@ export const GridFieldColumn = defineComponent({ codeListValue, tooltip, zIndex, + columnType, codeListItems, hiddenEmpty, findLayoutPanel, @@ -244,6 +248,14 @@ export const GridFieldColumn = defineComponent({ title={showTitle(this.tooltip)} > ); + } else if (this.columnType === 'attachment') { + content = ( + + ); } else { content = ( new GridFieldColumnProvider(), diff --git a/src/control/grid/grid/grid-control.util.ts b/src/control/grid/grid/grid-control.util.ts index edf6a66a9..b81a55888 100644 --- a/src/control/grid/grid/grid-control.util.ts +++ b/src/control/grid/grid/grid-control.util.ts @@ -1,6 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-param-reassign */ -import { NOOP, Namespace, eventPath, listenJSEvent } from '@ibiz-template/core'; +import { + NOOP, + Namespace, + eventPath, + listenJSEvent, + recursiveIterate, +} from '@ibiz-template/core'; import { Srfuf, ControlVO, @@ -10,7 +17,7 @@ import { IControlProvider, IGridRowState, } from '@ibiz-template/runtime'; -import { IDEGrid, IDEGridColumn } from '@ibiz/model-core'; +import { IDEGrid, IDEGridColumn, IDEGridFieldColumn } from '@ibiz/model-core'; import { TableColumnCtx } from 'element-plus'; import { chunk, orderBy } from 'lodash-es'; import { @@ -120,6 +127,7 @@ export function useITableEvent(c: GridController): { }) => string; cleanClick: () => void; cleanEnter: () => void; + cleanTab: () => void; } { const tableRef = ref(); let forbidChange = false; @@ -129,6 +137,7 @@ export function useITableEvent(c: GridController): { let cleanClick = NOOP; let cleanEnter = NOOP; + let cleanTab = NOOP; if (c.state.isAutoGrid) { // 行编辑模式才监听 @@ -189,6 +198,105 @@ export function useITableEvent(c: GridController): { } }); + // 可编辑属性列 + const editColumn = computed(() => { + const columns: IDEGridFieldColumn[] = []; + recursiveIterate({ children: c.state.columnStates }, columnState => { + if (!columnState.uaColumn && !columnState.hidden) { + const model = c.columns[columnState.key].model; + if (model.enableRowEdit) columns.push(model); + } + }); + return columns; + }); + + const tabListener = (event: KeyboardEvent): void => { + const tableEl = tableRef.value!.$el; + const currentElement = document.activeElement as HTMLElement; + // 焦点在当前表格,表格启用编辑时才启用 + if (event.key === 'Tab' && tableEl.contains(currentElement)) { + // 阻止默认 + event.preventDefault(); + // 单元格编辑模式 + const classList: string[] = []; + eventPath(event as any).forEach(e => { + if (e && (e as IData).classList) { + classList.push(...(e as IData).classList); + } + }); + // 获取主键 + const key = classList + .filter(className => className.startsWith('id-'))[0] + ?.substring(3); + const columnName = classList + .filter(className => className.startsWith('defgridcolumn-'))[0] + ?.substring(14); + const columnIndex = editColumn.value.findIndex( + column => column.id!.toLowerCase() === columnName?.toLowerCase(), + ); + // 不存在主键为新建行 + const rowIndex = c.state.rows.findIndex( + item => key && item.data.srfkey === key, + ); + const row = rowIndex !== -1 ? c.state.rows[rowIndex] : undefined; + // 如果当前行为新建行,则下一行默认为第一个非新建行 + const nextRow = + rowIndex !== -1 + ? c.state.rows[rowIndex + 1] + : c.state.rows.find(_row => _row.data.srfuf === Srfuf.UPDATE); + if (row) { + // 有下一个可编辑单元格或下一行 先关闭之前的编辑单元格 + if (nextRow || columnIndex < editColumn.value.length - 1) { + Object.keys(row.editColStates).forEach(fieldName => { + row.editColStates[fieldName].editable = false; + }); + } + // 打开下一个可编辑单元格 + if (columnIndex < editColumn.value.length - 1) { + row.editColStates[ + editColumn.value[columnIndex + 1].id!.toLowerCase() + ].editable = true; + } else if (nextRow) { + // 如果是这一行最后一个单元格则打开下一行第一个单元格 + nextRow.editColStates[ + editColumn.value[0].id!.toLowerCase() + ].editable = true; + } + } else { + // 新建行直接聚焦元素即可 + // 获取当前表格所有可聚焦元素 + const focusableElements = Array.from( + tableEl.querySelectorAll( + 'tbody select, tbody textarea, tbody input:not([type="checkbox"])', + ), + ) as HTMLElement[]; + const currentIndex = focusableElements.indexOf(currentElement); + // 下一个可聚焦元素 + const nextElement = focusableElements[currentIndex + 1]; + if (nextElement) { + nextElement.focus(); + } else if (nextRow) { + // 没有可聚焦元素时,下一行非新建行的首项启用编辑态 + nextRow.editColStates[ + editColumn.value[0].id!.toLowerCase() + ].editable = true; + } + } + } + }; + + watch( + () => tableRef.value, + table => { + const { enableRowEdit } = c.model; + if (table && enableRowEdit && c.editShowMode === 'cell') { + cleanTab = listenJSEvent(window, 'keydown', tabListener, { + capture: true, + }); + } + }, + ); + /** * 处理ctrl+click选中 * @@ -493,6 +601,7 @@ export function useITableEvent(c: GridController): { handleHeaderCellClassName, cleanClick, cleanEnter, + cleanTab, }; } diff --git a/src/control/grid/grid/grid.tsx b/src/control/grid/grid/grid.tsx index 26c1d806e..2ada6c957 100644 --- a/src/control/grid/grid/grid.tsx +++ b/src/control/grid/grid/grid.tsx @@ -100,6 +100,7 @@ export function renderFieldColumn( const panel = controlRenders.find( renderItem => renderItem.renderType === 'LAYOUTPANEL', )?.layoutPanel; + const columnType = c.model.userParam?.columnType; if (panel) { content = ( ); + // 特殊处理附件列 + } else if (columnType === 'attachment') { + content = ( + + ); } else { const showValue = c.formatValue(fieldValue); const tooltip = computed(() => { @@ -222,7 +228,7 @@ export function renderColumn( // 表格列自定义 return ( { zIndex.decrement(); - if (cleanup !== NOOP) { - cleanup(); - } - if (cleanClick !== NOOP) { - cleanClick(); - } - if (cleanEnter !== NOOP) { - cleanEnter(); - } + if (cleanup !== NOOP) cleanup(); + if (cleanClick !== NOOP) cleanClick(); + if (cleanEnter !== NOOP) cleanEnter(); + if (cleanTab !== NOOP) cleanTab(); }); // 是否可以加载更多 -- Gitee