You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
388 lines
18 KiB
388 lines
18 KiB
(function (global) {
|
|
'use strict';
|
|
|
|
/**
|
|
* 资源面板(壳侧):Bundle 优先 + 去重内存 + 可预览。
|
|
* @constructor
|
|
*/
|
|
function AssetsPanel() {
|
|
this.el = null;
|
|
this._bridge = null;
|
|
this._rows = [];
|
|
this._bundles = [];
|
|
this._totals = null;
|
|
this._bundleFilter = '';
|
|
this._typeFilter = '';
|
|
this._query = '';
|
|
this._sort = 'mem';
|
|
this._selectedUuid = '';
|
|
}
|
|
|
|
AssetsPanel.prototype.setBridge = function (bridge) {
|
|
this._bridge = bridge;
|
|
};
|
|
|
|
AssetsPanel.prototype.mount = function () {
|
|
var self = this;
|
|
var root = document.createElement('div');
|
|
root.className = 'pw-panel-inner pw-assets';
|
|
root.style.height = '100%';
|
|
root.style.display = 'flex';
|
|
root.style.flexDirection = 'column';
|
|
root.innerHTML =
|
|
'<div class="pw-panel-toolbar">' +
|
|
'<input id="pw-asset-q" class="pw-input" placeholder="搜索资源 / Bundle" style="flex:1;min-width:80px" />' +
|
|
'<select id="pw-asset-type" class="pw-select"><option value="">类型</option></select>' +
|
|
'<select id="pw-asset-sort" class="pw-select">' +
|
|
'<option value="mem">按内存</option><option value="name">按名称</option>' +
|
|
'<option value="type">按类型</option><option value="ref">按引用</option>' +
|
|
'</select>' +
|
|
'<button type="button" id="pw-asset-refresh" class="pw-btn pw-btn-icon" title="刷新" aria-label="刷新">⟳</button>' +
|
|
'</div>' +
|
|
'<div id="pw-asset-summary" class="pw-assets-summary"></div>' +
|
|
'<div class="pw-assets-body">' +
|
|
'<div class="pw-assets-bundles" id="pw-asset-bundles"></div>' +
|
|
'<div class="pw-assets-main">' +
|
|
'<div class="pw-assets-list" id="pw-asset-list"></div>' +
|
|
'<div class="pw-assets-side">' +
|
|
'<div class="pw-assets-preview" id="pw-asset-preview"><span class="pw-muted">选择资源预览</span></div>' +
|
|
'<div id="pw-asset-meta" class="pw-assets-meta"></div>' +
|
|
'<div class="pw-assets-refs"><h5>引用来源</h5><div id="pw-asset-refs"><span class="pw-muted">选择资源后显示</span></div></div>' +
|
|
'</div></div></div>';
|
|
this.el = root;
|
|
|
|
root.querySelector('#pw-asset-q').addEventListener('input', function (ev) {
|
|
self._query = (ev.target.value || '').toLowerCase();
|
|
self.renderList();
|
|
});
|
|
root.querySelector('#pw-asset-type').addEventListener('change', function (ev) {
|
|
self._typeFilter = ev.target.value;
|
|
self.renderList();
|
|
});
|
|
root.querySelector('#pw-asset-sort').addEventListener('change', function (ev) {
|
|
self._sort = ev.target.value;
|
|
self.renderList();
|
|
});
|
|
root.querySelector('#pw-asset-refresh').addEventListener('click', function () {
|
|
if (!self._bridge) return;
|
|
self._bridge.call('assets.list').then(function (d) { self.applyData(d); });
|
|
});
|
|
return root;
|
|
};
|
|
|
|
AssetsPanel.prototype.applyData = function (data) {
|
|
this._rows = (data && data.rows) || [];
|
|
this._bundles = (data && data.bundles) || [];
|
|
this._totals = (data && data.totals) || null;
|
|
this.renderSummary();
|
|
this.renderBundles();
|
|
this.renderList();
|
|
};
|
|
|
|
AssetsPanel.prototype.renderSummary = function () {
|
|
var el = this.el && this.el.querySelector('#pw-asset-summary');
|
|
if (!el) return;
|
|
var t = this._totals || {};
|
|
var f = function (n) {
|
|
n = Number(n) || 0;
|
|
return n >= 100 ? Math.round(n) : Math.round(n * 10) / 10;
|
|
};
|
|
el.innerHTML =
|
|
'<div class="pw-assets-kpi"><b>' + (t.bundleCount != null ? t.bundleCount : this._bundles.length) + '</b><span>Bundle</span></div>' +
|
|
'<div class="pw-assets-kpi"><b>' + (t.assetCount != null ? t.assetCount : this._rows.length) + '</b><span>资源</span></div>' +
|
|
'<div class="pw-assets-kpi accent"><b>' + f(t.estimateMB) + '</b><span>估算 MB</span></div>' +
|
|
'<div class="pw-assets-kpi"><b>' + f(t.texMB) + '</b><span>纹理</span></div>' +
|
|
'<div class="pw-assets-kpi"><b>' + f(t.gfxTextureMB) + '</b><span>GFX纹理</span></div>' +
|
|
'<div class="pw-assets-kpi"><b>' + f(t.jsHeapMB) + '</b><span>JS堆</span></div>' +
|
|
'<div class="pw-assets-note">估算=去重纹理+音频+其它;GFX/JS堆为实测</div>';
|
|
};
|
|
|
|
AssetsPanel.prototype.renderBundles = function () {
|
|
var el = this.el && this.el.querySelector('#pw-asset-bundles');
|
|
if (!el) return;
|
|
var q = this._query;
|
|
var list = this._bundles.slice();
|
|
if (q) {
|
|
list = list.filter(function (b) {
|
|
return String(b.name).toLowerCase().indexOf(q) >= 0;
|
|
});
|
|
}
|
|
var maxMem = 1;
|
|
for (var i = 0; i < this._bundles.length; i++) {
|
|
maxMem = Math.max(maxMem, this._bundles[i].memMB || 0);
|
|
}
|
|
var html = ['<div class="pw-assets-bundle-head">Bundle · ' + this._bundles.length + '</div>'];
|
|
var allActive = !this._bundleFilter;
|
|
html.push(
|
|
'<button type="button" class="pw-bundle-item' + (allActive ? ' active' : '') + '" data-bundle="">' +
|
|
'<div class="pw-bundle-name">全部</div>' +
|
|
'<div class="pw-bundle-sub">' + this._rows.length + ' 项</div>' +
|
|
'</button>'
|
|
);
|
|
for (var j = 0; j < list.length; j++) {
|
|
var b = list[j];
|
|
var mem = Number(b.memMB) || 0;
|
|
var pct = maxMem > 0 ? Math.min(100, (mem / maxMem) * 100) : 0;
|
|
var act = this._bundleFilter === b.name ? ' active' : '';
|
|
var st = b.status || (b.assetCount > 0 ? 'loaded' : 'unloaded');
|
|
var stCls = st === 'loaded' ? 'st-loaded' : (st === 'loaded-empty' ? 'st-empty' : 'st-unloaded');
|
|
var stText = st === 'loaded' ? '已加载' : (st === 'loaded-empty' ? '空缓存' : '未加载');
|
|
html.push(
|
|
'<button type="button" class="pw-bundle-item' + act + ' ' + stCls + '" data-bundle="' + escapeAttr(b.name) + '">' +
|
|
'<div class="pw-bundle-name">' + escapeHtml(b.name) +
|
|
'<span class="pw-bundle-tag">' + stText + '</span></div>' +
|
|
'<div class="pw-bundle-sub">' + (b.assetCount || 0) + ' 项 · ' + fmtMB(mem) +
|
|
(b.priority ? ' · P' + b.priority : '') + '</div>' +
|
|
'<div class="pw-bundle-bar"><i style="width:' + pct + '%"></i></div>' +
|
|
'</button>'
|
|
);
|
|
}
|
|
el.innerHTML = html.join('');
|
|
var self = this;
|
|
var btns = el.querySelectorAll('.pw-bundle-item');
|
|
for (var k = 0; k < btns.length; k++) {
|
|
btns[k].addEventListener('click', function (ev) {
|
|
var name = ev.currentTarget.getAttribute('data-bundle') || '';
|
|
self._bundleFilter = name;
|
|
self.renderBundles();
|
|
self.renderList();
|
|
});
|
|
}
|
|
};
|
|
|
|
AssetsPanel.prototype.renderList = function () {
|
|
if (!this.el) return;
|
|
var rows = this._rows.slice();
|
|
var q = this._query;
|
|
var bf = this._bundleFilter;
|
|
var tf = this._typeFilter;
|
|
rows = rows.filter(function (r) {
|
|
if (bf) {
|
|
var inBundle = r.bundle === bf;
|
|
var used = r.usedBy && r.usedBy.indexOf(bf) >= 0;
|
|
if (!inBundle && !used) return false;
|
|
}
|
|
if (tf && r.type !== tf && r.kind !== tf) return false;
|
|
if (q && String(r.name).toLowerCase().indexOf(q) < 0 && String(r.type).toLowerCase().indexOf(q) < 0) return false;
|
|
return true;
|
|
});
|
|
var sort = this._sort;
|
|
rows.sort(function (a, b) {
|
|
if (sort === 'name') return String(a.name).localeCompare(String(b.name));
|
|
if (sort === 'type') return String(a.type).localeCompare(String(b.type)) || String(a.name).localeCompare(String(b.name));
|
|
if (sort === 'ref') return (b.ref || 0) - (a.ref || 0);
|
|
return (b.mem || 0) - (a.mem || 0);
|
|
});
|
|
|
|
// 类型选项
|
|
var typeSel = this.el.querySelector('#pw-asset-type');
|
|
if (typeSel) {
|
|
var types = {};
|
|
for (var i = 0; i < this._rows.length; i++) types[this._rows[i].type] = 1;
|
|
var cur = this._typeFilter;
|
|
var html = '<option value="">类型</option>';
|
|
var keys = Object.keys(types).sort();
|
|
for (var t = 0; t < keys.length; t++) {
|
|
html += '<option value="' + escapeAttr(keys[t]) + '"' + (keys[t] === cur ? ' selected' : '') + '>' + escapeHtml(keys[t]) + '</option>';
|
|
}
|
|
typeSel.innerHTML = html;
|
|
}
|
|
|
|
var listEl = this.el.querySelector('#pw-asset-list');
|
|
if (!listEl) return;
|
|
if (!rows.length) {
|
|
listEl.innerHTML = '<div class="pw-muted" style="padding:16px">无匹配资源</div>';
|
|
return;
|
|
}
|
|
var out = [];
|
|
out.push('<div class="pw-assets-row pw-assets-row-head"><span>名称</span><span>类型</span><span>内存</span><span>引用</span><span>操作</span></div>');
|
|
var maxShow = Math.min(rows.length, 400);
|
|
for (var j = 0; j < maxShow; j++) {
|
|
var r = rows[j];
|
|
var sel = r.uuid === this._selectedUuid ? ' active' : '';
|
|
var memText = fmtMB(r.mem);
|
|
if (r.type === 'SpriteFrame') {
|
|
memText = (r.w && r.h) ? (r.w + '×' + r.h) : '共享纹理';
|
|
} else if (r.type === 'ImageAsset') {
|
|
memText = (r.w && r.h) ? (r.w + '×' + r.h) : '源像素';
|
|
}
|
|
var tag = '';
|
|
if (r.pack) tag += ' <span class="pw-assets-tag">图集</span>';
|
|
if (r.type === 'Texture2D' && r.pack) tag += ' <span class="pw-assets-tag">合包</span>';
|
|
if (r.via === 'dep') tag += ' <span class="pw-assets-tag pw-assets-tag-dep">依赖</span>';
|
|
out.push(
|
|
'<div class="pw-assets-row' + sel + '" data-uuid="' + escapeAttr(r.uuid) + '" title="' + escapeAttr((r.path || r.type) + ' · ' + r.bundle) + '">' +
|
|
'<span class="pw-assets-name">' + escapeHtml(r.name) + tag + '</span>' +
|
|
'<span class="pw-assets-type">' + escapeHtml(r.type) + '</span>' +
|
|
'<span class="pw-assets-mem">' + escapeHtml(memText) + '</span>' +
|
|
'<span class="pw-assets-ref">' + (r.ref || 0) + '</span>' +
|
|
'<span class="pw-res-ops">' +
|
|
'<button type="button" class="pw-btn pw-btn-mini" data-copy="name" title="复制名称">名</button>' +
|
|
'<button type="button" class="pw-btn pw-btn-mini" data-copy="uuid" title="复制 UUID">ID</button>' +
|
|
'</span></div>'
|
|
);
|
|
}
|
|
if (rows.length > maxShow) {
|
|
out.push('<div class="pw-muted" style="padding:8px">… 共 ' + rows.length + ' 项,已显示前 ' + maxShow + '</div>');
|
|
}
|
|
listEl.innerHTML = out.join('');
|
|
var self = this;
|
|
listEl.onclick = function (ev) {
|
|
var btn = ev.target.closest ? ev.target.closest('button[data-copy]') : null;
|
|
var item = (ev.target.closest && ev.target.closest('.pw-assets-row[data-uuid]')) || null;
|
|
if (btn && item) {
|
|
ev.stopPropagation();
|
|
var uuid = item.getAttribute('data-uuid') || '';
|
|
var name = (item.querySelector('.pw-assets-name') && item.querySelector('.pw-assets-name').textContent) || '';
|
|
if (btn.getAttribute('data-copy') === 'name') copyText(name.trim());
|
|
else copyText(uuid);
|
|
btn.classList.add('copied');
|
|
setTimeout(function () { btn.classList.remove('copied'); }, 600);
|
|
return;
|
|
}
|
|
if (!item) return;
|
|
var u = item.getAttribute('data-uuid');
|
|
self._selectedUuid = u;
|
|
self.renderList();
|
|
self.loadPreview(u);
|
|
};
|
|
};
|
|
|
|
AssetsPanel.prototype.loadPreview = function (uuid) {
|
|
var self = this;
|
|
var meta = this.el && this.el.querySelector('#pw-asset-meta');
|
|
var box = this.el && this.el.querySelector('#pw-asset-preview');
|
|
var refsEl = this.el && this.el.querySelector('#pw-asset-refs');
|
|
if (!this._bridge) return;
|
|
if (box) box.innerHTML = '<span class="pw-muted">加载预览…</span>';
|
|
if (meta) meta.innerHTML = '';
|
|
if (refsEl) refsEl.innerHTML = '<span class="pw-muted">…</span>';
|
|
|
|
this._bridge.call('assets.preview', { uuid: uuid }).then(function (d) {
|
|
if (!d) return;
|
|
if (box) {
|
|
if (d.kind === 'image' && d.url) {
|
|
box.innerHTML = '<img alt="" src="' + d.url + '" />';
|
|
} else if (d.kind === 'url' && d.url) {
|
|
box.innerHTML = '<img alt="" src="' + d.url + '" />';
|
|
} else {
|
|
box.innerHTML = '<span class="pw-muted">无预览<br><i>' + escapeHtml(d.type || '') + '</i></span>';
|
|
}
|
|
}
|
|
if (meta) {
|
|
meta.innerHTML =
|
|
'<div class="pw-asset-kv"><span>名称</span><b>' + escapeHtml(d.name || '') +
|
|
' <button type="button" class="pw-btn pw-btn-mini" data-copy-val="' + escapeAttr(d.name || '') + '">复制</button></b></div>' +
|
|
'<div class="pw-asset-kv"><span>UUID</span><b class="pw-mono">' + escapeHtml(d.uuid || '') +
|
|
' <button type="button" class="pw-btn pw-btn-mini" data-copy-val="' + escapeAttr(d.uuid || '') + '">复制</button></b></div>' +
|
|
'<div class="pw-asset-kv"><span>Bundle</span><b>' + escapeHtml(d.bundle || '—') + '</b></div>' +
|
|
'<div class="pw-asset-kv"><span>路径</span><b>' + escapeHtml(d.path || '—') + '</b></div>' +
|
|
'<div class="pw-asset-kv"><span>类型</span><b>' + escapeHtml(d.type || '') + '</b></div>' +
|
|
'<div class="pw-asset-kv"><span>引用计数</span><b>' + (d.ref || 0) + '</b></div>' +
|
|
(d.w && d.h ? '<div class="pw-asset-kv"><span>尺寸</span><b>' + d.w + '×' + d.h + '</b></div>' : '');
|
|
meta.onclick = function (ev) {
|
|
var b = ev.target.closest ? ev.target.closest('[data-copy-val]') : null;
|
|
if (!b) return;
|
|
copyText(b.getAttribute('data-copy-val') || '');
|
|
};
|
|
}
|
|
if (refsEl) {
|
|
var refs = d.refs || [];
|
|
if (!refs.length) {
|
|
refsEl.innerHTML = '<span class="pw-muted">无反向引用</span>';
|
|
} else {
|
|
var html = [];
|
|
for (var i = 0; i < refs.length; i++) {
|
|
html.push(
|
|
'<div class="pw-asset-ref-item" data-uuid="' + escapeAttr(refs[i].uuid || '') +
|
|
'" data-type="' + escapeAttr(refs[i].type || '') +
|
|
'" title="双击定位 · ' + escapeAttr(refs[i].uuid || '') + '">' +
|
|
'<b>' + escapeHtml(refs[i].name) + '</b>' +
|
|
'<i class="pw-muted">' + escapeHtml(refs[i].type || '') + '</i>' +
|
|
'</div>'
|
|
);
|
|
}
|
|
refsEl.innerHTML = html.join('');
|
|
}
|
|
refsEl.ondblclick = function (ev) {
|
|
var item = ev.target.closest ? ev.target.closest('.pw-asset-ref-item') : null;
|
|
if (!item) return;
|
|
self._locateRef(item.getAttribute('data-uuid') || '', item.getAttribute('data-type') || '');
|
|
};
|
|
}
|
|
}).catch(function () {
|
|
if (box) box.innerHTML = '<span class="pw-muted">预览失败</span>';
|
|
});
|
|
};
|
|
|
|
function copyText(text) {
|
|
try {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(String(text));
|
|
return;
|
|
}
|
|
} catch (e) { /* */ }
|
|
var ta = document.createElement('textarea');
|
|
ta.value = String(text);
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
try { document.execCommand('copy'); } catch (e2) { /* */ }
|
|
document.body.removeChild(ta);
|
|
}
|
|
|
|
/** 双击引用项:资源→左侧列表定位;节点→层级面板选中 */
|
|
AssetsPanel.prototype._locateRef = function (uuid, type) {
|
|
if (!uuid) return;
|
|
if (type === 'Node') {
|
|
// 层级选中 + 高亮(走引擎 hier.select → pick.selected 同步)
|
|
if (this._bridge) {
|
|
this._bridge.call('hier.select', { uuid: uuid }).catch(function () { /* */ });
|
|
this._bridge.call('pick.select', { uuid: uuid }).catch(function () { /* */ });
|
|
}
|
|
return;
|
|
}
|
|
// 资源:列表定位并选中
|
|
var known = false;
|
|
for (var i = 0; i < this._rows.length; i++) {
|
|
if (this._rows[i].uuid === uuid) { known = true; break; }
|
|
}
|
|
if (!known) {
|
|
// 当前筛选下可能不可见:清筛选后再找
|
|
this._bundleFilter = '';
|
|
this._typeFilter = '';
|
|
this._query = '';
|
|
var qEl = this.el && this.el.querySelector('#pw-asset-q');
|
|
if (qEl) qEl.value = '';
|
|
}
|
|
this._selectedUuid = uuid;
|
|
this.renderList();
|
|
this.loadPreview(uuid);
|
|
var listEl = this.el && this.el.querySelector('#pw-asset-list');
|
|
if (listEl) {
|
|
var row = listEl.querySelector('.pw-assets-row[data-uuid="' + cssEscape(uuid) + '"]');
|
|
if (row && row.scrollIntoView) row.scrollIntoView({ block: 'center' });
|
|
}
|
|
};
|
|
|
|
function cssEscape(s) {
|
|
return String(s).replace(/"/g, '\\"');
|
|
}
|
|
|
|
function fmtMB(mb) {
|
|
var n = Number(mb) || 0;
|
|
if (n <= 0) return '0';
|
|
if (n < 0.01) return '<0.01MB';
|
|
if (n < 10) return (Math.round(n * 100) / 100) + 'MB';
|
|
return (Math.round(n * 10) / 10) + 'MB';
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s == null ? '' : s)
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
function escapeAttr(s) {
|
|
return escapeHtml(s).replace(/"/g, '"');
|
|
}
|
|
|
|
global.PWAssetsPanel = AssetsPanel;
|
|
})(window);
|
|
|