UNPKG

@wiajs/ui

Version:

wia ui packages

1,402 lines (1,399 loc) 178 kB
/** @jsxImportSource @wiajs/core */ /** * 在线编辑表格 */ import { jsx as _jsx, jsxs as _jsxs } from "@wiajs/core/jsx-runtime"; import { Event } from '@wiajs/core'; import { isDate, promisify } from '@wiajs/core/util/tool'; import { log as Log } from '@wiajs/util'; import DataTable, { col, th } from '../dataTable'; import { edit as editAttach, fillAttach, view as viewAttach } from './attach'; import * as chip from './chip'; import * as tool from './tool'; const log = Log({ m: 'editTable' }) // 创建日志实例 ; /** * @typedef {import('jquery')} $ * @typedef {JQuery} Dom */ /** @typedef {object} Opts * @prop {Dom} [el] - contain * @prop {Dom} [tb] - $table * @prop {string} [name] * @prop {string[]} [editTag] - 可编辑元素标签 * @prop {boolean} [edit] - 编辑模式 * @prop {boolean} [newEdit] - 新编辑 * @prop {boolean} [add] - 新增模式 * @prop {boolean} [kv] - key value * @prop {number} [col] - 最大列数 * @prop {number[]} [colWidth] - 列宽 * @prop {number} [viewid] - 数据卡id * @prop {*} [upload] - 上传接口 * @prop {*} [updateJson] - 编辑卡片json * @prop {*[]} [head] - 非kv模式表头 * @prop {*[]} [data] - 非kv模式数据 * @prop {*[]} [use] - 插件,如果带data,则需带插件,否则附件加载有问题 */ // * @prop {{url:string, token: string, param:*}} [api] /** @typedef {object} Opt * @prop {Dom} tb - $table * @prop {Dom} el - contain * @prop {string} name * @prop {string} domProp * @prop {string[]} editTag * @prop {boolean} edit * @prop {boolean} newEdit * @prop {boolean} add * @prop {boolean} kv - key value * @prop {number} labelWidth - label 宽度 百分比 * @prop {number} col - 最大列数 * @prop {number[]} [colWidth] - 列宽 * @prop {number} [colRatio] - 列比 * @prop {number} [viewid] - 数据卡id * @prop {*} [upload] - 上传接口 * @prop {*} [prjid] - 项目id * @prop {*} [getSelAll] - 公司列表接口 * @prop {*} [saveEmbTb] - 保存表格接口 * @prop {*} [updateJson] - 编辑卡片json * @prop {*[]} [head] - 非kv模式表头 * @prop {*[]} [data] - 非kv模式数据 * @prop {*[]} [use] - 插件,如果带data,则需带插件,否则附件加载有问题 */ /** @type {Opt} */ const def = { domProp: 'wiaEditTable', tb: null, el: null, editTag: [ 'input', 'textarea' ], edit: false, newEdit: false, add: false, kv: false, labelWidth: 10, col: 8, // colWidth: [0.1, 0.15, 0.1, 0.15, 0.1, 0.15, 0.1, 0.15], colRatio: 1, name: null, head: null, data: null }; /** * @enum {number} 数据类型-页面呈现方式 */ const DataType = { null: 0, text: 1, texts: 2, number: 3, date: 4, bool: 5, select: 6, radio: 7, checkbox: 8, chip: 9, button: 10, img: 11, file: 12, path: 13, url: 14, email: 15, tel: 16, password: 17, time: 18, datetime: 19, month: 20, week: 21, color: 22, attach: 23, table: 24, view: 25, page: 26, search: 27, html: 28, json: 29 }; /** * @enum {string} 数据类型-页面呈现方式 */ const DataTypes = { null: 'null', text: 'text', texts: 'texts', number: 'number', date: 'date', bool: 'bool', select: 'select', radio: 'radio', checkbox: 'checkbox', chip: 'chip', button: 'button', img: 'img', file: 'file', path: 'path', url: 'url', email: 'email', tel: 'tel', password: 'password', time: 'time', datetime: 'datetime', month: 'month', week: 'week', color: 'color', attach: 'attach', table: 'table', view: 'view', page: 'page', search: 'search', html: 'html', json: 'json' }; /** * @enum {number} 状态 */ const State = { null: 0, view: 1, edit: 2, json: 3 }; /** * EditTable */ let EditTable = class EditTable extends Event { /** * 构造函数 * @param {Page} page Page 实例 * @param {Opts} opts */ constructor(page, opts){ super(opts, [ page ]), /** @type {*} */ this.editTx = null // 当前被编辑的目标对象 , /** @type {boolean} */ this._keyDown = false // 记录键盘被按下的状态,当有键盘按下时其值为true , /** @type {*} */ this._selRow = null // 最近的选择行 , /** @type {*} */ this._editCell = null // 当前编辑对象 , /** @type {*} */ this.editRow = null // 最后编辑行 , this.editCursorPos = 0 // 最后编辑光标位置 , this.rowNum = 0 // 表行数 , this.newNum = 0 // 新增行数 , this.dataid = 0 // 当前数据索引,用于render row , /** @type {State} */ this.state = State.null, this.sel = new Set() // 选择 , this.add = new Set() // 新增 , this.del = new Set() // 删除 , this.uses = new Set() // 插件 ; const _ = this; _.page = page; const opt = { ...def, ...opts }; _.opt = opt; const { el, tb: tb1, kv, head, data } = opt; // 是否为kv模式,非kv需带表头 if (kv && tb1) { _.tb = tb1; // 克隆数组数据,操作时,不改变原数据 if (data?.length) _.data = [ ...data ]; } else if (head && el) { _.head = opt.head; const cfg = { ...opt.head[0] || {} }; if (cfg.id) cfg.checkbox = cfg.id; else cfg.checkbox = 'index'; _.cfg = cfg; const fields = _.head.slice(1); // 创建 field 的深拷贝,避免修改原配置对象value后,导致原列表出问题 _.fields = fields.map((f)=>({ ...f })); _.lastW = window.innerWidth; _.lastH = window.innerHeight; // 已创建,直接返回 if (el.dom[opt.domProp]) { const instance = el.dom[opt.domProp]; _.destroy(); return instance; } el.dom[opt.domProp] = this; // 容器 _.el = el; // 克隆数组数据,操作时,不改变原数据 if (data) _.data = [ ...data ]; } // 4 改为 8,兼容旧模式 if (!opt.colWidth) { opt.colRatio = 2; opt.col = opt.col * 2; if (opt.col === 8) opt.colWidth = [ 0.1, 0.15, 0.1, 0.15, 0.1, 0.15, 0.1, 0.15 ]; } _.init(); if (opt.edit) _.edit(); else _.view(); _.bind(); // const txs = $(tr).find('input.etCellView') // const spans = $(tr).find('span.etLabel') // spans.html('hello') // for (const tx of txs.get()) { // const $tx = $(tx) // $tx.click(ev => editSpec(tx)) // $tx.focus(ev => editSpec(tx)) // $tx.blur(ev => viewSpec(tx)) // $tx.upper('td').addClass('border-bot') // } } static hi(msg) { alert(msg); } /** * kv模式,构建空表头、表体 */ init() { const _ = this; const { opt, tb: tb1 } = _; const { colWidth, edit, kv, use } = opt; try { // 加载数据之前,先加载插件 for (const u of use || [])if (u.cls) _.use(u.cls, u.opts); if (kv) { // 列宽控制 const cg = tb1.find('colgroup'); if (!cg.dom && colWidth?.length) { tb1.prepend(/*#__PURE__*/ _jsx("colgroup", { children: colWidth.map((v)=>{ let width; if (v < 1) width = `${v * 100}%`; else width = `${v}px`; return /*#__PURE__*/ _jsx("col", { style: `width: ${width}` }); }) })); } // 构造空body let body = tb1.find('tbody'); if (!body.dom) { if (edit) tb1.append(/*#__PURE__*/ _jsx("tbody", { class: "etEdit" })); else tb1.append(/*#__PURE__*/ _jsx("tbody", { class: "etView" })); } body = tb1.find('tbody'); // 构造空表头 const th = tb1.find('thead'); if (!th.dom) { body.before(/*#__PURE__*/ _jsx("thead", { children: /*#__PURE__*/ _jsx("tr", { style: "display: none", class: "etRowOdd" }) })); } // 数据视图 if (_.data?.length) _.setKv(); } else _.render(); } catch (e) { log.err(e, 'init'); } } /** * 生成edit table,包括 thead、tbody * @returns */ render() { const _ = this; try { const { el, opt, cfg, fields } = _; const { edit } = opt; const head = [ cfg, ...fields ]; if (!head) { console.log('param is null.'); return; } // checkbox const { checkbox: ck, layout, sum, fix } = cfg; if (fix.includes('table')) // 固定表格,上下滚动 el.append(/*#__PURE__*/ _jsx("div", { class: "data-table-content overflow-auto" })); else el.append(/*#__PURE__*/ _jsx("div", { class: "data-table-content" })); let ckv = ''; if (ck) { // checkbox if (Array.isArray(ck) && ck.length) { ckv = `\${r[${ck[0]}]}`; } else if (ck === 'index') ckv = '${r.index}'; } const { name } = opt; // 默认固定表头、表尾 const clas = [ 'edit-table', 'fix-h', 'fix-b' ]; if (fix.includes('right1')) // 固定表头 表尾 clas.push('fix-r1'); if (fix.includes('right2')) clas.push('fix-r2'); if (fix.includes('left1')) clas.push('fix-l1'); if (fix.includes('left2')) clas.push('fix-l2'); if (fix.includes('left3')) clas.push('fix-l3'); if (fix.includes('left4')) clas.push('fix-l4'); if (fix.includes('left5')) clas.push('fix-l5'); const style = [ `table-layout: ${layout}` ]; const tb1 = $(/*#__PURE__*/ _jsx("table", { name: name, class: clas.join(' '), style: style.join(';') })); // 保存tb _.tb = tb1; // 加入到容器 const tbWrap = el.findNode('.data-table-content'); tbWrap.append(tb1); // 列宽 tb1.append(/*#__PURE__*/ _jsx("colgroup", { children: col(head) })); // <table name="tbLoan"> // jsx 通过函数调用,实现html生成。 let v = th(head, false); // 加入到表格 tb1.append(v); const thead = tb1.tag('THEAD'); thead.append(/*#__PURE__*/ _jsx("tr", { name: `${name}-tp`, style: "display: none", children: ck && /*#__PURE__*/ _jsx("td", { class: "checkbox-cell", children: /*#__PURE__*/ _jsxs("label", { class: "checkbox", children: [ /*#__PURE__*/ _jsx("input", { type: "checkbox", "data-val": ckv }), /*#__PURE__*/ _jsx("i", { class: "icon-checkbox" }) ] }) }) })); // 表主体 v = /*#__PURE__*/ _jsx("tbody", { name: "tbBody", class: `${edit ? 'etEdit' : 'etView'}` }); // 加入到表格 tb1.append(v); _.header = el.findNode('.data-table-header'); _.$headerSel = el.findNode('.data-table-header-selected'); // 数据视图 if (_.data?.length) _.setView(); } catch (ex) { console.log('render', { ex: ex.message }); } } bind() { const _ = this; const { opt, fields, vals } = _; const { kv } = opt; // 表格点击事件 // 编辑元素(input) 不能 focus,不能 onblur?原因:pointer-events: none _.tb.click(async (ev)=>{ // 阻止冒泡,否则会莫名其妙的(内嵌表格编辑叠加到kv编辑,导致混乱)事件! // ev.preventDefault() ev.stopPropagation(); const $ev = $(ev); const th = $ev.upper('th'); if (th?.length) return; if (_.state !== State.edit) return; // 点击 input、select 则跳过 if ([ 'SELECT', 'INPUT' ].includes(ev.target.tagName)) return; const td = $ev.upper('td'); let span = $ev.upper('span'); span = td.find('span'); if (span.eq(0).css('display') === 'none') { // debugger return; } const idx = td?.data('idx') // 数据或字段索引 ; const idy = td?.data('idy') // 表编辑,多行数据行索引 ; const idv = td?.data('idv') // 数据中的value索引,多值数组模式下 ; const value = td?.attr('data-value') // 数据原值 data() 会自动转换 json 字符串 ; const r = fields?.[idx]; if (r) { if (r.read) return; // 只读 // 方法2.2(更可靠) // document.body.setAttribute('tabindex', '-1') // document.body.focus() // document.body.removeAttribute('tabindex') let type = r.type ?? DataType.text // 默认单行字符串 ; if (type === 'string') type = DataType.text; // 多值 if (idv && idv >= 0 && Array.isArray(type) && type[idv]) { type = type[idv]; } const inputType = _.getInputType(type); if (inputType) { const span = td.find('span'); span.hide(); let tx = td.find('input'); if (!tx.dom) { tx = document.createElement('input'); tx.name = r.field; tx.type = inputType; td.append(tx); tx = $(tx); tx.addClass('dy-input'); tx.blur((ev)=>{ // _.viewCell() const val = tx.val(); span.eq(0).html(val); // 比较值是否被修改 if (`${val}` === `${value}`) { tx.hide(); span.show(); td.removeClass('etChange'); } else { vals[idy][r.field] = val; td.addClass('etChange'); } }); } tx.val(span.eq(0).html()); tx.show(); tx.focus(); // 自动聚焦到输入框 // setTimeout(() => { // tx.focus() // }, 50) } else if (type === DataType.texts || type === DataTypes.texts) { const span = td.find('span'); if (!span.hasClass('edit')) { let tx = td.find('input'); if (!tx.dom) { tx = document.createElement('input'); tx.name = r.field; tx.value = span.html(); tx.hidden = true; td.append(tx); } span.dom.tabIndex = '-1'; // span 可编辑 // span.focus(ev => span.addClass('edit')) span.addClass('edit'); span.blur((ev)=>{ // _.viewCell() const val = span.html(); tx.value = val; if (`${val}` === `${value}`) { span.removeClass('edit') // span 可编辑 ; td.removeClass('etChange'); } else { vals[idy][r.field] = val; td.addClass('etChange'); } }); span.focus(); // span.dom.addEventListener('focusout', ev => { // tx.value = span.html() // span.removeClass('edit') // span 可编辑 // }) } } else if (type === DataType.select || type === DataTypes.select) { const span = td.find('span'); span.hide(); let key; let sel = td.find('select'); // 第一次创建 if (!sel.dom) { sel = document.createElement('select'); sel.name = r.field; td.append(sel); sel = $(sel); sel.addClass('dy-select dy-select-primary'); sel.click((ev)=>ev.stopPropagation()) // 阻止事件冒泡 tb 无感知? ; // tx.addClass('dy-input') // tx.val(span.html()) // tx.change(ev => { sel.blur((ev)=>{ // _.viewCell() let val; const { option } = r; if (Array.isArray(option)) val = sel.val(); else if (typeof option === 'object') { key = sel.val(); val = option[key]; } if (`${val}` === `${value}` || val === '') { sel.hide(); span.html(value) // 还原值 ; span.show(); td.removeClass('etChange'); } else { span.html(val) // 修改值 ; vals[idy][r.field] = val; td.addClass('etChange'); } }); sel.focus((ev)=>{ // 关联参数发生编号,重新查询 _.fillOption(r, td, sel, value, idy); }); } // 选项 await _.fillOption(r, td, sel, value, idy); sel.show(); sel.focus(); // 弹出下拉列表,基本无效! // 等待一帧,确保已渲染 requestAnimationFrame(()=>{ // 尝试用鼠标事件打开(Chromium 下通常有效) const ev = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }); sel.dom.dispatchEvent(ev); // 如果还不行,退一步触发键盘组合(某些浏览器用 Alt+↓ 打开) if (document.activeElement === sel) { const keyEv = new KeyboardEvent('keydown', { key: 'ArrowDown', altKey: true, bubbles: true }); sel.dom.dispatchEvent(keyEv); } }); } else if ((type === DataType.search || type === DataTypes.search) && _.Autocomplete) { const span = td.find('span'); // 切换到编辑模式 if (span.css('display') !== 'none') { span.hide(); let dvAc = td.find('.autocomplete'); let ac = dvAc.dom?._wiaAutocomplete; // 创建Ac if (!ac) { const { source, field, addUrl } = r; const { placeholder } = r; // td.append() dvAc = $(/*#__PURE__*/ _jsx("div", { class: "autocomplete" })).appendTo(td); // 保存当前字段 search回来的数据,表编辑时 其它行共享 if (!r.option) r.option = []; // 处理 source.param 中的 ${} 引用(与 select 组件保持一致) let processedSource = source; if (source && typeof source === 'object' && source.param) { let { param = {} } = source; // 查询参数 可引用其他字段值 param = _.parseRef(param, idy); processedSource = { ...source, param }; } // r.option = option // 不保存到字段定义,避免污染 // tx.addClass('dy-input') ac = new _.Autocomplete(_.page, { el: dvAc, data: r.option, name: field, value, placeholder, // refEl: [span.dom], // 点击该关联元素不关闭下拉列表,点击其他地方,关闭列表 source: processedSource, addUrl }); ac.on('blur', ()=>{ // 选择赋值在 blur 后 setTimeout(()=>{ const val = ac.val() //tx.val() ; if (`${val}` === `${value}` || val === '') { ac.hide(); span.eq(0).html(value) // 还原值 ; span.show(); td.removeClass('etChange'); } else { span.eq(0).html(val) // 修改值 ; vals[idy][r.field] = val; td.addClass('etChange'); } }, 200); }); } ac.show(); ac.focus() // 自动触发下拉 ; } } else if (type === DataType.bool || type === DataTypes.bool) { const span = td.find('span'); span.hide(); let val = span.html(); let key; let tx = td.find('select'); if (!tx.dom) { tx = document.createElement('select'); tx.name = r.field; td.append(tx); tx = $(tx); tx.addClass('dy-select dy-select-primary'); const option = { true: '是', false: '否' }; // 添加选项 const htm = []; for (const k of Object.keys(option)){ const v = option[k]; if (v === val) { key = k; htm.push(/*#__PURE__*/ _jsx("option", { selected: true, value: k, children: v })); } else htm.push(/*#__PURE__*/ _jsx("option", { value: k, children: v })); } tx.html(htm.join('')); if (key) tx.val(key); else tx.val(val); tx.click((ev)=>ev.stopPropagation()) // 阻止事件冒泡 ; // tx.addClass('dy-input') // tx.val(span.html()) tx.blur((ev)=>{ // _.viewCell() key = tx.val(); val = option[key]; span.html(val); if (`${val}` === `${value}`) { tx.hide(); span.show(); td.removeClass('etChange'); } else { vals[idy][r.field] = val; td.addClass('etChange'); } }); } tx.show(); tx.focus(); setTimeout(()=>{ // 创建并触发鼠标事件来展开下拉框 // const event = new MouseEvent('mousedown') const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window }); tx.dom.dispatchEvent(event); }, 100); // tx.click() } else if (type === DataType.url || type === DataTypes.url) { // urlChange const span = td.find('span'); span.hide(); let tx = td.find('input'); if (!tx.dom) { tx = document.createElement('input'); tx.name = r.field; tx.type = 'url'; td.append(tx); tx = $(tx); tx.addClass('dy-input'); tx.blur((ev)=>{ // _.viewCell() const val = tx.val(); span.eq(0).find('a').attr('href', val); // 比较值是否被修改 if (`${val}` === `${value}`) { tx.hide(); span.show(); td.removeClass('etChange'); } else { vals[idy][r.field] = val; td.addClass('etChange'); } }); } // urlChange tx.val(span.eq(0).find('a').attr('href')); tx.show(); tx.focus(); } } // for (const tag of _.opt.editTag) { // const tx = $ev.upper(tag) // if (tx.dom) { // _.editCell(tx.dom) // break // } // } }); // 处理checkbox _.tb.on('change', '.checkbox-cell input[type="checkbox"]', function(ev) { _.handleCheck(ev, this); }); _.tb.on('change', '.etCheckbox input[type="checkbox"]', (ev)=>{ const td = $(ev).upper('td'); const idx = td?.data('idx') // 数据或字段索引 ; const r = fields?.[idx]; const { type } = r; if ([ DataType.checkbox, DataTypes.checkbox ].includes(type)) { const { val, value } = _.getVal(td)?.[0] || {}; if (JSON.stringify(val) !== JSON.stringify(value)) td.addClass('etChange'); else td.removeClass('etChange'); } }); _.tb.on('change', '.etRadio input[type="radio"]', (ev)=>{ const td = $(ev).upper('td'); const idx = td?.data('idx') // 数据或字段索引 ; const r = fields?.[idx]; const { type } = r; if ([ DataType.radio, DataTypes.radio ].includes(type)) { const { val, value } = _.getVal(td)?.[0] || {}; if (JSON.stringify(val) !== JSON.stringify(value)) td.addClass('etChange'); else td.removeClass('etChange'); } }); } /** * checkbox Events * @param {*} ev * @param {HTMLElement} el * @returns */ handleCheck(ev, el) { const _ = this; try { // 代码更改checkbox属性,不会触发change,代码触发change,这里需排除,避免循环 if (ev.detail && ev.detail.sentByWiaF7Table) { // Scripted event, don't do anything return; } const $el = $(el); // 是否被选中 const { checked } = el; const val = $el.data('val'); if (val != null) { if (checked) _.sel.add(val); else _.sel.delete(val); } // 列数 const columnIndex = $el.parents('td,th').index(); // 表体checkbox if (columnIndex === 0) $el.parents('tr')[checked ? 'addClass' : 'removeClass']('data-table-row-selected'); // _.headerCheck(columnIndex) // 延迟到change事件后触发,避免统计选择行数据差错 _.handleSel(); } catch {} } /** * 表头选择区域显示切换,统计选择行,触发选择改变事件,方便跨页统计 */ handleSel() { const _ = this; try { const { el } = _; // 选中行 const rs = el.find('.data-table-row-selected'); const len = rs.length; // 改变表头操作面板 const hd = el.find('.data-table-header'); const hdSel = el.find('.data-table-header-selected'); if (hd.length && hdSel.length) { if (len && !el.hasClass('data-table-has-checked')) el.addClass('data-table-has-checked'); else if (!len && el.hasClass('data-table-has-checked')) el.removeClass('data-table-has-checked'); // 选中数量,跨行选择数量与当前也选择数量不一致 hdSel.find('.data-table-selected-count').text(len); } // 触发当前表选择事件,参数为选择行 // 延迟到change事件后触发,避免跨页统计选择行数据差错 setTimeout(()=>{ _.emit('local::select', rs); }, 10); } catch {} } /** * 选择表所有行,包括跨页 * 表头checkbox只能选择当前页面所有行 */ cancelSel() { try { this.clearSel(); // 每行checkbox // this.headerCheck() // 表头选择区 this.handleSel(); } catch (e) { log.err(e, 'cancelSel'); } } /** * 清除选择(包括跨页),切换表头区及表头checkbox状态 */ clearSel() { const _ = this; try { const { el } = _; if (_.sel?.size) _.sel.clear(); // 切换表头为非选择模式 el?.removeClass('data-table-has-checked'); const rs = el.find('.data-table-row-selected'); if (rs.length) rs.removeClass('data-table-row-selected'); // 更新header的checkbox // const col = 0 // const ckb = this.el.findNode(`thead .checkbox-cell:nth-child(${col + 1}) input[type="checkbox"]`) // ckb.prop('indeterminate', false) // 部分选中 const cks = el.find(`.checkbox-cell input[type="checkbox"]`); if (cks.length) cks.prop('checked', false); } catch (e) { log.err(e, 'clearSel'); } } /** * 填充select option * 每次点击下拉时,检查关联参数是否变化,变化则重新获取选项 * 根据选项,重新生成select 内容,不缓存 * @param {*} r - 字段定义 * @param {Dom} td * @param {Dom} sel - select * @param {string} value - 原数据 * @param {number} idy - 表数据行索引 * @returns */ async fillOption(r, td, sel, value, idy = 0) { const _ = this; try { const { source } = r; let { param = {} } = source || {}; // 查询参数 可引用其他字段值 param = _.parseRef(param, idy); // 引用字段值是否变化 let change; const curParam = JSON.stringify(param); const lastParam = r.lastSourceParam; // if (typeof lastParam !== 'string') lastParam = JSON.stringify(lastParam) if (curParam !== lastParam) { change = true; r.lastSourceParam = curParam // 保存查询参数,避免重复查询 ; } let { option } = r; let { name } = param; // if (name?.includes('${')) { // const lastRefField = td.data('lastRefField') // 保存关联 // const match = name.match(/\$\{([^}]+)\}/) // const ref = match?.[1] // const i = _.getDataIdx({field: ref}) // if (i) { // // 关联节点 // const n = _.tb.findNode(`[data-idx="${i}"]`) // const v = n.findNode('span').html() // if (v && v !== lastRefField) { // change = true // td.data('lastRefField', v) // 保存关联 // // 替换 'city:${province}' // name = name.replace(`\${${ref}}`, v) // } // } // } // 关联字段变化或无选项,动态获取 if (source && (change || !option?.length)) { sel.html(''); // 数据字典查询 // 默认 name = field if (!name) name = r.field; // source.param.name = name option = await getOption(source, name); r.option = option // 保存选项到字段定义,避免重复查询 ; let cnt = 0; if (Array.isArray(option)) cnt = option.length; else if (typeof option === 'object') cnt = Object.keys(option).length; log({ source, name, cnt }, 'fillOption.getOption'); } // 插入当前值,保存后需清除 if (!option && value) option = [ value ]; if (option) { let key; const span = td.find('span'); const val = span.html() // 当前显示值 ; // 添加选项 let htm = []; if (Array.isArray(option)) { if (!option.includes(value)) option.unshift(value) // 加入原始值 ; htm = option.map((v)=>{ let rt; if (v === val) rt = /*#__PURE__*/ _jsx("option", { selected: true, value: v, children: v }); else rt = /*#__PURE__*/ _jsx("option", { value: v, children: v }); return rt; }); } else if (typeof option === 'object') { const has = Object.values(option).some((v)=>`${v}` === `${value}`); if (!has) option[value] = value // 加入原始值 ; if (!val) { htm.push(/*#__PURE__*/ _jsx("option", { selected: true, value: "", children: "请选择" })); } for (const k of Object.keys(option)){ const v = option[k]; if (v === val) { key = k; htm.push(/*#__PURE__*/ _jsx("option", { selected: true, value: k, children: v })); } else htm.push(/*#__PURE__*/ _jsx("option", { value: k, children: v })); } } // r.option = option // 不保存到字段定义,避免污染 sel.html(htm.join('')); if (key) sel.val(key); else sel.val(val); } } catch (e) { log.err(e, 'fillOption'); } } clearOption() {} /** * 解析引用字段 * @param {*} src * @param {number} [idy] - 表数据行索引 * @param {*} [fv] - 字段值,外部传入可加快速度 * @returns */ /** * 递归检查对象中是否包含 ${} 引用 * @param {*} obj 要检查的对象 * @returns {boolean} 是否包含引用 */ hasRef(obj) { // 处理 null/undefined if (obj == null) return false; // 字符串类型:直接检查 if (typeof obj === 'string') { return /\$\{[^}]*\}/.test(obj); } // 对象类型(排除数组):递归检查 if (typeof obj === 'object' && !Array.isArray(obj)) { for (const k of Object.keys(obj)){ if (this.hasRef(obj[k])) return true; } } return false; } /** * 递归解析对象中的 ${} 引用(直接修改原对象) * @param {*} obj 要解析的对象 * @param {*} fv 字段值对象 * @returns {*} 解析后的值或对象 */ parseRefRecursive(obj, fv) { // 处理 null/undefined:直接返回 if (obj == null) return obj; // 字符串类型:检查并解析 if (typeof obj === 'string') { if (/\$\{[^}]*\}/.test(obj)) { const val = Function('r', `return \`${obj}\``)(fv); log({ src: obj, val }, 'parseRef'); return val; } return obj; } // 对象类型(排除数组):递归解析每个属性 if (typeof obj === 'object' && !Array.isArray(obj)) { for (const k of Object.keys(obj)){ obj[k] = this.parseRefRecursive(obj[k], fv); } return obj; } // 其他类型(数组、数字等):直接返回 return obj; } parseRef(src, idy = 0, fv = null) { let R = src; const _ = this; try { const { data, opt, vals, fields } = _; const { kv } = opt; // 使用递归方法检查是否包含引用 const ref = _.hasRef(src); if (ref) { if (!fv) { /** @type {*} */ fv = {} // 获取当前行最新数据 ; if (kv) { for (const d of data){ const { field, type } = d; fv[field] = vals[0][field] ?? d.value; if ([ 'number', DataType.number ].includes(type) && isNumber(fv[field])) fv[field] = Number(fv[field]); } } else { for (const f of fields){ const { field, idx, type } = f; const val = data[idy][idx]; fv[field] = vals[idy][field] ?? val; if ([ 'number', DataType.number ].includes(type) && isNumber(fv[field])) fv[field] = Number(fv[field]); } } } // 使用递归方法解析引用(直接修改原对象) if (typeof src === 'object' && !Array.isArray(src)) { _.parseRefRecursive(src, fv); R = src; } else { const val = Function('r', `return \`${src}\``)(fv); R = val; log({ idy, src, val }, 'parseRef'); } } } catch (e) { log.err(e, 'parseRef'); } return R; } /** * 获得数据索引 * @param {*} opts * @returns {number} */ getDataIdx(opts) { let R; const _ = this; try { const { field } = opts; if (field) { const idx = _.data.findIndex((v)=>v.field === field); if (idx >= 0) R = idx; } } catch (e) { log.err(e, 'getData'); } return R; } /** * 加载插件 * @param {*} cls * @param {*} [opts] */ use(cls, opts) { const _ = this; try { const { opt } = _; _[cls.name] = cls; _.uses.add({ cls, opts }); if (cls.name === 'Uploader' && opts?.upload) opt.upload = opts.upload; else if (cls.name === 'Tabulate' && (opts?.getSelAll || opts?.saveEmbTb)) { opt.getSelAll = opts.getSelAll; opt.saveEmbTb = opts.saveEmbTb; opt.prjid = opts.prjid; opt.upload = opts.upload; } else if (cls.name === 'JsonView' && opts?.updateJson) opt.updateJson = opts.updateJson; } catch (e) { log.err(e, 'use'); } } /** * input type * @param {DataType|DataTypes} type */ getInputType(type) { let R; switch(type){ case DataTypes.text: case DataType.text: R = 'text'; break; case DataTypes.number: case DataType.number: R = 'text'; break; case DataTypes.date: case DataType.date: R = 'date'; break; // urlChange // case DataTypes.url: // case DataType.url: // R = 'url' // break case DataTypes.email: case DataType.email: R = 'email'; break; case DataTypes.tel: case DataType.tel: R = 'tel'; break; case DataTypes.password: case DataType.password: R = 'password'; break; case DataTypes.time: case DataType.time: R = 'time'; break; case DataTypes.datetime: case DataType.datetime: R = 'datetime-local'; break; case DataTypes.month: case DataType.month: R = 'month'; break; case DataTypes.week: case DataType.week: R = 'week'; break; case DataTypes.color: case DataType.color: R = 'color'; break; } return R; } /* // 异步存储 set(key, data, expires) { _storage.save({ key: key, // Note: Do not use underscore("_") in key! data: data, expires: expires }); } // Promise同步方法 getStore(key) { // load _storage.load({ key: key, autoSync: true, syncInBackground: true }).then(data => { return data; }).catch(err => { console.warn(err); return null; }) } // async 的写法 async get(key) { try { let data = await storage.load({ key: key }); return data; } catch (err) { console.warn(err); return null; } } */ // Chrome、Safari 阻止浏览器的默认事件,实现 全选 mouseupCell(evt) { const ev = evt || window.event; ev.preventDefault(); } addHandler() { const data = this.tabulate.getData(); this.tabulate.addRow(); } saveTable() { this.tabulate.saveTable(); } /** * 初始化表格编辑器 */ async editModeTable() { const _ = this; try { const { page, opt, data } = _; const tds = _.tb.find('td[data-idx]'); console.log(tds, 'tds'); //! 应该根据field 创建,支持多个内嵌表格编辑 //! 需判断是否已创建,避免重复创建 //! 应该根据字段类型(内嵌表)创建,而不是在编辑模式没有内嵌表也创建 // if (_.hasTable && !_.tabulate && _.Tabulate) { if (_.hasTable) { if (opt.newEdit) { const dvs = _.tb.find('div.data-table'); for (const dv of dvs){ const $dv = $(dv); const name = $dv.attr('name'); const { wiaDataTable: dtb, wiaEditTable: etb } = dv; if (!etb) { const { head } = dtb; const { api } = head[0]; if (api.param) api.param = _.parseRef(api.param); dv.wiaEditTable = makeEdit(page, { el: $dv, name, head, data: dtb.data, use: [ ..._.uses ] }); } else { etb.show(); dtb.hide(); } } } else { _.tabulate = new _.Tabulate({ containerName: 't-table', addButtonName: 'add-button', targetBox: _.tb.tag('tbody')[0].querySelectorAll('.data-table'), baseTableInfo: _.baseTableInfo, getSelAll: opt.getSelAll, saveEmbTb: opt.saveEmbTb, viewid: opt.viewid, prjid: opt.prjid, upload: opt.upload }); } } else { const tTableDivs = new Set(); const dataTables = _.tb.tag('tbody')[0].querySelectorAll('.data-table'); dataTables.forEach((table)=>{ table.style.display = 'none'; // 获取当前 .data-table 的父节点(兄弟元素的共同容器) const parent = table.parentNode; // 在父节点中查找 name="t-table" 的 div const tTableDiv = parent.querySelector('div[name="t-table"]'); const addButton = parent.querySelector('button[name="add-button"]'); // 找到后添加到集合(去重,避免重复元素) if (tTableDiv) { tTableDiv.style.display = 'block'; addButton.style.display = 'block'; tTableDivs.add(tTableDiv); } else { _.tabulate.destroyTabulateInstance(_.tabulate); _.tabulate = new _.Tabulate({ containerName: 't-table', addButtonName: 'add-button', targetBox: _.tb.tag('tbody')[0].querySelectorAll('.data-table'), baseTableInfo: _.baseTableInfo, getSelAll: opt.getSelAll, saveEmbTb: opt.saveEmbTb, viewid: opt.viewid, prjid: opt.prjid, upload: opt.upload }); } }); } } catch (e) {} } /** * 编辑模式 */ edit() { try { const _ = this; if (_.state === State.json) { _.tb.parent().find('.json-view-box').hide(); _.tb.show(); } _.state = State.edit; _.tb.tag('tbody').addClass('etEdit').removeClass('etView'); _.editModeTable(); editAttach(_.tb); chip.edit(_.tb); // 更新所有 CatAttach 实例的编辑状态和图集模式 const catAttachTds = _.tb.find('td[catAttachField]'); for (const td of catAttachTds){ const $td = $(td); const instance = $td.data('catAttach'); if (instance) { // 进入编辑模式时,先切换到图集模式 if (typeof instance.setAllToGridMode === 'function') { instance.setAllToGridMode(); } // 然后设置编辑状态 if (typeof instance.setEditMode === 'function') { instance.setEditMode(true); } } } // _.bind() } catch (e) { log.err(e, 'edit'); } } /** * @param {{ etb: EditTable; dtb: any; path: string; no: string; tab?: string; name: string; icon?: string; card?: HTMLElement; tb?: JQuery; data: any[]; }} data */ json(data) { const _ =