1143 lines
43 KiB
JavaScript
1143 lines
43 KiB
JavaScript
/**
|
||
* TH1 Dashboard - Form Helper
|
||
*/
|
||
|
||
let fhSkills = [];
|
||
let fhSkillLoaded = false;
|
||
let fhSkillLoading = false;
|
||
let fhSkillSortDirection = 'desc';
|
||
let fhActiveCollection = 'skills';
|
||
|
||
const fhCollections = {
|
||
techs: {
|
||
title: 'TechDataAssets / TechList',
|
||
itemLabel: 'TechType',
|
||
nameField: 'TechName',
|
||
descField: 'Description',
|
||
rows: [],
|
||
fields: [],
|
||
readonlyFields: [],
|
||
loaded: false,
|
||
loading: false,
|
||
sortDirection: 'desc',
|
||
},
|
||
'tech-atoms': {
|
||
title: 'TechDataAssets / TechAtomList',
|
||
itemLabel: 'TechAtom',
|
||
nameField: 'TechAtomName',
|
||
descField: 'Desc',
|
||
rows: [],
|
||
fields: [],
|
||
readonlyFields: [],
|
||
loaded: false,
|
||
loading: false,
|
||
sortDirection: 'desc',
|
||
},
|
||
units: {
|
||
title: 'UnitTypeDataAssets / UnitTypeList',
|
||
itemLabel: 'UnitType',
|
||
nameField: 'Name',
|
||
descField: 'Desc',
|
||
rows: [],
|
||
fields: [],
|
||
readonlyFields: [],
|
||
loaded: false,
|
||
loading: false,
|
||
sortDirection: 'desc',
|
||
},
|
||
resources: {
|
||
title: 'GridAndResourceDataAssets / ResourceInfoList',
|
||
itemLabel: 'Resource',
|
||
nameField: 'ResourceName',
|
||
descField: 'ResourceDesc',
|
||
rows: [],
|
||
fields: [],
|
||
readonlyFields: [],
|
||
loaded: false,
|
||
loading: false,
|
||
sortDirection: 'desc',
|
||
},
|
||
actions: {
|
||
title: 'ActionDataAssets / ActionList',
|
||
itemLabel: 'ActionIndex',
|
||
nameField: 'ActionName',
|
||
descField: 'Desc',
|
||
rows: [],
|
||
fields: [],
|
||
readonlyFields: [],
|
||
loaded: false,
|
||
loading: false,
|
||
sortDirection: 'desc',
|
||
},
|
||
texts: {
|
||
title: 'TextDataAssets / Text Fields',
|
||
itemLabel: 'TextIndex',
|
||
nameField: 'FieldName',
|
||
descField: 'Text',
|
||
rows: [],
|
||
fields: [],
|
||
readonlyFields: [],
|
||
loaded: false,
|
||
loading: false,
|
||
sortDirection: 'desc',
|
||
nameReadonly: true,
|
||
},
|
||
'wiki-list': {
|
||
title: 'WikiDataAssets / Items',
|
||
itemLabel: 'Id',
|
||
nameField: 'Name',
|
||
descField: 'Desc',
|
||
rows: [],
|
||
fields: [],
|
||
readonlyFields: [],
|
||
loaded: false,
|
||
loading: false,
|
||
sortDirection: 'desc',
|
||
canAdd: true,
|
||
},
|
||
};
|
||
|
||
const FH_VIEW_TYPES = [
|
||
{ value: 0, label: 'Normal' },
|
||
{ value: 1, label: 'Special' },
|
||
{ value: 2, label: 'Unique' },
|
||
{ value: 3, label: 'Negative' },
|
||
{ value: 4, label: 'Positive' },
|
||
];
|
||
|
||
const FH_PRIORITIES = [
|
||
{ value: 0, label: 'Normal' },
|
||
{ value: 1, label: 'Origin' },
|
||
];
|
||
|
||
function fhEsc(value) {
|
||
const div = document.createElement('div');
|
||
div.textContent = value == null ? '' : String(value);
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function fhAttr(value) {
|
||
return fhEsc(value).replace(/"/g, '"');
|
||
}
|
||
|
||
function fhBindLineTextareas(root) {
|
||
if (!root) return;
|
||
root.querySelectorAll('textarea.fh-line-textarea').forEach(textarea => {
|
||
const resize = () => {
|
||
textarea.style.height = 'auto';
|
||
textarea.style.height = `${textarea.scrollHeight + 2}px`;
|
||
};
|
||
textarea.addEventListener('input', resize);
|
||
resize();
|
||
});
|
||
}
|
||
|
||
function fhLoad() {
|
||
fhInit();
|
||
if (fhActiveCollection !== 'skills') {
|
||
fhLoadCollection(fhActiveCollection);
|
||
} else if (!fhSkillLoaded && !fhSkillLoading) {
|
||
fhLoadSkills();
|
||
}
|
||
}
|
||
|
||
function fhInit() {
|
||
const tabs = document.querySelectorAll('#form-helper-sub-tabs .sub-tab');
|
||
tabs.forEach(tab => {
|
||
if (tab.dataset.bound) return;
|
||
tab.dataset.bound = '1';
|
||
tab.addEventListener('click', () => {
|
||
tabs.forEach(item => item.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.querySelectorAll('#panel-form-helper .sub-panel').forEach(panel => panel.classList.remove('active'));
|
||
const panel = document.getElementById('sub-' + tab.dataset.sub);
|
||
if (panel) panel.classList.add('active');
|
||
fhActiveCollection = tab.dataset.sub === 'fh-skills' ? 'skills' : tab.dataset.sub.replace(/^fh-/, '');
|
||
if (fhActiveCollection === 'skills') {
|
||
fhLoadSkills();
|
||
} else {
|
||
fhLoadCollection(fhActiveCollection);
|
||
}
|
||
});
|
||
});
|
||
|
||
const search = document.getElementById('fh-skill-search');
|
||
if (search && !search.dataset.bound) {
|
||
search.dataset.bound = '1';
|
||
search.addEventListener('input', fhRenderSkills);
|
||
}
|
||
|
||
const refresh = document.getElementById('fh-skill-refresh');
|
||
if (refresh && !refresh.dataset.bound) {
|
||
refresh.dataset.bound = '1';
|
||
refresh.addEventListener('click', () => fhLoadSkills(true));
|
||
}
|
||
|
||
const sort = document.getElementById('fh-skill-sort');
|
||
if (sort && !sort.dataset.bound) {
|
||
sort.dataset.bound = '1';
|
||
sort.addEventListener('click', () => {
|
||
fhSkillSortDirection = fhSkillSortDirection === 'asc' ? 'desc' : 'asc';
|
||
fhRenderSkills();
|
||
});
|
||
}
|
||
fhUpdateSkillSortButton();
|
||
|
||
Object.keys(fhCollections).forEach(key => fhInitCollectionControls(key));
|
||
}
|
||
|
||
async function fhLoadSkills(force = false) {
|
||
if (fhSkillLoading) return;
|
||
if (fhSkillLoaded && !force) return;
|
||
|
||
fhSkillLoading = true;
|
||
const badge = document.getElementById('fh-skill-badge');
|
||
const content = document.getElementById('fh-skill-content');
|
||
if (badge) badge.textContent = '加载中';
|
||
if (content) content.innerHTML = '<div class="loading-inline">正在读取 SkillDataAssets...</div>';
|
||
|
||
try {
|
||
const resp = await fetch('/api/form-helper/skills?t=' + Date.now());
|
||
const data = await resp.json();
|
||
if (!resp.ok || data.error) {
|
||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||
}
|
||
fhSkills = (data.skills || []).map(fhNormalizeSkill);
|
||
fhSkillLoaded = true;
|
||
|
||
const source = document.getElementById('fh-skill-source');
|
||
if (source) {
|
||
source.textContent = `${data.source || 'SkillDataAssets.asset'} · ${data.generatedAt || ''}`;
|
||
}
|
||
fhRenderSkills();
|
||
} catch (err) {
|
||
if (badge) badge.textContent = '读取失败';
|
||
if (content) {
|
||
content.innerHTML = `<div class="loading-inline">读取失败:${fhEsc(err.message || err)}</div>`;
|
||
}
|
||
} finally {
|
||
fhSkillLoading = false;
|
||
}
|
||
}
|
||
|
||
function fhNormalizeSkill(skill) {
|
||
const row = { ...skill };
|
||
row._original = fhCloneSkill(skill);
|
||
return row;
|
||
}
|
||
|
||
function fhCloneSkill(skill) {
|
||
return JSON.parse(JSON.stringify({
|
||
assetIndex: skill.assetIndex,
|
||
skillType: skill.skillType,
|
||
skillViewType: Number(skill.skillViewType || 0),
|
||
name: skill.name || '',
|
||
desc: skill.desc || '',
|
||
notShow: !!skill.notShow,
|
||
showOnUnitMono: !!skill.showOnUnitMono,
|
||
hasShowList: !!skill.hasShowList,
|
||
skillPriority: Number(skill.skillPriority || 0),
|
||
reserveOnCarry: !!skill.reserveOnCarry,
|
||
reserveLeaveCarry: !!skill.reserveLeaveCarry,
|
||
reserveGiantUpgrade: !!skill.reserveGiantUpgrade,
|
||
reserveCommonTransform: !!skill.reserveCommonTransform,
|
||
showList: (skill.showList || []).map(item => ({
|
||
index: item.index,
|
||
unitType: Number(item.unitType || 0),
|
||
giantType: Number(item.giantType || 0),
|
||
unitLevel: Number(item.unitLevel || 0),
|
||
ignoreUnitGiantType: !!item.ignoreUnitGiantType,
|
||
ignoreUnitLevel: !!item.ignoreUnitLevel,
|
||
name: item.name || '',
|
||
desc: item.desc || '',
|
||
})),
|
||
}));
|
||
}
|
||
|
||
function fhRenderSkills() {
|
||
const content = document.getElementById('fh-skill-content');
|
||
if (!content) return;
|
||
if (!fhSkillLoaded) {
|
||
content.innerHTML = '<div class="loading-inline">正在读取 SkillDataAssets...</div>';
|
||
return;
|
||
}
|
||
|
||
const query = (document.getElementById('fh-skill-search')?.value || '').trim().toLowerCase();
|
||
let rows = fhSkills.slice();
|
||
if (query) {
|
||
rows = rows.filter(skill => {
|
||
const haystack = [
|
||
skill.skillType,
|
||
skill.assetIndex,
|
||
skill.skillViewTypeLabel,
|
||
skill.name,
|
||
skill.desc,
|
||
...(skill.showList || []).flatMap(item => [item.name, item.desc]),
|
||
].join(' ').toLowerCase();
|
||
return haystack.includes(query);
|
||
});
|
||
}
|
||
rows.sort((a, b) => {
|
||
const left = Number(a.skillType || 0);
|
||
const right = Number(b.skillType || 0);
|
||
return fhSkillSortDirection === 'asc' ? left - right : right - left;
|
||
});
|
||
fhUpdateSkillSortButton();
|
||
|
||
const badge = document.getElementById('fh-skill-badge');
|
||
if (badge) badge.textContent = `${rows.length}/${fhSkills.length}`;
|
||
|
||
const body = rows.map(skill => `
|
||
<tr data-asset-index="${skill.assetIndex}">
|
||
<td class="num">${skill.skillType}</td>
|
||
<td class="num">${skill.assetIndex}</td>
|
||
<td>
|
||
<input class="fh-line-input" data-field="name" value="${fhAttr(skill.name)}">
|
||
</td>
|
||
<td>
|
||
<textarea class="fh-line-textarea fh-line-desc" data-field="desc">${fhEsc(skill.desc)}</textarea>
|
||
</td>
|
||
<td><span class="badge ${fhBadgeFor(skill.skillViewTypeLabel)}">${fhEsc(skill.skillViewTypeLabel)}</span></td>
|
||
<td>${skill.notShow ? '<span class="badge badge-red">隐藏</span>' : '<span class="badge badge-green">可见</span>'}</td>
|
||
<td class="fh-action-cell">
|
||
<button class="gb-btn-sm fh-restore-row" onclick="fhRestoreRow(${skill.assetIndex})">还原</button>
|
||
<button class="gb-btn-sm gb-btn-primary fh-save-row" onclick="fhSaveRow(${skill.assetIndex})">修改</button>
|
||
<button class="gb-btn-sm fh-detail-row" onclick="fhOpenDetail(${skill.assetIndex})">详细修改</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
content.innerHTML = `
|
||
<div class="table-count">${rows.length} 个 Skill</div>
|
||
<div class="table-wrap fh-table-wrap">
|
||
<table class="dtable fh-skill-table">
|
||
<thead>
|
||
<tr>
|
||
<th>SkillType</th>
|
||
<th>Index</th>
|
||
<th>Name</th>
|
||
<th>Desc</th>
|
||
<th>类型</th>
|
||
<th>显示</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${body || '<tr><td colspan="7" class="fh-empty">没有匹配的 Skill</td></tr>'}</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
fhBindLineTextareas(content);
|
||
}
|
||
|
||
function fhUpdateSkillSortButton() {
|
||
const sort = document.getElementById('fh-skill-sort');
|
||
if (!sort) return;
|
||
sort.textContent = fhSkillSortDirection === 'asc' ? 'ID 正序' : 'ID 倒序';
|
||
}
|
||
|
||
function fhBadgeFor(type) {
|
||
const map = {
|
||
Normal: 'badge-blue',
|
||
Special: 'badge-purple',
|
||
Unique: 'badge-orange',
|
||
Negative: 'badge-red',
|
||
Positive: 'badge-green',
|
||
};
|
||
return map[type] || 'badge-gray';
|
||
}
|
||
|
||
function fhFindSkill(assetIndex) {
|
||
return fhSkills.find(skill => Number(skill.assetIndex) === Number(assetIndex));
|
||
}
|
||
|
||
function fhApplyLineInputs(assetIndex) {
|
||
const skill = fhFindSkill(assetIndex);
|
||
const row = document.querySelector(`#fh-skill-content tr[data-asset-index="${assetIndex}"]`);
|
||
if (!skill || !row) return skill;
|
||
skill.name = row.querySelector('[data-field="name"]')?.value ?? skill.name;
|
||
skill.desc = row.querySelector('[data-field="desc"]')?.value ?? skill.desc;
|
||
return skill;
|
||
}
|
||
|
||
function fhRestoreRow(assetIndex) {
|
||
const skill = fhFindSkill(assetIndex);
|
||
if (!skill || !skill._original) return;
|
||
skill.name = skill._original.name;
|
||
skill.desc = skill._original.desc;
|
||
fhRenderSkills();
|
||
}
|
||
|
||
async function fhSaveRow(assetIndex) {
|
||
const skill = fhApplyLineInputs(assetIndex);
|
||
if (!skill) return;
|
||
await fhSaveSkill(skill, {
|
||
SkillName: skill.name,
|
||
SkillDesc: skill.desc,
|
||
}, undefined, { render: false });
|
||
}
|
||
|
||
function fhCaptureSkillScroll() {
|
||
const wrap = document.querySelector('#fh-skill-content .fh-table-wrap');
|
||
return {
|
||
windowX: window.scrollX,
|
||
windowY: window.scrollY,
|
||
wrapLeft: wrap ? wrap.scrollLeft : 0,
|
||
wrapTop: wrap ? wrap.scrollTop : 0,
|
||
};
|
||
}
|
||
|
||
function fhRestoreSkillScroll(scrollState) {
|
||
if (!scrollState) return;
|
||
requestAnimationFrame(() => {
|
||
const wrap = document.querySelector('#fh-skill-content .fh-table-wrap');
|
||
if (wrap) {
|
||
wrap.scrollLeft = scrollState.wrapLeft;
|
||
wrap.scrollTop = scrollState.wrapTop;
|
||
}
|
||
window.scrollTo(scrollState.windowX, scrollState.windowY);
|
||
});
|
||
}
|
||
|
||
async function fhSaveSkill(skill, values, showList = undefined, options = {}) {
|
||
const payload = {
|
||
assetIndex: skill.assetIndex,
|
||
skillType: skill.skillType,
|
||
values,
|
||
};
|
||
if (showList !== undefined) payload.showList = showList;
|
||
|
||
try {
|
||
const resp = await fetch('/api/form-helper/skills/save', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || !data.success) {
|
||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||
}
|
||
const index = fhSkills.findIndex(item => item.assetIndex === skill.assetIndex);
|
||
if (index >= 0) fhSkills[index] = fhNormalizeSkill(data.skill);
|
||
if (options.render !== false) {
|
||
const scrollState = fhCaptureSkillScroll();
|
||
fhRenderSkills();
|
||
fhRestoreSkillScroll(scrollState);
|
||
}
|
||
fhToast(data.changed ? '已保存 SkillDataAssets' : '没有变化');
|
||
return data.skill;
|
||
} catch (err) {
|
||
fhToast(`保存失败:${err.message || err}`, true);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
function fhToast(message, isError = false) {
|
||
const toast = document.getElementById('toast');
|
||
if (!toast) return;
|
||
toast.textContent = message;
|
||
toast.style.display = 'block';
|
||
toast.style.background = isError ? 'var(--accent-red)' : 'var(--accent-green)';
|
||
clearTimeout(fhToast._timer);
|
||
fhToast._timer = setTimeout(() => {
|
||
toast.style.display = 'none';
|
||
toast.style.background = '';
|
||
}, 2200);
|
||
}
|
||
|
||
function fhOpenDetail(assetIndex) {
|
||
const rowSkill = fhApplyLineInputs(assetIndex);
|
||
const skill = rowSkill ? fhCloneSkill(rowSkill) : null;
|
||
if (!skill) return;
|
||
|
||
document.getElementById('fh-skill-detail-modal')?.remove();
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'fh-skill-detail-modal';
|
||
overlay.className = 'gb-modal-overlay';
|
||
overlay.innerHTML = `
|
||
<div class="gb-modal fh-modal">
|
||
<div class="gb-modal-header">
|
||
<span class="gb-modal-title">Skill ${skill.skillType} · ${fhEsc(skill.name || '未命名')}</span>
|
||
<button class="gb-modal-close" onclick="fhCloseDetail()">×</button>
|
||
</div>
|
||
<div class="gb-modal-body">
|
||
${fhRenderDetailForm(skill)}
|
||
</div>
|
||
<div class="fh-modal-footer">
|
||
<button class="gb-btn-sm" onclick="fhRestoreDetail(${skill.assetIndex})">还原</button>
|
||
<button class="gb-btn-sm gb-btn-primary" onclick="fhSaveDetail(${skill.assetIndex})">保存</button>
|
||
<button class="gb-btn-sm" onclick="fhCloseDetail()">关闭</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
overlay.addEventListener('click', event => {
|
||
if (event.target === overlay) fhCloseDetail();
|
||
});
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
function fhRenderDetailForm(skill) {
|
||
return `
|
||
<div class="fh-detail-grid">
|
||
${fhNumberField('SkillType', 'SkillType', skill.skillType)}
|
||
${fhSelectField('SkillViewType', 'SkillViewType', skill.skillViewType, FH_VIEW_TYPES)}
|
||
${fhSelectField('skillPriority', 'skillPriority', skill.skillPriority, FH_PRIORITIES)}
|
||
${fhCheckboxField('NotShow', 'NotShow', skill.notShow)}
|
||
${fhCheckboxField('ShowOnUnitMono', 'ShowOnUnitMono', skill.showOnUnitMono)}
|
||
${fhCheckboxField('HasShowList', 'HasShowList', skill.hasShowList)}
|
||
${fhCheckboxField('ReserveOnCarry', 'ReserveOnCarry', skill.reserveOnCarry)}
|
||
${fhCheckboxField('ReserveLeaveCarry', 'ReserveLeaveCarry', skill.reserveLeaveCarry)}
|
||
${fhCheckboxField('ReserveGiantUpgrade', 'ReserveGiantUpgrade', skill.reserveGiantUpgrade)}
|
||
${fhCheckboxField('ReserveCommonTransform', 'ReserveCommonTransform', skill.reserveCommonTransform)}
|
||
</div>
|
||
<label class="fh-field fh-wide">
|
||
<span>SkillName</span>
|
||
<input data-fh-field="SkillName" value="${fhAttr(skill.name)}">
|
||
</label>
|
||
<label class="fh-field fh-wide">
|
||
<span>SkillDesc</span>
|
||
<textarea data-fh-field="SkillDesc">${fhEsc(skill.desc)}</textarea>
|
||
</label>
|
||
<div class="fh-readonly-line"><span>SkillIcon</span><code>${fhEsc(fhFindSkill(skill.assetIndex)?.skillIcon || '')}</code></div>
|
||
<div class="fh-section-title">SkillShowList</div>
|
||
${fhRenderShowList(skill.showList || [])}
|
||
`;
|
||
}
|
||
|
||
function fhNumberField(label, key, value) {
|
||
return `<label class="fh-field"><span>${label}</span><input type="number" data-fh-field="${key}" value="${fhAttr(value)}"></label>`;
|
||
}
|
||
|
||
function fhSelectField(label, key, value, options) {
|
||
const opts = options.map(option => `
|
||
<option value="${option.value}" ${Number(value) === option.value ? 'selected' : ''}>${fhEsc(option.label)}</option>
|
||
`).join('');
|
||
return `<label class="fh-field"><span>${label}</span><select data-fh-field="${key}">${opts}</select></label>`;
|
||
}
|
||
|
||
function fhCheckboxField(label, key, value) {
|
||
return `<label class="fh-check"><input type="checkbox" data-fh-field="${key}" ${value ? 'checked' : ''}><span>${label}</span></label>`;
|
||
}
|
||
|
||
function fhRenderShowList(showList) {
|
||
if (!showList.length) {
|
||
return '<div class="fh-empty">无 SkillShowList 条目</div>';
|
||
}
|
||
return showList.map(item => `
|
||
<div class="fh-show-card" data-show-index="${item.index}">
|
||
<div class="fh-show-head">#${item.index}</div>
|
||
<div class="fh-detail-grid">
|
||
${fhNumberShowField(item.index, 'UnitType', item.unitType)}
|
||
${fhNumberShowField(item.index, 'GiantType', item.giantType)}
|
||
${fhNumberShowField(item.index, 'UnitLevel', item.unitLevel)}
|
||
${fhCheckboxShowField(item.index, 'IgnoreUnitGiantType', item.ignoreUnitGiantType)}
|
||
${fhCheckboxShowField(item.index, 'IgnoreUnitLevel', item.ignoreUnitLevel)}
|
||
</div>
|
||
<label class="fh-field fh-wide">
|
||
<span>SkillName</span>
|
||
<input data-fh-show="${item.index}" data-fh-show-field="SkillName" value="${fhAttr(item.name)}">
|
||
</label>
|
||
<label class="fh-field fh-wide">
|
||
<span>SkillDesc</span>
|
||
<textarea data-fh-show="${item.index}" data-fh-show-field="SkillDesc">${fhEsc(item.desc)}</textarea>
|
||
</label>
|
||
<div class="fh-readonly-line"><span>Icon</span><code>${fhEsc(item.icon || '')}</code></div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function fhNumberShowField(index, key, value) {
|
||
return `<label class="fh-field"><span>${key}</span><input type="number" data-fh-show="${index}" data-fh-show-field="${key}" value="${fhAttr(value)}"></label>`;
|
||
}
|
||
|
||
function fhCheckboxShowField(index, key, value) {
|
||
return `<label class="fh-check"><input type="checkbox" data-fh-show="${index}" data-fh-show-field="${key}" ${value ? 'checked' : ''}><span>${key}</span></label>`;
|
||
}
|
||
|
||
function fhCloseDetail() {
|
||
document.getElementById('fh-skill-detail-modal')?.remove();
|
||
}
|
||
|
||
function fhRestoreDetail(assetIndex) {
|
||
const skill = fhFindSkill(assetIndex);
|
||
if (!skill || !skill._original) return;
|
||
const modalBody = document.querySelector('#fh-skill-detail-modal .gb-modal-body');
|
||
if (modalBody) modalBody.innerHTML = fhRenderDetailForm(skill._original);
|
||
}
|
||
|
||
async function fhSaveDetail(assetIndex) {
|
||
const skill = fhFindSkill(assetIndex);
|
||
const modal = document.getElementById('fh-skill-detail-modal');
|
||
if (!skill || !modal) return;
|
||
|
||
const values = {
|
||
SkillType: fhReadField(modal, 'SkillType', 'int'),
|
||
SkillViewType: fhReadField(modal, 'SkillViewType', 'int'),
|
||
SkillName: fhReadField(modal, 'SkillName', 'string'),
|
||
SkillDesc: fhReadField(modal, 'SkillDesc', 'string'),
|
||
NotShow: fhReadField(modal, 'NotShow', 'bool'),
|
||
ShowOnUnitMono: fhReadField(modal, 'ShowOnUnitMono', 'bool'),
|
||
HasShowList: fhReadField(modal, 'HasShowList', 'bool'),
|
||
skillPriority: fhReadField(modal, 'skillPriority', 'int'),
|
||
ReserveOnCarry: fhReadField(modal, 'ReserveOnCarry', 'bool'),
|
||
ReserveLeaveCarry: fhReadField(modal, 'ReserveLeaveCarry', 'bool'),
|
||
ReserveGiantUpgrade: fhReadField(modal, 'ReserveGiantUpgrade', 'bool'),
|
||
ReserveCommonTransform: fhReadField(modal, 'ReserveCommonTransform', 'bool'),
|
||
};
|
||
|
||
const showList = (skill.showList || []).map(item => ({
|
||
UnitType: fhReadShowField(modal, item.index, 'UnitType', 'int'),
|
||
GiantType: fhReadShowField(modal, item.index, 'GiantType', 'int'),
|
||
UnitLevel: fhReadShowField(modal, item.index, 'UnitLevel', 'int'),
|
||
IgnoreUnitGiantType: fhReadShowField(modal, item.index, 'IgnoreUnitGiantType', 'bool'),
|
||
IgnoreUnitLevel: fhReadShowField(modal, item.index, 'IgnoreUnitLevel', 'bool'),
|
||
SkillName: fhReadShowField(modal, item.index, 'SkillName', 'string'),
|
||
SkillDesc: fhReadShowField(modal, item.index, 'SkillDesc', 'string'),
|
||
}));
|
||
|
||
const saved = await fhSaveSkill(skill, values, showList, { render: true });
|
||
fhCloseDetail();
|
||
if (saved) fhOpenDetail(saved.assetIndex);
|
||
}
|
||
|
||
function fhReadField(root, key, kind) {
|
||
const el = root.querySelector(`[data-fh-field="${key}"]`);
|
||
return fhReadInput(el, kind);
|
||
}
|
||
|
||
function fhReadShowField(root, index, key, kind) {
|
||
const el = root.querySelector(`[data-fh-show="${index}"][data-fh-show-field="${key}"]`);
|
||
return fhReadInput(el, kind);
|
||
}
|
||
|
||
function fhReadInput(el, kind) {
|
||
if (!el) return kind === 'bool' ? false : kind === 'int' ? 0 : '';
|
||
if (kind === 'bool') return !!el.checked;
|
||
if (kind === 'int') return Number.parseInt(el.value || '0', 10) || 0;
|
||
return el.value || '';
|
||
}
|
||
|
||
function fhInitCollectionControls(key) {
|
||
const search = document.getElementById(`fh-${key}-search`);
|
||
if (search && !search.dataset.bound) {
|
||
search.dataset.bound = '1';
|
||
search.addEventListener('input', () => fhRenderCollection(key));
|
||
}
|
||
|
||
const refresh = document.getElementById(`fh-${key}-refresh`);
|
||
if (refresh && !refresh.dataset.bound) {
|
||
refresh.dataset.bound = '1';
|
||
refresh.addEventListener('click', () => fhLoadCollection(key, true));
|
||
}
|
||
|
||
const sort = document.getElementById(`fh-${key}-sort`);
|
||
if (sort && !sort.dataset.bound) {
|
||
sort.dataset.bound = '1';
|
||
sort.addEventListener('click', () => {
|
||
const config = fhCollections[key];
|
||
config.sortDirection = config.sortDirection === 'asc' ? 'desc' : 'asc';
|
||
fhRenderCollection(key);
|
||
});
|
||
}
|
||
fhUpdateCollectionSortButton(key);
|
||
|
||
const add = document.getElementById(`fh-${key}-add`);
|
||
if (add && !add.dataset.bound) {
|
||
add.dataset.bound = '1';
|
||
add.addEventListener('click', () => fhOpenCollectionAdd(key));
|
||
}
|
||
}
|
||
|
||
async function fhLoadCollection(key, force = false) {
|
||
const config = fhCollections[key];
|
||
if (!config || config.loading) return;
|
||
if (config.loaded && !force) return;
|
||
|
||
config.loading = true;
|
||
const badge = document.getElementById(`fh-${key}-badge`);
|
||
const content = document.getElementById(`fh-${key}-content`);
|
||
if (badge) badge.textContent = '加载中';
|
||
if (content) content.innerHTML = `<div class="loading-inline">正在读取 ${fhEsc(config.title)}...</div>`;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/form-helper/collection/${key}?t=${Date.now()}`);
|
||
const data = await resp.json();
|
||
if (!resp.ok || data.error || data.success === false) {
|
||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||
}
|
||
config.title = data.title || config.title;
|
||
config.fields = data.fields || [];
|
||
config.readonlyFields = data.readonlyFields || [];
|
||
config.canAdd = !!data.canAdd;
|
||
config.rows = (data.rows || []).map(row => fhNormalizeCollectionRow(row));
|
||
config.loaded = true;
|
||
|
||
const source = document.getElementById(`fh-${key}-source`);
|
||
if (source) {
|
||
source.textContent = `${data.source || config.title} · ${data.generatedAt || ''}`;
|
||
}
|
||
fhRenderCollection(key);
|
||
} catch (err) {
|
||
if (badge) badge.textContent = '读取失败';
|
||
if (content) content.innerHTML = `<div class="loading-inline">读取失败:${fhEsc(err.message || err)}</div>`;
|
||
} finally {
|
||
config.loading = false;
|
||
}
|
||
}
|
||
|
||
function fhNormalizeCollectionRow(row) {
|
||
const normalized = { ...row };
|
||
normalized._original = fhCloneCollectionRow(row);
|
||
return normalized;
|
||
}
|
||
|
||
function fhCloneCollectionRow(row) {
|
||
return JSON.parse(JSON.stringify({
|
||
assetIndex: row.assetIndex,
|
||
itemId: row.itemId,
|
||
name: row.name || '',
|
||
desc: row.desc || '',
|
||
values: row.values || {},
|
||
fields: row.fields || [],
|
||
readonly: row.readonly || {},
|
||
}));
|
||
}
|
||
|
||
function fhRenderCollection(key) {
|
||
const config = fhCollections[key];
|
||
const content = document.getElementById(`fh-${key}-content`);
|
||
if (!config || !content) return;
|
||
if (!config.loaded) {
|
||
content.innerHTML = `<div class="loading-inline">正在读取 ${fhEsc(config.title)}...</div>`;
|
||
return;
|
||
}
|
||
|
||
const query = (document.getElementById(`fh-${key}-search`)?.value || '').trim().toLowerCase();
|
||
let rows = config.rows.slice();
|
||
if (query) {
|
||
rows = rows.filter(row => {
|
||
const haystack = [
|
||
row.assetIndex,
|
||
row.itemId,
|
||
row.name,
|
||
row.desc,
|
||
row.searchText,
|
||
JSON.stringify(row.values || {}),
|
||
].join(' ').toLowerCase();
|
||
return haystack.includes(query);
|
||
});
|
||
}
|
||
|
||
rows.sort((a, b) => {
|
||
const left = Number(a.itemId || 0);
|
||
const right = Number(b.itemId || 0);
|
||
if (left !== right) {
|
||
return config.sortDirection === 'asc' ? left - right : right - left;
|
||
}
|
||
return Number(a.assetIndex || 0) - Number(b.assetIndex || 0);
|
||
});
|
||
fhUpdateCollectionSortButton(key);
|
||
|
||
const badge = document.getElementById(`fh-${key}-badge`);
|
||
if (badge) badge.textContent = `${rows.length}/${config.rows.length}`;
|
||
|
||
const body = rows.map(row => `
|
||
<tr data-collection="${key}" data-asset-index="${row.assetIndex}">
|
||
<td class="num">${fhEsc(row.itemId)}</td>
|
||
<td class="num">${fhEsc(row.assetIndex)}</td>
|
||
<td>
|
||
${config.nameReadonly ? `<span class="fh-readonly-name">${fhEsc(row.name)}</span>` : `<input class="fh-line-input" data-field="name" value="${fhAttr(row.name)}">`}
|
||
</td>
|
||
<td>
|
||
<textarea class="fh-line-textarea fh-line-desc" data-field="desc">${fhEsc(row.desc)}</textarea>
|
||
</td>
|
||
<td class="fh-summary-cell">${fhEsc(fhCollectionSummary(key, row))}</td>
|
||
<td class="fh-action-cell">
|
||
<button class="gb-btn-sm fh-restore-row" onclick="fhRestoreCollectionRow('${key}', ${row.assetIndex})">还原</button>
|
||
<button class="gb-btn-sm gb-btn-primary fh-save-row" onclick="fhSaveCollectionRow('${key}', ${row.assetIndex})">修改</button>
|
||
<button class="gb-btn-sm fh-detail-row" onclick="fhOpenCollectionDetail('${key}', ${row.assetIndex})">详细修改</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
content.innerHTML = `
|
||
<div class="table-count">${rows.length} 条记录</div>
|
||
<div class="table-wrap fh-table-wrap">
|
||
<table class="dtable fh-collection-table">
|
||
<thead>
|
||
<tr>
|
||
<th>${fhEsc(config.itemLabel || 'ID')}</th>
|
||
<th>Index</th>
|
||
<th>Name</th>
|
||
<th>Desc</th>
|
||
<th>字段摘要</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${body || '<tr><td colspan="6" class="fh-empty">没有匹配记录</td></tr>'}</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
fhBindLineTextareas(content);
|
||
}
|
||
|
||
function fhCollectionSummary(key, row) {
|
||
const config = fhCollections[key];
|
||
if (!config) return '';
|
||
const skip = new Set([config.nameField, config.descField]);
|
||
return (config.fields || [])
|
||
.filter(field => !skip.has(field.key))
|
||
.slice(0, 6)
|
||
.map(field => `${field.label || field.key}: ${fhCompactValue(row.values?.[field.key])}`)
|
||
.join(' · ');
|
||
}
|
||
|
||
function fhCompactValue(value) {
|
||
if (Array.isArray(value)) return value.join(',');
|
||
if (typeof value === 'boolean') return value ? '1' : '0';
|
||
return value == null ? '' : String(value);
|
||
}
|
||
|
||
function fhUpdateCollectionSortButton(key) {
|
||
const config = fhCollections[key];
|
||
const sort = document.getElementById(`fh-${key}-sort`);
|
||
if (!config || !sort) return;
|
||
sort.textContent = config.sortDirection === 'asc' ? 'ID 正序' : 'ID 倒序';
|
||
}
|
||
|
||
function fhFindCollectionRow(key, assetIndex) {
|
||
return fhCollections[key]?.rows.find(row => Number(row.assetIndex) === Number(assetIndex));
|
||
}
|
||
|
||
function fhApplyCollectionLineInputs(key, assetIndex) {
|
||
const config = fhCollections[key];
|
||
const row = fhFindCollectionRow(key, assetIndex);
|
||
const tr = document.querySelector(`#fh-${key}-content tr[data-asset-index="${assetIndex}"]`);
|
||
if (!config || !row || !tr) return row;
|
||
if (!config.nameReadonly) {
|
||
row.name = tr.querySelector('[data-field="name"]')?.value ?? row.name;
|
||
}
|
||
row.desc = tr.querySelector('[data-field="desc"]')?.value ?? row.desc;
|
||
row.values = { ...(row.values || {}) };
|
||
if (!config.nameReadonly) {
|
||
row.values[config.nameField] = row.name;
|
||
}
|
||
row.values[config.descField] = row.desc;
|
||
return row;
|
||
}
|
||
|
||
function fhRestoreCollectionRow(key, assetIndex) {
|
||
const row = fhFindCollectionRow(key, assetIndex);
|
||
if (!row || !row._original) return;
|
||
const restored = fhNormalizeCollectionRow(row._original);
|
||
const rows = fhCollections[key].rows;
|
||
const index = rows.findIndex(item => item.assetIndex === assetIndex);
|
||
if (index >= 0) rows[index] = restored;
|
||
fhRenderCollection(key);
|
||
}
|
||
|
||
async function fhSaveCollectionRow(key, assetIndex) {
|
||
const config = fhCollections[key];
|
||
const row = fhApplyCollectionLineInputs(key, assetIndex);
|
||
if (!config || !row) return;
|
||
const values = config.nameReadonly
|
||
? { [config.descField]: row.desc }
|
||
: { [config.nameField]: row.name, [config.descField]: row.desc };
|
||
await fhSaveCollection(key, row, values, { render: false });
|
||
}
|
||
|
||
function fhCaptureCollectionScroll(key) {
|
||
const wrap = document.querySelector(`#fh-${key}-content .fh-table-wrap`);
|
||
return {
|
||
windowX: window.scrollX,
|
||
windowY: window.scrollY,
|
||
wrapLeft: wrap ? wrap.scrollLeft : 0,
|
||
wrapTop: wrap ? wrap.scrollTop : 0,
|
||
};
|
||
}
|
||
|
||
function fhRestoreCollectionScroll(key, scrollState) {
|
||
if (!scrollState) return;
|
||
requestAnimationFrame(() => {
|
||
const wrap = document.querySelector(`#fh-${key}-content .fh-table-wrap`);
|
||
if (wrap) {
|
||
wrap.scrollLeft = scrollState.wrapLeft;
|
||
wrap.scrollTop = scrollState.wrapTop;
|
||
}
|
||
window.scrollTo(scrollState.windowX, scrollState.windowY);
|
||
});
|
||
}
|
||
|
||
async function fhSaveCollection(key, row, values, options = {}) {
|
||
const payload = {
|
||
assetIndex: row.assetIndex,
|
||
itemId: row.itemId,
|
||
values,
|
||
};
|
||
|
||
try {
|
||
const resp = await fetch(`/api/form-helper/collection/${key}/save`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || !data.success) {
|
||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||
}
|
||
const rows = fhCollections[key].rows;
|
||
const index = rows.findIndex(item => item.assetIndex === row.assetIndex);
|
||
if (index >= 0) rows[index] = fhNormalizeCollectionRow(data.row);
|
||
if (options.render !== false) {
|
||
const scrollState = fhCaptureCollectionScroll(key);
|
||
fhRenderCollection(key);
|
||
fhRestoreCollectionScroll(key, scrollState);
|
||
}
|
||
fhToast(data.changed ? `已保存 ${fhCollections[key].title}` : '没有变化');
|
||
return data.row;
|
||
} catch (err) {
|
||
fhToast(`保存失败:${err.message || err}`, true);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
async function fhOpenCollectionAdd(key) {
|
||
const config = fhCollections[key];
|
||
if (!config || !config.canAdd) return;
|
||
if (!config.loaded) {
|
||
await fhLoadCollection(key, true);
|
||
}
|
||
if (!config.fields?.length) {
|
||
fhToast('字段配置尚未加载', true);
|
||
return;
|
||
}
|
||
|
||
const maxId = Math.max(0, ...(config.rows || []).map(row => Number(row.itemId || 0)));
|
||
const row = {
|
||
assetIndex: -1,
|
||
itemId: maxId + 1,
|
||
name: '',
|
||
desc: '',
|
||
values: {},
|
||
fields: (config.fields || []).map(field => ({ ...field })),
|
||
readonly: {},
|
||
};
|
||
(config.fields || []).forEach(field => {
|
||
if (field.key === config.itemLabel || field.key === 'Id') {
|
||
row.values[field.key] = maxId + 1;
|
||
} else if (field.key === config.nameField) {
|
||
row.values[field.key] = '';
|
||
} else if (field.key === config.descField) {
|
||
row.values[field.key] = '';
|
||
} else if (field.kind === 'bool') {
|
||
row.values[field.key] = false;
|
||
} else if (field.kind === 'intList') {
|
||
row.values[field.key] = [];
|
||
} else if (field.kind === 'int' || field.kind === 'float') {
|
||
row.values[field.key] = 0;
|
||
} else {
|
||
row.values[field.key] = '';
|
||
}
|
||
});
|
||
|
||
document.getElementById('fh-collection-add-modal')?.remove();
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'fh-collection-add-modal';
|
||
overlay.className = 'gb-modal-overlay';
|
||
overlay.innerHTML = `
|
||
<div class="gb-modal fh-modal">
|
||
<div class="gb-modal-header">
|
||
<span class="gb-modal-title">新增 ${fhEsc(config.title)}</span>
|
||
<button class="gb-modal-close" onclick="fhCloseCollectionAdd()">×</button>
|
||
</div>
|
||
<div class="gb-modal-body">
|
||
${fhRenderCollectionDetailForm(key, row)}
|
||
</div>
|
||
<div class="fh-modal-footer">
|
||
<button class="gb-btn-sm gb-btn-primary" onclick="fhSaveCollectionAdd('${key}')">新增</button>
|
||
<button class="gb-btn-sm" onclick="fhCloseCollectionAdd()">关闭</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
overlay.addEventListener('click', event => {
|
||
if (event.target === overlay) fhCloseCollectionAdd();
|
||
});
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
function fhCloseCollectionAdd() {
|
||
document.getElementById('fh-collection-add-modal')?.remove();
|
||
}
|
||
|
||
async function fhSaveCollectionAdd(key) {
|
||
const config = fhCollections[key];
|
||
const modal = document.getElementById('fh-collection-add-modal');
|
||
if (!config || !modal) return;
|
||
|
||
const values = {};
|
||
(config.fields || []).forEach(field => {
|
||
values[field.key] = fhReadCollectionField(modal, field.key, field.kind || 'string');
|
||
});
|
||
|
||
try {
|
||
const resp = await fetch(`/api/form-helper/collection/${key}/add`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ values }),
|
||
});
|
||
const data = await resp.json();
|
||
if (!resp.ok || !data.success) {
|
||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||
}
|
||
if (data.row) {
|
||
config.rows.push(fhNormalizeCollectionRow(data.row));
|
||
} else {
|
||
config.loaded = false;
|
||
await fhLoadCollection(key, true);
|
||
}
|
||
fhCloseCollectionAdd();
|
||
fhRenderCollection(key);
|
||
fhToast(`已新增 ${config.title}`);
|
||
if (data.row) fhOpenCollectionDetail(key, data.row.assetIndex);
|
||
} catch (err) {
|
||
fhToast(`新增失败:${err.message || err}`, true);
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
function fhOpenCollectionDetail(key, assetIndex) {
|
||
const liveRow = fhApplyCollectionLineInputs(key, assetIndex);
|
||
const row = liveRow ? fhCloneCollectionRow(liveRow) : null;
|
||
const config = fhCollections[key];
|
||
if (!row || !config) return;
|
||
|
||
document.getElementById('fh-collection-detail-modal')?.remove();
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'fh-collection-detail-modal';
|
||
overlay.className = 'gb-modal-overlay';
|
||
overlay.innerHTML = `
|
||
<div class="gb-modal fh-modal">
|
||
<div class="gb-modal-header">
|
||
<span class="gb-modal-title">${fhEsc(config.title)} · ${fhEsc(row.itemId)} · ${fhEsc(row.name || '未命名')}</span>
|
||
<button class="gb-modal-close" onclick="fhCloseCollectionDetail()">×</button>
|
||
</div>
|
||
<div class="gb-modal-body">
|
||
${fhRenderCollectionDetailForm(key, row)}
|
||
</div>
|
||
<div class="fh-modal-footer">
|
||
<button class="gb-btn-sm" onclick="fhRestoreCollectionDetail('${key}', ${row.assetIndex})">还原</button>
|
||
<button class="gb-btn-sm gb-btn-primary" onclick="fhSaveCollectionDetail('${key}', ${row.assetIndex})">保存</button>
|
||
<button class="gb-btn-sm" onclick="fhCloseCollectionDetail()">关闭</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
overlay.addEventListener('click', event => {
|
||
if (event.target === overlay) fhCloseCollectionDetail();
|
||
});
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
function fhRenderCollectionDetailForm(key, row) {
|
||
const config = fhCollections[key];
|
||
const fields = row.fields?.length ? row.fields : config.fields;
|
||
const controls = fields.map(field => fhRenderCollectionField(field, row.values?.[field.key])).join('');
|
||
const readonly = row.readonly || {};
|
||
const readonlyHtml = Object.keys(readonly).length
|
||
? `<div class="fh-section-title">只读字段</div>${Object.entries(readonly).map(([label, value]) => fhReadonlyValue(label, value)).join('')}`
|
||
: '';
|
||
return `
|
||
<div class="fh-detail-grid">
|
||
${controls}
|
||
</div>
|
||
${readonlyHtml}
|
||
`;
|
||
}
|
||
|
||
function fhRenderCollectionField(field, value) {
|
||
const key = field.key;
|
||
const kind = field.kind || 'string';
|
||
const label = field.label || key;
|
||
if (kind === 'bool') {
|
||
return fhCheckboxField(label, key, !!value).replaceAll('data-fh-field', 'data-fh-collection-field');
|
||
}
|
||
if (kind === 'int') {
|
||
return `<label class="fh-field"><span>${fhEsc(label)}</span><input type="number" data-fh-collection-field="${fhAttr(key)}" data-kind="${kind}" value="${fhAttr(value)}"></label>`;
|
||
}
|
||
if (kind === 'float') {
|
||
return `<label class="fh-field"><span>${fhEsc(label)}</span><input type="number" step="0.01" data-fh-collection-field="${fhAttr(key)}" data-kind="${kind}" value="${fhAttr(value)}"></label>`;
|
||
}
|
||
if (kind === 'intList') {
|
||
return `<label class="fh-field fh-wide"><span>${fhEsc(label)}</span><textarea data-fh-collection-field="${fhAttr(key)}" data-kind="${kind}" placeholder="用逗号或空格分隔 ID">${fhEsc(Array.isArray(value) ? value.join(', ') : '')}</textarea></label>`;
|
||
}
|
||
return `<label class="fh-field fh-wide"><span>${fhEsc(label)}</span><textarea data-fh-collection-field="${fhAttr(key)}" data-kind="${kind}">${fhEsc(value || '')}</textarea></label>`;
|
||
}
|
||
|
||
function fhReadonlyValue(label, value) {
|
||
const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
||
return `<div class="fh-readonly-line"><span>${fhEsc(label)}</span><code>${fhEsc(text || '')}</code></div>`;
|
||
}
|
||
|
||
function fhCloseCollectionDetail() {
|
||
document.getElementById('fh-collection-detail-modal')?.remove();
|
||
}
|
||
|
||
function fhRestoreCollectionDetail(key, assetIndex) {
|
||
const row = fhFindCollectionRow(key, assetIndex);
|
||
if (!row || !row._original) return;
|
||
const modalBody = document.querySelector('#fh-collection-detail-modal .gb-modal-body');
|
||
if (modalBody) modalBody.innerHTML = fhRenderCollectionDetailForm(key, row._original);
|
||
}
|
||
|
||
async function fhSaveCollectionDetail(key, assetIndex) {
|
||
const row = fhFindCollectionRow(key, assetIndex);
|
||
const modal = document.getElementById('fh-collection-detail-modal');
|
||
if (!row || !modal) return;
|
||
|
||
const values = {};
|
||
(fhCollections[key].fields || []).forEach(field => {
|
||
values[field.key] = fhReadCollectionField(modal, field.key, field.kind || 'string');
|
||
});
|
||
|
||
const saved = await fhSaveCollection(key, row, values, { render: true });
|
||
fhCloseCollectionDetail();
|
||
if (saved) fhOpenCollectionDetail(key, saved.assetIndex);
|
||
}
|
||
|
||
function fhReadCollectionField(root, key, kind) {
|
||
const el = root.querySelector(`[data-fh-collection-field="${key}"]`);
|
||
if (!el) {
|
||
if (kind === 'bool') return false;
|
||
if (kind === 'int' || kind === 'float') return 0;
|
||
if (kind === 'intList') return [];
|
||
return '';
|
||
}
|
||
if (kind === 'bool') return !!el.checked;
|
||
if (kind === 'int') return Number.parseInt(el.value || '0', 10) || 0;
|
||
if (kind === 'float') return Number.parseFloat(el.value || '0') || 0;
|
||
if (kind === 'intList') {
|
||
return (el.value || '')
|
||
.split(/[,,\s]+/)
|
||
.map(item => item.trim())
|
||
.filter(Boolean)
|
||
.map(item => Number.parseInt(item, 10))
|
||
.filter(item => Number.isFinite(item));
|
||
}
|
||
return el.value || '';
|
||
}
|
||
|
||
window.fhLoad = fhLoad;
|
||
window.fhRestoreRow = fhRestoreRow;
|
||
window.fhSaveRow = fhSaveRow;
|
||
window.fhOpenDetail = fhOpenDetail;
|
||
window.fhRestoreDetail = fhRestoreDetail;
|
||
window.fhSaveDetail = fhSaveDetail;
|
||
window.fhCloseDetail = fhCloseDetail;
|
||
window.fhRestoreCollectionRow = fhRestoreCollectionRow;
|
||
window.fhSaveCollectionRow = fhSaveCollectionRow;
|
||
window.fhOpenCollectionDetail = fhOpenCollectionDetail;
|
||
window.fhRestoreCollectionDetail = fhRestoreCollectionDetail;
|
||
window.fhSaveCollectionDetail = fhSaveCollectionDetail;
|
||
window.fhCloseCollectionDetail = fhCloseCollectionDetail;
|
||
window.fhOpenCollectionAdd = fhOpenCollectionAdd;
|
||
window.fhSaveCollectionAdd = fhSaveCollectionAdd;
|
||
window.fhCloseCollectionAdd = fhCloseCollectionAdd;
|