From e9f01e2e6d94b8fe86071b30bdc647143e42916a Mon Sep 17 00:00:00 2001 From: augushong Date: Sat, 3 Oct 2026 12:31:09 +0000 Subject: [PATCH] =?UTF-8?q?feat(table):=20=E5=BD=A2=E6=80=81=E6=84=9F?= =?UTF-8?q?=E7=9F=A5=E5=B7=A5=E5=85=B7=E6=A0=8F=E4=B8=8E=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=9D=A2=E6=9D=BF=EF=BC=88pro#5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TABLE_CARD_SETTING 图标注入与形态联动 卡片设置面板:配置开关与排序循环(升→降→取消),显式 where 传参 setLayoutForm 排序箭头修复 settings 上下文暴露与操作按钮三态显隐 --- public/static/admin/css/public.css | 75 ++++++++ public/static/admin/css/public.scss | 81 +++++++++ .../static/plugs/ulthon-admin/admin-table.js | 171 +++++++++++++++++- 3 files changed, 319 insertions(+), 8 deletions(-) diff --git a/public/static/admin/css/public.css b/public/static/admin/css/public.css index 2012a91..2297b5d 100644 --- a/public/static/admin/css/public.css +++ b/public/static/admin/css/public.css @@ -859,6 +859,81 @@ table样式 margin: 0; } +/* 卡片操作按钮三态显隐:卡片网格内默认隐藏(保占位防 footer 跳动)、 + hover 显示;设置面板开关写入容器的 ua-card-operat-always 类后常显 + (admin-table.js 两条渲染路径挂类);触屏设备无 hover 能力恒显。 */ +.ua-layout-cards .ua-data-card-operat { + opacity: 0; + visibility: hidden; + transition: opacity 0.2s, visibility 0.2s; +} + +.ua-data-card:hover .ua-data-card-operat { + opacity: 1; + visibility: visible; +} + +.ua-card-operat-always .ua-data-card-operat { + opacity: 1; + visibility: visible; +} + +@media (hover: none) { + .ua-layout-cards .ua-data-card-operat { + opacity: 1; + visibility: visible; + } +} + +.ua-card-setting-panel { + min-width: 200px; +} + +.ua-card-setting-title { + padding: 10px 18px 2px; + color: #999; + font-size: 12px; +} + +.ua-card-setting-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 18px; +} +.ua-card-setting-item:hover { + background-color: #f8f8f8; +} + +.ua-card-setting-switch { + position: relative; + flex: none; + width: 36px; + height: 18px; + border-radius: 9px; + background-color: #d2d2d2; + cursor: pointer; + transition: background-color 0.2s; +} +.ua-card-setting-switch::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: #fff; + transition: left 0.2s; +} +.ua-card-setting-switch.on { + background-color: rgb(22, 183, 119); +} +.ua-card-setting-switch.on::after { + left: 20px; +} + @media (max-width: 768px) { .ua-layout-cards { grid-template-columns: 1fr; diff --git a/public/static/admin/css/public.scss b/public/static/admin/css/public.scss index 8abd42b..1ce6c95 100644 --- a/public/static/admin/css/public.scss +++ b/public/static/admin/css/public.scss @@ -881,6 +881,87 @@ table样式 } } +// 卡片操作按钮三态显隐:卡片网格内默认隐藏(保占位防 footer 跳动)、 +// hover 显示;设置面板开关写入容器的 ua-card-operat-always 类后常显 +// (admin-table.js 两条渲染路径挂类);触屏设备无 hover 能力恒显。 +.ua-layout-cards .ua-data-card-operat { + opacity: 0; + visibility: hidden; + transition: opacity .2s, visibility .2s; +} + +.ua-data-card:hover .ua-data-card-operat { + opacity: 1; + visibility: visible; +} + +.ua-card-operat-always .ua-data-card-operat { + opacity: 1; + visibility: visible; +} + +@media (hover: none) { + .ua-layout-cards .ua-data-card-operat { + opacity: 1; + visibility: visible; + } +} + +// 卡片设置面板(admin-table.js listenToolbar case 'TABLE_CARD_SETTING' 产出; +// 面板骨架复用上方 .ul-nav-block 基类,此处只补面板本体、配置行与自绘开关) +.ua-card-setting-panel { + min-width: 200px; +} + +.ua-card-setting-title { + padding: 10px 18px 2px; + color: #999; + font-size: 12px; +} + +.ua-card-setting-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 18px; + + &:hover { + background-color: #f8f8f8; + } +} + +.ua-card-setting-switch { + position: relative; + flex: none; + width: 36px; + height: 18px; + border-radius: 9px; + background-color: #d2d2d2; + cursor: pointer; + transition: background-color .2s; + + &::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: #fff; + transition: left .2s; + } + + &.on { + background-color: rgb(22, 183, 119); + + &::after { + left: 20px; + } + } +} + @media (max-width: 768px) { .ua-layout-cards { grid-template-columns: 1fr; diff --git a/public/static/plugs/ulthon-admin/admin-table.js b/public/static/plugs/ulthon-admin/admin-table.js index 1b7e0b7..313b283 100644 --- a/public/static/plugs/ulthon-admin/admin-table.js +++ b/public/static/plugs/ulthon-admin/admin-table.js @@ -342,8 +342,9 @@ }; // 形态可见性同步:引擎数据区(表头+表体+合计行)与模板容器互斥显隐, - // 引擎视图的工具栏(刷新/新增/批量删除等)两形态共用保持可见,搜索区 - // 本就挂在表格元素之前不受影响。非 table 形态下引擎根本不渲染行数据 + // 引擎视图的工具栏(刷新/新增/批量删除等)两形态共用保持可见(print 与 + // 卡片设置图标除外,随形态互斥显隐),搜索区本就挂在表格元素之前不受影响。 + // 非 table 形态下引擎根本不渲染行数据 // (wrapper 非 table 分支不走引擎渲染),并非旧寄生实现"引擎恒渲+CSS 隐藏"。 var syncLayoutVisibility = function (tableId) { var layout = controlledLayouts[tableId]; @@ -354,7 +355,8 @@ var $box = $view.find('.layui-table-box').first(); var $total = $view.find('.layui-table-total').first(); var $container = $('#ua-layout-' + tableId); - if (resolveCurrentLayout(tableId).kind === 'table') { + var isTable = resolveCurrentLayout(tableId).kind === 'table'; + if (isTable) { destroyLayoutVueInstances(tableId); $box.add($total).removeClass('layui-hide'); $container.addClass('layui-hide').empty(); @@ -362,6 +364,10 @@ $box.add($total).addClass('layui-hide'); $container.removeClass('layui-hide'); } + // 工具栏形态联动:print 仅表格形态有意义;卡片设置仅非表格形态有意义。 + // filter(LAYTABLE_COLS)与视图切换(TABLE_LAYOUT)两形态常驻不动 + $view.find('[lay-event="LAYTABLE_PRINT"]').toggleClass('layui-hide', !isTable); + $view.find('[lay-event="TABLE_CARD_SETTING"]').toggleClass('layui-hide', isTable); }; if (window.UA_TABLE_STORE) { @@ -466,7 +472,9 @@ rows: [], page: state ? state.page : {curr: 1, limit: 0}, count: state ? state.count : 0, - checkedAll: state ? window.UA_TABLE_STORE.selection.isAll(tableId) : false + checkedAll: state ? window.UA_TABLE_STORE.selection.isAll(tableId) : false, + // 面板设置快照:模板经 d._list.settings / list.settings 只读 + settings: {cardOperatAlways: ua.getPageSetting('card-operat-always', '') === '1'} }; $.each(rows, function (i, row) { @@ -511,7 +519,7 @@ // 【tplFunction 调用约定】 // 单行 HTML = tplFunction.call(rowCtx, rowCtx, listCtx) // - this/第一参 = 行上下文 {...row, _cells, _operatHtml, _checked, _disabled} - // - 第二参 = 列表上下文 {tableId, rows, page, count, checkedAll} + // - 第二参 = 列表上下文 {tableId, rows, page, count, checkedAll, settings} // - 返回值 = 该行的 HTML 字符串(框架直接拼接,不做转义) // 【laytpl 调用约定(tplLay / tplLayId)】 // 单行 HTML = laytpl(tplLay | $(tplLayId).html()).render(rowCtx) @@ -533,7 +541,7 @@ // v-for="d in rows" 承接: // - rows = 行上下文数组,d 与 tplFunction/tplLay 的行上下文同构 // (d._cells.xxx / d._operatHtml / d._checked / d._disabled); - // - list = 列表上下文 {tableId, rows, page, count, checkedAll} + // - list = 列表上下文 {tableId, rows, page, count, checkedAll, settings} // (laytpl 的 d._list 同物;Vue 实例对 _/$ 前缀 data 键不做代理, // 裸标识符 _list 会 ReferenceError,故 Vue 形态改名为 list); // - _cells/_operatHtml 含 HTML,必须用 v-html 输出,纯文本用 {{ }}; @@ -616,6 +624,9 @@ if (vueTemplate !== null) { // 自定义模板形态移除内置 card 网格类(与 laytpl 同规则) $container.toggleClass('ua-layout-cards', resolved.entry.builtinCard === true); + // 常显开关类随渲染重挂(设置面板开关已写入页级 pageSetting, + // 切形态/重渲后保持;CSS 三态效果见 public.scss) + $container.toggleClass('ua-card-operat-always', ua.getPageSetting('card-operat-always', '') === '1'); if (context.rows.length === 0) { $container.html('
无数据
'); form.render(null, 'ua-layout-' + tableId); @@ -705,6 +716,9 @@ // 内置 card 形态容器挂网格布局类(样式见 public.scss 的 .ua-layout-cards), // 自定义模板形态移除该标记,不受卡片网格影响 $container.toggleClass('ua-layout-cards', resolved.entry.builtinCard === true); + // 常显开关类随渲染重挂(设置面板开关已写入页级 pageSetting, + // 切形态/重渲后保持;CSS 三态效果见 public.scss) + $container.toggleClass('ua-card-operat-always', ua.getPageSetting('card-operat-always', '') === '1'); $container.html(html); // 容器恒为 .layui-form(lay-filter 与容器 id 同名),scoped form.render // 美化模板产出的 switch/checkbox 控件 @@ -731,10 +745,21 @@ // table 形态(含无钩子条目回退):引擎纯渲染当页数据,直呼备份 // 原引用不走 shim(防路由回 store 造成 fetch) var state = window.UA_TABLE_STORE.dataProvider.getState(tableId); - originalTableReloadData.call(table, tableId, { + var reloadOpts = { data: state ? state.currentPageData : [], totalRow: state && state.lastResponse ? state.lastResponse.totalRow : undefined - }); + }; + // 当前排序(store where.sort 单键映射)转引擎 initSort + // {field, type},切回表格形态时表头箭头与数据一致 + var currentSort = state && state.where ? state.where.sort : null; + if (currentSort && Object.keys(currentSort).length > 0) { + var sortField = Object.keys(currentSort)[0]; + var sortType = currentSort[sortField]; + if (sortType === 'asc' || sortType === 'desc') { + reloadOpts.initSort = {field: sortField, type: sortType}; + } + } + originalTableReloadData.call(table, tableId, reloadOpts); } syncLayoutVisibility(tableId); syncControlledPager(tableId); @@ -795,6 +820,23 @@ extend: 'data-table-id="' + options.id + '"' }); } + // 卡片设置入口(pro#5):存在任何非 table 形态即注入(不依赖 + // 条目数>1,单形态 card 页也需要打开设置面板)。不写 extend 属性 + // (layui 不渲染该属性,面板定位走 lay-table-id 模式) + var hasNonTableLayout = false; + $.each(layoutEntries, function (i, entry) { + if (entry && entry.name !== 'table') { + hasNonTableLayout = true; + return false; + } + }); + if (hasNonTableLayout) { + defaultToolbar.push({ + title: '卡片设置', + layEvent: 'TABLE_CARD_SETTING', + icon: 'layui-icon-set' + }); + } } if (options.defaultToolbar === undefined) { @@ -819,6 +861,22 @@ extend: 'data-table-id="' + options.id + '"' }); } + // 卡片设置入口与 TABLE_LAYOUT 同规则去重追加:autoInjected + // 即默认注入 card,条件恒成立;已有同名 layEvent 不重复加 + var hasCardSettingBtn = false; + $.each(options.defaultToolbar, function (i, btn) { + if (btn && typeof btn === 'object' && btn.layEvent === 'TABLE_CARD_SETTING') { + hasCardSettingBtn = true; + return false; + } + }); + if (!hasCardSettingBtn) { + options.defaultToolbar.push({ + title: '卡片设置', + layEvent: 'TABLE_CARD_SETTING', + icon: 'layui-icon-set' + }); + } } @@ -2156,6 +2214,103 @@ } }); break; + case 'TABLE_CARD_SETTING': + // 卡片设置面板(pro#5,交互结构参照 TABLE_LAYOUT 实现 + // 重建):入口图标仅卡片形态可见(syncLayoutVisibility + // 联动),面板 append 在工具栏图标元素内,面板内点击 + // 均阻断冒泡(不触发 layui toolbar 重开、不触发 + // document 收起),开关可反复切换;再点图标收起 + var cardSettingId = 'ua-card-setting-' + tableId; + var $toolbarSettingElem = $('[lay-table-id="' + tableId + '"]').find('[lay-event="TABLE_CARD_SETTING"]'); + var $cardSettingElem = $('#' + cardSettingId); + if ($cardSettingElem.length > 0) { + $cardSettingElem.remove(); + break; + } + + // 配置段 + 排序段(面板每次点图标重建,排序段随建随取 + // store 当前态,表头排序/搜索后重开面板即最新) + var cardSettingHtml = '
'; + var operatAlwaysOn = ua.getPageSetting('card-operat-always', '') === '1'; + cardSettingHtml += '
配置
'; + cardSettingHtml += '
操作按钮始终显示' + + '
'; + cardSettingHtml += '
排序
'; + // 当前排序读 store 单一事实源(where.sort 形状为 {字段名: 'asc'|'desc'} + // 单键映射,与 store setSort/buildRequestData 同构) + var cardSortState = window.UA_TABLE_STORE.dataProvider.getState(tableId); + var cardSortCurrent = cardSortState && cardSortState.where ? cardSortState.where.sort : null; + $.each(((controlledLayouts[tableId] && controlledLayouts[tableId].cols[0]) || []), function (i, col) { + if (!col || !col.field || col.sort === false || col.hide) { + return; + } + var sortClass = ''; + var sortIconClass = 'layui-icon layui-hide'; + if (cardSortCurrent && cardSortCurrent[col.field]) { + sortClass = ' current'; + sortIconClass = 'layui-icon layui-icon-' + (cardSortCurrent[col.field] === 'desc' ? 'down' : 'up'); + } + cardSettingHtml += '
' + + (col.title || col.field) + + '
'; + }); + cardSettingHtml += '
'; + + $cardSettingElem = $(cardSettingHtml).appendTo($toolbarSettingElem); + + // 面板内条目点击不冒泡(排序段等后续 .item 条目沿用) + $cardSettingElem.on('click', '.item', function (event) { + event.stopPropagation(); + }); + + // 开关:页级记忆 + 模板容器即时生效,不收起面板不重渲 + $cardSettingElem.on('click', '.ua-card-setting-switch', function (event) { + var on = !$(this).hasClass('on'); + ua.setPageSetting('card-operat-always', on ? '1' : ''); + $(this).toggleClass('on', on); + $('#ua-layout-' + tableId).toggleClass('ua-card-operat-always', on); + event.stopPropagation(); + }); + + // 排序:循环 升 → 降 → 取消。与 listenTableSort 同构: + // 以 lastTableWhere 为基底就地改 sort,显式传 where + // (store.reload 仅在 opts.where 存在时经 setWhere 更新 + // 请求参数,漏传排序到不了服务端);initSort 供表头 + // 箭头(取消时省略);当前态以 store getState 为准 + // (搜索路径会整体覆盖 lastTableWhere,admin-table.js + // 侧变量不可作判定依据)。点击后收起面板 + $cardSettingElem.on('click', '.ua-card-setting-sort-item', function (event) { + event.stopPropagation(); + var field = $(this).data('field'); + var sortState = window.UA_TABLE_STORE.dataProvider.getState(tableId); + var currentType = sortState && sortState.where && sortState.where.sort ? (sortState.where.sort[field] || null) : null; + var lastWhere = lastTableWhere[tableId] ? lastTableWhere[tableId] : {}; + var reloadOpts = {where: lastWhere}; + if (currentType === 'asc') { + lastWhere.sort = {}; + lastWhere.sort[field] = 'desc'; + reloadOpts.initSort = {field: field, type: 'desc'}; + } else if (currentType === 'desc') { + delete lastWhere.sort; + } else { + lastWhere.sort = {}; + lastWhere.sort[field] = 'asc'; + reloadOpts.initSort = {field: field, type: 'asc'}; + } + lastTableWhere[tableId] = lastWhere; + ua.table.reloadData(tableId, reloadOpts); + $cardSettingElem.remove(); + $(document).off('click.ua-card-setting'); + }); + + // 点击面板外区域收起(命名空间事件,收起时解除) + $(document).off('click.ua-card-setting').on('click.ua-card-setting', function (event) { + if (!$(event.target).closest($toolbarSettingElem).length) { + $('#' + cardSettingId).remove(); + $(document).off('click.ua-card-setting'); + } + }); + break; } }); },