refactor(component): tagInput/propertyInput/tableData 模板改为懒加载异步获取

原实现三个组件在 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,无兼容影响
This commit is contained in:
augushong
2026-10-05 14:08:03 +00:00
parent 14b190d9a8
commit f3ac7189f2
5 changed files with 392 additions and 319 deletions

View File

@@ -9,6 +9,9 @@
<link rel="stylesheet" href="__STATIC__/admin/css/public.css?t={:get_site_version_key()}" media="all"> <link rel="stylesheet" href="__STATIC__/admin/css/public.css?t={:get_site_version_key()}" media="all">
<link rel="stylesheet" href="__STATIC__/common/css/theme/index.css?t={:get_site_version_key()}"> <link rel="stylesheet" href="__STATIC__/common/css/theme/index.css?t={:get_site_version_key()}">
<link rel="stylesheet" href="__STATIC__/plugs/wangeditor/style.css"> <link rel="stylesheet" href="__STATIC__/plugs/wangeditor/style.css">
<link rel="stylesheet" href="__STATIC__/plugs/lay-module/tagInput/tagInput.css?t={:get_site_version_key()}">
<link rel="stylesheet" href="__STATIC__/plugs/lay-module/propertyInput/propertyInput.css?t={:get_site_version_key()}">
<link rel="stylesheet" href="__STATIC__/plugs/lay-module/tableData/tableData.css?t={:get_site_version_key()}">
<!-- 基础js --> <!-- 基础js -->
<script src="__STATIC__/plugs/jquery-3.4.1/jquery-3.4.1.min.js?t={:get_site_version_key()}"></script> <script src="__STATIC__/plugs/jquery-3.4.1/jquery-3.4.1.min.js?t={:get_site_version_key()}"></script>

View File

