From f3ac7189f2ec7d0970d11d5f3b6c2910857a0c96 Mon Sep 17 00:00:00 2001 From: augushong Date: Mon, 5 Oct 2026 14:08:03 +0000 Subject: [PATCH] =?UTF-8?q?refactor(component):=20tagInput/propertyInput/t?= =?UTF-8?q?ableData=20=E6=A8=A1=E6=9D=BF=E6=94=B9=E4=B8=BA=E6=87=92?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=BC=82=E6=AD=A5=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现三个组件在 JS 顶层用 $.ajax({async:false}) 同步拉取 .html 模板, 每个页面打开固定发 3 个同步请求(阻塞主线程,与组件是否被使用无关), 且路径硬编码 /static/ 与 __STATIC__(OSS 部署)脱节,模板 URL 无版本 参数存在与 js 缓存不同步的漂移问题,请求失败时静默渲染空白。 改造内容: - 模板改为首次 render() 时异步获取,Deferred 缓存供同页多实例共享, 失败不缓存并在下次 render 自动重试,失败时 console.error + 提示 - 模板 URL 取 CONFIG.STATIC + CONFIG.STATIC_VERSION,layout 的 window.CONFIG 新增这两项注入;无注入时逐级回退 /static 与 VERSION, 兼容下游覆盖 layout 的旧页面 - 组件 CSS 由 _require.html 直接 link 引入(__STATIC__ + 版本键), 替代原 JS 动态注入 - render() 返回 Promise(resolve Vue 实例);仓库内调用均为 fire-and-forget,无兼容影响 --- extend/base/admin/view/common/_require.html | 3 + extend/base/admin/view/layout/default.html | 8 +- .../lay-module/propertyInput/propertyInput.js | 219 ++++++++------ .../plugs/lay-module/tableData/tableData.js | 286 ++++++++++-------- .../plugs/lay-module/tagInput/tagInput.js | 195 ++++++------ 5 files changed, 392 insertions(+), 319 deletions(-) diff --git a/extend/base/admin/view/common/_require.html b/extend/base/admin/view/common/_require.html index d5bcad6..44eb964 100644 --- a/extend/base/admin/view/common/_require.html +++ b/extend/base/admin/view/common/_require.html @@ -9,6 +9,9 @@ + + + diff --git a/extend/base/admin/view/layout/default.html b/extend/base/admin/view/layout/default.html index 73261b3..11b681d 100644 --- a/extend/base/admin/view/layout/default.html +++ b/extend/base/admin/view/layout/default.html @@ -17,7 +17,13 @@ IS_SUPER_ADMIN: "{$isSuperAdmin|default='false'}", VERSION: "{$version|default='1.0.0'}", CSRF_TOKEN: "{:token()}", - PAGE_KEY_NAME:'{$pageKeyName|default=$Request.url}' + PAGE_KEY_NAME:'{$pageKeyName|default=$Request.url}', + // 静态资源根路径(与 __STATIC__ 模板替换一致,OSS 部署时指向 OSS), + // 供 JS 内部动态拼接静态资源地址的组件使用(tagInput/propertyInput/tableData 等) + STATIC: "__STATIC__", + // 静态资源版本键,与 _require.html 中 js/css 的 ?t= 参数同源, + // 保证动态请求的模板与引入的 js/css 缓存版本一致 + STATIC_VERSION: "{:get_site_version_key()}" }; var PATH_CONFIG = { diff --git a/public/static/plugs/lay-module/propertyInput/propertyInput.js b/public/static/plugs/lay-module/propertyInput/propertyInput.js index d109c1f..96d258b 100644 --- a/public/static/plugs/lay-module/propertyInput/propertyInput.js +++ b/public/static/plugs/lay-module/propertyInput/propertyInput.js @@ -1,30 +1,51 @@ (function () { - const propertyInputCss = '/static/plugs/lay-module/propertyInput/propertyInput.css'; - const propertyInputHtml = '/static/plugs/lay-module/propertyInput/propertyInput.html'; + // 组件模板懒加载:首次 render 时才异步请求,同页多实例共享一次请求; + // 路径与版本跟随页面注入的 CONFIG.STATIC / CONFIG.STATIC_VERSION, + // 与 _require.html 中 js/css 的缓存键保持一致;失败不缓存,下次 render 重试。 + // 样式 propertyInput.css 已改为在 _require.html 中直接引入,不再由 JS 注入。 + var propertyInputTemplatePromise = null; - var propertyInput = function () { - $(function () { - var cssElement = document.createElement('link'); - - cssElement.setAttribute('rel', 'stylesheet'); - - cssElement.setAttribute('href', propertyInputCss); - - document.body.appendChild(cssElement); - }); - }; - var propertyInputTemplate = ''; - - $.ajax({ - type: "get", - url: propertyInputHtml, - cache: true, - async: false, - success: function (template) { - propertyInputTemplate = template; + function loadPropertyInputTemplate() { + if (propertyInputTemplatePromise !== null) { + return propertyInputTemplatePromise; } - }); + var config = window.CONFIG || {}; + var staticRoot = config.STATIC || '/static'; + var version = config.STATIC_VERSION || config.VERSION || ''; + var url = staticRoot + '/plugs/lay-module/propertyInput/propertyInput.html?t=' + version; + var deferred = $.Deferred(); + + $.ajax({ + type: 'get', + url: url, + cache: true, + dataType: 'text' + }).done(function (template) { + if ($.trim(template)) { + deferred.resolve(template); + } else { + deferred.reject('模板内容为空'); + } + }).fail(function (xhr) { + deferred.reject(xhr); + }); + + propertyInputTemplatePromise = deferred.promise(); + propertyInputTemplatePromise.fail(function (reason) { + console.error('propertyInput 模板加载失败: ' + url, reason); + var ua = window.UA_ADMIN || window.ua; + if (ua) { + ua.msg.error('propertyInput 组件初始化失败'); + } + // 失败不缓存,下次 render 自动重试 + propertyInputTemplatePromise = null; + }); + + return propertyInputTemplatePromise; + } + + var propertyInput = function () { }; propertyInput.prototype.render = function (elem, data) { @@ -184,88 +205,88 @@ } } - app = new Vue({ - el: elem, - data() { - return { - setting: options, - value: '', - listItem: [ + return loadPropertyInputTemplate().then(function (template) { + return new Vue({ + el: elem, + data() { + return { + setting: options, + value: '', + listItem: [ - ], - originalValue: '', - field: options.field, - }; - }, + ], + originalValue: '', + field: options.field, + }; + }, - created() { - if (this.setting.value) { - this.originalValue = $.extend(true, [], this.setting.value); - this.listItem = $.extend(true, [], this.setting.value); - this.updateValue(this.listItem); + created() { + if (this.setting.value) { + this.originalValue = $.extend(true, [], this.setting.value); + this.listItem = $.extend(true, [], this.setting.value); + this.updateValue(this.listItem); - if (this.listItem.length == 0) { - this.onAddItem(); + if (this.listItem.length == 0) { + this.onAddItem(); + } + } + + }, + mounted() { + if (options.required == 1) { + $(this.$refs['propertyInput']).closest('.layui-form-item').children('.layui-form-label').addClass('required'); + } + }, + template: template, + + methods: { + onResetItem() { + this.listItem = $.extend(true, [], this.originalValue); + if (this.listItem.length == 0) { + this.onAddItem(); + } + + this.updateValue(this.listItem); + }, + onAddItem() { + + var emptyItem = {}; + + emptyItem.uid = ua.randdomString(); + + for (var i = 0; i < this.field.length; i++) { + emptyItem[this.field[i].key] = this.field[i].default; + } + + this.listItem.push(emptyItem); + + this.updateValue(this.listItem); + }, + onItemChange(item, index, itemField, event) { + this.listItem[index][itemField.key] = event.target.value; + + this.updateValue(this.listItem); + }, + onItemDelete(item, index) { + if (this.listItem.length <= 1) { + this.listItem = [ + { + name: '', + value: '' + } + ]; + } else { + this.listItem.splice(index, 1); + } + + this.updateValue(this.listItem); + }, + updateValue(newValue) { + this.value = JSON.stringify(newValue); } } - - }, - mounted() { - if (options.required == 1) { - $(this.$refs['propertyInput']).closest('.layui-form-item').children('.layui-form-label').addClass('required'); - } - }, - template: propertyInputTemplate, - - methods: { - onResetItem() { - this.listItem = $.extend(true, [], this.originalValue); - if (this.listItem.length == 0) { - this.onAddItem(); - } - - this.updateValue(this.listItem); - }, - onAddItem() { - - var emptyItem = {}; - - emptyItem.uid = ua.randdomString(); - - for (var i = 0; i < this.field.length; i++) { - emptyItem[this.field[i].key] = this.field[i].default; - } - - this.listItem.push(emptyItem); - - this.updateValue(this.listItem); - }, - onItemChange(item, index, itemField, event) { - this.listItem[index][itemField.key] = event.target.value; - - this.updateValue(this.listItem); - }, - onItemDelete(item, index) { - if (this.listItem.length <= 1) { - this.listItem = [ - { - name: '', - value: '' - } - ]; - } else { - this.listItem.splice(index, 1); - } - - this.updateValue(this.listItem); - }, - updateValue(newValue) { - this.value = JSON.stringify(newValue); - } - } + }); }); - - return app; }; window.propertyInput = new propertyInput(); diff --git a/public/static/plugs/lay-module/tableData/tableData.js b/public/static/plugs/lay-module/tableData/tableData.js index cf0f8e2..ce35bdd 100644 --- a/public/static/plugs/lay-module/tableData/tableData.js +++ b/public/static/plugs/lay-module/tableData/tableData.js @@ -1,29 +1,53 @@ (function () { - const tableDataCss = '/static/plugs/lay-module/tableData/tableData.css'; - const tableDataHtml = '/static/plugs/lay-module/tableData/tableData.html'; + // 组件模板懒加载:首次 render 时才异步请求,同页多实例共享一次请求; + // 路径与版本跟随页面注入的 CONFIG.STATIC / CONFIG.STATIC_VERSION, + // 与 _require.html 中 js/css 的缓存键保持一致;失败不缓存,下次 render 重试。 + // 样式 tableData.css 已改为在 _require.html 中直接引入,不再由 JS 注入。 + var tableDataTemplatePromise = null; - var tableData = function () { - $(function () { - var cssElement = document.createElement('link'); - cssElement.setAttribute('rel', 'stylesheet'); - cssElement.setAttribute('href', tableDataCss); - document.body.appendChild(cssElement); - }); - }; - var tableDataTemplate = ''; - - $.ajax({ - type: "get", - url: tableDataHtml, - cache: true, - async: false, - success: function (template) { - tableDataTemplate = template; + function loadTableDataTemplate() { + if (tableDataTemplatePromise !== null) { + return tableDataTemplatePromise; } - }); + + var config = window.CONFIG || {}; + var staticRoot = config.STATIC || '/static'; + var version = config.STATIC_VERSION || config.VERSION || ''; + var url = staticRoot + '/plugs/lay-module/tableData/tableData.html?t=' + version; + var deferred = $.Deferred(); + + $.ajax({ + type: 'get', + url: url, + cache: true, + dataType: 'text' + }).done(function (template) { + if ($.trim(template)) { + deferred.resolve(template); + } else { + deferred.reject('模板内容为空'); + } + }).fail(function (xhr) { + deferred.reject(xhr); + }); + + tableDataTemplatePromise = deferred.promise(); + tableDataTemplatePromise.fail(function (reason) { + console.error('tableData 模板加载失败: ' + url, reason); + var ua = window.UA_ADMIN || window.ua; + if (ua) { + ua.msg.error('tableData 组件初始化失败'); + } + // 失败不缓存,下次 render 自动重试 + tableDataTemplatePromise = null; + }); + + return tableDataTemplatePromise; + } + + var tableData = function () { }; tableData.prototype.render = function (elem, data) { - var app; var clienWidth, clientHeight; clienWidth = '100%'; clientHeight = '100%'; @@ -65,123 +89,123 @@ options.className = 'layui-textarea'; } - app = new Vue({ - el: elem, - data() { - return { - setting: options, - listSelected: [], - value: '' - }; - }, - watch: { - listSelected(value) { + return loadTableDataTemplate().then(function (template) { + return new Vue({ + el: elem, + data() { + return { + setting: options, + listSelected: [], + value: '' + }; + }, + watch: { + listSelected(value) { - var valueList = value.map(itemValue => itemValue[valueField]); - var valueStr = valueList.join(','); - if (options.selectType == 'checkbox') { - if (options.trim == 0) { - valueStr = ',' + valueStr + ','; - } - } - this.value = valueStr; - } - }, - computed: { - listSelectedShow() { - if (this.setting.fieldName.indexOf('|') < 0) { - return this.listSelected.map( - (item) => { - item['_select_show'] = ua.dataGet(item, this.setting.fieldName); - return item; + var valueList = value.map(itemValue => itemValue[valueField]); + var valueStr = valueList.join(','); + if (options.selectType == 'checkbox') { + if (options.trim == 0) { + valueStr = ',' + valueStr + ','; } - ); - } - - var fieldNameList = this.setting.fieldName.split('|'); - return this.listSelected.map(item => { - var showText = ''; - fieldNameList.forEach(fieldName => { - showText += ua.dataGet(item, fieldName) + ' '; - }); - item['_select_show'] = showText; - - return item; - }); - }, - }, - created() { - this.value = options.selectValue; - if (typeof this.value != 'string') { - this.value = this.value.toString(); - } - var valueLength = this.value.split(',').length; - loading.show(); - $.get(options.index, { - page: 1, - limit: valueLength, - filter: JSON.stringify({ [options.valueField]: this.value }), - op: JSON.stringify({ [options.valueField]: 'in' }), - group: options.valueField - }, (result) => { - loading.hide(); - this.listSelected = result.data; - }); - - }, - mounted() { - if (options.required == 1) { - $(this.$refs['tableData']).closest('.layui-form-item').children('.layui-form-label').addClass('required'); - } - }, - template: tableDataTemplate, - - methods: { - openSelectPage() { - - if (options.readonly == 1) { - return false; - } - - var selectedIds = this.listSelected.map(item => item.id); - var index = layer.open({ - title: '选择数据', - type: 2, - area: [options.width, options.height], - content: options.selectIndex + '&selectedIds=' + selectedIds.join(','), - maxmin: true, - moveOut: true, - shadeClose: true, - success: (layero, index) => { - window[options.selectConfirmCallback] = (data) => { - data.forEach(dataItem => { - - if (options.selectType == 'radio') { - this.listSelected = []; - } - var itemFind = this.listSelected.find(itemSelect => itemSelect[valueField] == dataItem[valueField]); - if (itemFind == undefined) { - this.listSelected.push(dataItem); - } - }); - this.$forceUpdate(); - layer.close(index); - }; - }, - end: function () { - index = null; - window[options.selectConfirmCallback] = undefined; } + this.value = valueStr; + } + }, + computed: { + listSelectedShow() { + if (this.setting.fieldName.indexOf('|') < 0) { + return this.listSelected.map( + (item) => { + item['_select_show'] = ua.dataGet(item, this.setting.fieldName); + return item; + } + ); + } + + var fieldNameList = this.setting.fieldName.split('|'); + return this.listSelected.map(item => { + var showText = ''; + fieldNameList.forEach(fieldName => { + showText += ua.dataGet(item, fieldName) + ' '; + }); + item['_select_show'] = showText; + + return item; + }); + }, + }, + created() { + this.value = options.selectValue; + if (typeof this.value != 'string') { + this.value = this.value.toString(); + } + var valueLength = this.value.split(',').length; + loading.show(); + $.get(options.index, { + page: 1, + limit: valueLength, + filter: JSON.stringify({ [options.valueField]: this.value }), + op: JSON.stringify({ [options.valueField]: 'in' }), + group: options.valueField + }, (result) => { + loading.hide(); + this.listSelected = result.data; }); }, - removeItem(item, index) { - this.listSelected.splice(index, 1); - } - } - }); + mounted() { + if (options.required == 1) { + $(this.$refs['tableData']).closest('.layui-form-item').children('.layui-form-label').addClass('required'); + } + }, + template: template, - return app; + methods: { + openSelectPage() { + + if (options.readonly == 1) { + return false; + } + + var selectedIds = this.listSelected.map(item => item.id); + var index = layer.open({ + title: '选择数据', + type: 2, + area: [options.width, options.height], + content: options.selectIndex + '&selectedIds=' + selectedIds.join(','), + maxmin: true, + moveOut: true, + shadeClose: true, + success: (layero, index) => { + window[options.selectConfirmCallback] = (data) => { + data.forEach(dataItem => { + + if (options.selectType == 'radio') { + this.listSelected = []; + } + var itemFind = this.listSelected.find(itemSelect => itemSelect[valueField] == dataItem[valueField]); + if (itemFind == undefined) { + this.listSelected.push(dataItem); + } + }); + this.$forceUpdate(); + layer.close(index); + }; + }, + end: function () { + index = null; + window[options.selectConfirmCallback] = undefined; + } + }); + + }, + removeItem(item, index) { + this.listSelected.splice(index, 1); + } + } + }); + }); }; window.tableData = new tableData(); diff --git a/public/static/plugs/lay-module/tagInput/tagInput.js b/public/static/plugs/lay-module/tagInput/tagInput.js index 25390e6..48525f0 100644 --- a/public/static/plugs/lay-module/tagInput/tagInput.js +++ b/public/static/plugs/lay-module/tagInput/tagInput.js @@ -1,30 +1,51 @@ (function () { - const tagInputCss = '/static/plugs/lay-module/tagInput/tagInput.css'; - const tagInputHtml = '/static/plugs/lay-module/tagInput/tagInput.html'; + // 组件模板懒加载:首次 render 时才异步请求,同页多实例共享一次请求; + // 路径与版本跟随页面注入的 CONFIG.STATIC / CONFIG.STATIC_VERSION, + // 与 _require.html 中 js/css 的缓存键保持一致;失败不缓存,下次 render 重试。 + // 样式 tagInput.css 已改为在 _require.html 中直接引入,不再由 JS 注入。 + var tagInputTemplatePromise = null; - var tagInput = function () { - $(function () { - var cssElement = document.createElement('link'); - - cssElement.setAttribute('rel', 'stylesheet'); - - cssElement.setAttribute('href', tagInputCss); - - document.body.appendChild(cssElement); - }); - }; - var tagInputTemplate = ''; - - $.ajax({ - type: "get", - url: tagInputHtml, - cache: true, - async: false, - success: function (template) { - tagInputTemplate = template; + function loadTagInputTemplate() { + if (tagInputTemplatePromise !== null) { + return tagInputTemplatePromise; } - }); + var config = window.CONFIG || {}; + var staticRoot = config.STATIC || '/static'; + var version = config.STATIC_VERSION || config.VERSION || ''; + var url = staticRoot + '/plugs/lay-module/tagInput/tagInput.html?t=' + version; + var deferred = $.Deferred(); + + $.ajax({ + type: 'get', + url: url, + cache: true, + dataType: 'text' + }).done(function (template) { + if ($.trim(template)) { + deferred.resolve(template); + } else { + deferred.reject('模板内容为空'); + } + }).fail(function (xhr) { + deferred.reject(xhr); + }); + + tagInputTemplatePromise = deferred.promise(); + tagInputTemplatePromise.fail(function (reason) { + console.error('tagInput 模板加载失败: ' + url, reason); + var ua = window.UA_ADMIN || window.ua; + if (ua) { + ua.msg.error('tagInput 组件初始化失败'); + } + // 失败不缓存,下次 render 自动重试 + tagInputTemplatePromise = null; + }); + + return tagInputTemplatePromise; + } + + var tagInput = function () { }; tagInput.prototype.render = function (elem, data) { @@ -35,81 +56,79 @@ var options = $.extend(defaultOption, data); - - - app = new Vue({ - el: elem, - data() { - return { - setting: options, - value: '', - inputValue: '', - inputWidth: 100, - inputValuePreview: '', - listTag: [] - }; - }, - watch: { - inputValue(value) { - - this.inputValuePreview = value; - this.updateInputWidth(); + return loadTagInputTemplate().then(function (template) { + return new Vue({ + el: elem, + data() { + return { + setting: options, + value: '', + inputValue: '', + inputWidth: 100, + inputValuePreview: '', + listTag: [] + }; }, - listTag(value) { - this.value = value.join(','); - } - }, - created() { - if (options.value.length > 0) { - this.listTag = options.value.split(','); + watch: { + inputValue(value) { - } + this.inputValuePreview = value; + this.updateInputWidth(); + }, + listTag(value) { + this.value = value.join(','); + } + }, + created() { + if (options.value.length > 0) { + this.listTag = options.value.split(','); - }, - mounted() { - if (options.required == 1) { - $(this.$refs['tagInput']).closest('.layui-form-item').children('.layui-form-label').addClass('required'); - } - }, - template: tagInputTemplate, + } - methods: { - onInputConfirm(e) { + }, + mounted() { + if (options.required == 1) { + $(this.$refs['tagInput']).closest('.layui-form-item').children('.layui-form-label').addClass('required'); + } + }, + template: template, - if (e) e.preventDefault(); + methods: { + onInputConfirm(e) { - if ($.trim(this.inputValue).length == 0) { + if (e) e.preventDefault(); + + if ($.trim(this.inputValue).length == 0) { + return false; + } + + this.listTag.push(this.inputValue); + this.inputValue = ''; return false; + }, + onContainerClick() { + $(this.$refs['tagInputMain']).focus(); + }, + onInputChange(e) { + + var cvalue = $(e.target).val(); + this.inputValuePreview = cvalue; + + }, + updateInputWidth() { + var width = $(this.$refs['preshow']).width(); + + if (width < 100) { + width = 100; + } + this.inputWidth = width + 60; + }, + removeItem(item, index) { + this.listTag.splice(index, 1); } - - this.listTag.push(this.inputValue); - this.inputValue = ''; - return false; - }, - onContainerClick() { - $(this.$refs['tagInputMain']).focus(); - }, - onInputChange(e) { - - var cvalue = $(e.target).val(); - this.inputValuePreview = cvalue; - - }, - updateInputWidth() { - var width = $(this.$refs['preshow']).width(); - - if (width < 100) { - width = 100; - } - this.inputWidth = width + 60; - }, - removeItem(item, index) { - this.listTag.splice(index, 1); } - } + }); }); - - return app; }; window.tagInput = new tagInput();