@@ -17,7 +17,13 @@
IS_SUPER_ADMIN: "{$isSuperAdmin|default='false'}", IS_SUPER_ADMIN: "{$isSuperAdmin|default='false'}",
VERSION: "{$version|default='1.0.0'}", VERSION: "{$version|default='1.0.0'}",
CSRF_TOKEN: "{:token()}", 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 = { var PATH_CONFIG = {

View File

@@ -1,30 +1,51 @@
(function () { (function () {
const propertyInputCss = '/static/plugs/lay-module/propertyInput/propertyInput.css'; // 组件模板懒加载:首次 render 时才异步请求,同页多实例共享一次请求;
const propertyInputHtml = '/static/plugs/lay-module/propertyInput/propertyInput.html'; // 路径与版本跟随页面注入的 CONFIG.STATIC / CONFIG.STATIC_VERSION,
// 与 _require.html 中 js/css 的缓存键保持一致;失败不缓存,下次 render 重试。
// 样式 propertyInput.css 已改为在 _require.html 中直接引入,不再由 JS 注入。
var propertyInputTemplatePromise = null;
var propertyInput = function () { function loadPropertyInputTemplate() {
$(function () { if (propertyInputTemplatePromise !== null) {
var cssElement = document.createElement('link'); return propertyInputTemplatePromise;
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;
} }
}); 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) { propertyInput.prototype.render = function (elem, data) {
@@ -184,88 +205,88 @@
} }
} }
app = new Vue({ return loadPropertyInputTemplate().then(function (template) {
el: elem, return new Vue({
data() { el: elem,
return { data() {
setting: options, return {
value: '', setting: options,
listItem: [ value: '',
listItem: [
], ],
originalValue: '', originalValue: '',
field: options.field, field: options.field,
}; };
}, },
created() { created() {
if (this.setting.value) { if (this.setting.value) {
this.originalValue = $.extend(true, [], this.setting.value); this.originalValue = $.extend(true, [], this.setting.value);
this.listItem = $.extend(true, [], this.setting.value); this.listItem = $.extend(true, [], this.setting.value);
this.updateValue(this.listItem); this.updateValue(this.listItem);
if (this.listItem.length == 0) { if (this.listItem.length == 0) {
this.onAddItem(); 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(); window.propertyInput = new propertyInput();

View File

@@ -1,29 +1,53 @@
(function () { (function () {
const tableDataCss = '/static/plugs/lay-module/tableData/tableData.css'; // 组件模板懒加载:首次 render 时才异步请求,同页多实例共享一次请求;
const tableDataHtml = '/static/plugs/lay-module/tableData/tableData.html'; // 路径与版本跟随页面注入的 CONFIG.STATIC / CONFIG.STATIC_VERSION,
// 与 _require.html 中 js/css 的缓存键保持一致;失败不缓存,下次 render 重试。
// 样式 tableData.css 已改为在 _require.html 中直接引入,不再由 JS 注入。
var tableDataTemplatePromise = null;
var tableData = function () { function loadTableDataTemplate() {
$(function () { if (tableDataTemplatePromise !== null) {
var cssElement = document.createElement('link'); return tableDataTemplatePromise;
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;
} }
});
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) { tableData.prototype.render = function (elem, data) {
var app;
var clienWidth, clientHeight; var clienWidth, clientHeight;
clienWidth = '100%'; clienWidth = '100%';
clientHeight = '100%'; clientHeight = '100%';
@@ -65,123 +89,123 @@
options.className = 'layui-textarea'; options.className = 'layui-textarea';
} }
app = new Vue({ return loadTableDataTemplate().then(function (template) {
el: elem, return new Vue({
data() { el: elem,
return { data() {
setting: options, return {
listSelected: [], setting: options,
value: '' listSelected: [],
}; value: ''
}, };
watch: { },
listSelected(value) { watch: {
listSelected(value) {
var valueList = value.map(itemValue => itemValue[valueField]); var valueList = value.map(itemValue => itemValue[valueField]);
var valueStr = valueList.join(','); var valueStr = valueList.join(',');
if (options.selectType == 'checkbox') { if (options.selectType == 'checkbox') {
if (options.trim == 0) { if (options.trim == 0) {
valueStr = ',' + valueStr + ','; 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 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) { mounted() {
this.listSelected.splice(index, 1); 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(); window.tableData = new tableData();

View File

@@ -1,30 +1,51 @@
(function () { (function () {
const tagInputCss = '/static/plugs/lay-module/tagInput/tagInput.css'; // 组件模板懒加载:首次 render 时才异步请求,同页多实例共享一次请求;
const tagInputHtml = '/static/plugs/lay-module/tagInput/tagInput.html'; // 路径与版本跟随页面注入的 CONFIG.STATIC / CONFIG.STATIC_VERSION,
// 与 _require.html 中 js/css 的缓存键保持一致;失败不缓存,下次 render 重试。
// 样式 tagInput.css 已改为在 _require.html 中直接引入,不再由 JS 注入。
var tagInputTemplatePromise = null;
var tagInput = function () { function loadTagInputTemplate() {
$(function () { if (tagInputTemplatePromise !== null) {
var cssElement = document.createElement('link'); return tagInputTemplatePromise;
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;
} }
}); 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) { tagInput.prototype.render = function (elem, data) {
@@ -35,81 +56,79 @@
var options = $.extend(defaultOption, data); var options = $.extend(defaultOption, data);
return loadTagInputTemplate().then(function (template) {
return new Vue({
app = new Vue({ el: elem,
el: elem, data() {
data() { return {
return { setting: options,
setting: options, value: '',
value: '', inputValue: '',
inputValue: '', inputWidth: 100,
inputWidth: 100, inputValuePreview: '',
inputValuePreview: '', listTag: []
listTag: [] };
};
},
watch: {
inputValue(value) {
this.inputValuePreview = value;
this.updateInputWidth();
}, },
listTag(value) { watch: {
this.value = value.join(','); inputValue(value) {
}
},
created() {
if (options.value.length > 0) {
this.listTag = options.value.split(',');
} 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; 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(); window.tagInput = new tagInput();