Files
Leantime/app/Domain/Bom/Templates/detail.blade.php.bak-handsontable

470 lines
24 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends($layout)
@section('content')
@php
$bom = $detail['bom'] ?? [];
$columns = $detail['columns'] ?? [];
$items = $detail['items'] ?? [];
$sources = $detail['sources'] ?? [];
$hiddenColumns = $detail['hiddenColumns'] ?? [];
$filledKeys = $detail['filledKeys'] ?? [];
$imageColumns = $detail['imageColumns'] ?? [];
$fileColumns = $detail['fileColumns'] ?? [];
$bomId = (int)($bom['id'] ?? 0);
@endphp
<link rel="stylesheet" href="{{ BASE_URL }}/dist/handsontable/handsontable.full.min.css">
<div class="pageheader">
<div class="pageicon"><span class="fa fa-fw fa-list-check"></span></div>
<div class="pagetitle">
<h1>{{ $bom['productName'] ?? $bom['bomNo'] ?? __('menu.bom') }}</h1>
<p>{{ __('text.bom_subtitle', 'BOM 明细') }} — {{ $bom['bomNo'] ?? '' }}</p>
</div>
<div class="maincontent" style="margin-top:16px;">
<a href="{{ BASE_URL }}/bom/show" class="btn btn-default"><i class="fa fa-arrow-left"></i> {{ __('links.back', '返回') }}</a>
<button type="button" class="btn btn-primary" id="btnAddRow"><i class="fa fa-plus"></i> {{ __('label.add_row', '新增行') }}</button>
<button type="button" class="btn btn-default" id="btnAddColumn"><i class="fa fa-columns"></i> {{ __('label.add_column', '增加列') }}</button>
<button type="button" class="btn btn-default" id="btnColumnSettings"><i class="fa fa-eye"></i> 列设置</button>
{{-- Teable 导入下拉 --}}
<div class="btn-group" style="position:relative;display:inline-block;">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-cloud-download"></i> Teable 导入 <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="javascript:void(0)" id="btnTeableImport"><i class="fa fa-refresh"></i> 从数据源导入</a></li>
<li><a href="javascript:void(0)" id="btnTeablePaste"><i class="fa fa-paste"></i> 粘贴 API 导入</a></li>
<li class="divider"></li>
<li><a href="javascript:void(0)" id="btnSourceManager"><i class="fa fa-database"></i> 数据源管理</a></li>
</ul>
</div>
{{-- Excel 下拉 --}}
<div class="btn-group" style="position:relative;display:inline-block;">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-file-excel"></i> Excel <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="javascript:void(0)" id="btnExcelTemplate"><i class="fa fa-download"></i> 模板导出</a></li>
<li><a href="javascript:void(0)" id="btnExcelImport"><i class="fa fa-upload"></i> 导入数据</a></li>
<li class="divider"></li>
<li><a href="javascript:void(0)" id="btnExcelExport"><i class="fa fa-file-excel-o"></i> 导出数据 (xlsx)</a></li>
<li><a href="javascript:void(0)" id="btnExcelExportCsv"><i class="fa fa-file"></i> 导出数据 (csv)</a></li>
</ul>
</div>
</div>
</div>
<div class="maincontent">
<div class="maincontentinner">
{{-- Teable 源配置区已移至「数据源管理」弹窗 --}}
<div id="bomAlert" class="alert" style="display:none;"></div>
{{-- Handsontable 容器JS 动态铺满剩余视口) --}}
<div id="bomHot" style="width:100%;height:600px;overflow:hidden;"></div>
</div>
</div>
{{-- Teable API 粘贴导入弹窗 --}}
<div id="teablePasteModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:9999;overflow:auto;">
<div style="max-width:760px;margin:6% auto;background:#fff;border-radius:4px;padding:18px;">
<h4 style="margin-top:0;"><i class="fa fa-paste"></i> 粘贴 Teable API 文档</h4>
<p class="text-muted">直接粘贴整段 API 文档或 curl 命令,自动解析连接信息;解析不全时可手动补齐下方字段。</p>
<textarea id="teablePasteText" style="width:100%;height:180px;font-family:monospace;font-size:12px;" placeholder="# Table: UAGP350B&#10;&#10;curl -X GET &quot;https://table.universal-onebot.com/api/table/tblXXX/record?fieldKeyType=name&quot; \&#10; -H &quot;Authorization: Bearer teable_...&quot;"></textarea>
<div style="margin-top:10px;">
<div class="row">
<div class="col-md-12"><label style="font-size:12px;">表名/别名</label><input type="text" id="pasteName" class="form-control"></div>
</div>
<div class="row" style="margin-top:6px;">
<div class="col-md-12"><label style="font-size:12px;">Base URL</label><input type="text" id="pasteBaseUrl" class="form-control" placeholder="https://table.universal-onebot.com"></div>
</div>
<div class="row" style="margin-top:6px;">
<div class="col-md-5"><label style="font-size:12px;">Table ID</label><input type="text" id="pasteTableId" class="form-control" placeholder="tbl..."></div>
<div class="col-md-7"><label style="font-size:12px;">Token</label><input type="text" id="pasteToken" class="form-control" placeholder="teable_acc..."></div>
</div>
</div>
<div id="teableParseResult" style="display:none;margin:10px 0;padding:10px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;font-size:13px;"></div>
<div style="text-align:right;margin-top:12px;">
<button type="button" class="btn btn-default" id="btnTeableParse"><i class="fa fa-search"></i> 解析</button>
<button type="button" class="btn btn-primary" id="btnTeablePasteGo"><i class="fa fa-cloud-download"></i> 导入</button>
<button type="button" class="btn btn-default" id="btnTeablePasteClose">取消</button>
</div>
</div>
</div>
{{-- 列设置弹窗 --}}
<div id="columnSettingsModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:9999;overflow:auto;">
<div style="max-width:520px;margin:6% auto;background:#fff;border-radius:4px;padding:18px;">
<h4 style="margin-top:0;"><i class="fa fa-eye"></i> 列设置</h4>
<p class="text-muted">勾选需要显示的列;未勾选的列将被隐藏。</p>
<div id="columnChecklist" style="max-height:360px;overflow-y:auto;border:1px solid #ddd;padding:10px;border-radius:4px;"></div>
<div style="margin-top:10px;">
<button type="button" class="btn btn-default btn-xs" id="btnHideEmpty"><i class="fa fa-eye-slash"></i> 一键隐藏空列</button>
<button type="button" class="btn btn-default btn-xs" id="btnShowAll"><i class="fa fa-eye"></i> 全部显示</button>
</div>
<div style="text-align:right;margin-top:12px;">
<button type="button" class="btn btn-primary" id="btnColumnSettingsSave">确定</button>
<button type="button" class="btn btn-default" id="btnColumnSettingsClose">取消</button>
</div>
</div>
</div>
{{-- 数据源管理弹窗 --}}
<div id="sourceManagerModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.45);z-index:9999;overflow:auto;">
<div style="max-width:640px;margin:6% auto;background:#fff;border-radius:4px;padding:18px;">
<h4 style="margin-top:0;"><i class="fa fa-database"></i> Teable 数据源管理</h4>
<div id="sourceList" style="margin-bottom:10px;">
@forelse ($sources as $s)
<span class="label label-info" style="margin-right:6px;">
{{ $s['name'] ?: $s['tableId'] }}
<a href="javascript:void(0)" class="btnDelSource" data-id="{{ $s['id'] }}" style="color:#fff;">&times;</a>
</span>
@empty
<em class="text-muted">{{ __('text.no_sources', '未配置数据源') }}</em>
@endforelse
</div>
<div class="row">
<div class="col-md-3"><input type="text" id="srcName" class="form-control" placeholder="{{ __('label.source_name', '表名/别名') }}"></div>
<div class="col-md-3"><input type="text" id="srcBaseUrl" class="form-control" placeholder="https://table.universal-onebot.com"></div>
<div class="col-md-2"><input type="text" id="srcTableId" class="form-control" placeholder="tbl..."></div>
<div class="col-md-3"><input type="text" id="srcToken" class="form-control" placeholder="teable_acc..."></div>
<div class="col-md-1"><button type="button" class="btn btn-default" id="btnAddSource">{{ __('links.add', '添加') }}</button></div>
</div>
<div style="text-align:right;margin-top:12px;">
<button type="button" class="btn btn-default" id="btnSourceManagerClose">关闭</button>
</div>
</div>
</div>
{{-- 隐藏文件输入 --}}
<input type="file" id="excelFile" accept=".xlsx,.csv" style="display:none;">
<script src="{{ BASE_URL }}/dist/handsontable/handsontable.full.min.js"></script>
@once
@push('scripts')
<script>
jQuery(document).ready(function () {
var BOM_ID = {{ $bomId }};
var BASE = '{{ BASE_URL }}';
var FIXED = {!! json_encode(array_keys($fixedColumns)) !!};
var FIXED_LABELS = {!! json_encode($fixedColumns) !!};
var HIDDEN = {!! json_encode(array_values($hiddenColumns)) !!};
var FILLED = {!! json_encode(array_values($filledKeys)) !!};
var COLUMNS = {!! json_encode(array_map(function($c){ return ['key'=>$c['key'],'label'=>$c['label']]; }, $columns)) !!};
var IMAGE_COLS = {!! json_encode(array_values($imageColumns)) !!};
var FILE_COLS = {!! json_encode(array_values($fileColumns)) !!};
var RAW_ITEMS = {!! json_encode($items, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) !!};
var ALL_KEYS = FIXED.concat(COLUMNS.map(function (c) { return c.key; }));
var csrf = function () { return jQuery('meta[name="csrf-token"]').attr('content') || ''; };
function showAlert(msg, type) {
var el = jQuery('#bomAlert');
el.attr('class', 'alert alert-' + (type || 'info')).text(msg).show();
}
function api(method, url, data, cb) {
var opts = { url: url, method: method, headers: { 'X-CSRF-TOKEN': csrf() }, dataType: 'json' };
if (data !== undefined) { opts.contentType = 'application/json'; opts.data = JSON.stringify(data); }
return jQuery.ajax(opts).done(function (res) { cb && cb(res); }).fail(function (xhr) {
var m = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : ('HTTP ' + xhr.status);
showAlert(m, 'danger');
});
}
// ---- 数据映射:后端 items -> Handsontable data ----
var data = RAW_ITEMS.map(function (it) {
var o = { _id: it.id || null };
ALL_KEYS.forEach(function (k) { o[k] = (it[k] === undefined || it[k] === null) ? '' : it[k]; });
return o;
});
// ---- renderer ----
function parseAttach(value) {
if (Array.isArray(value)) { return value; }
if (typeof value === 'string' && value.charAt(0) === '[') {
try { var a = JSON.parse(value); return Array.isArray(a) ? a : []; } catch (e) { return []; }
}
return [];
}
function imageRenderer(instance, td, row, col, prop, value) {
td.innerHTML = '';
td.style.verticalAlign = 'middle';
parseAttach(value).forEach(function (it) {
if (!it || !it.url) { return; }
var a = document.createElement('a');
a.href = it.url; a.target = '_blank'; a.title = it.name || '';
var img = document.createElement('img');
img.src = it.url; img.alt = it.name || '';
img.style.height = '42px'; img.style.width = 'auto'; img.style.margin = '2px'; img.style.borderRadius = '2px';
a.appendChild(img); td.appendChild(a);
});
return td;
}
function fileRenderer(instance, td, row, col, prop, value) {
td.innerHTML = '';
parseAttach(value).forEach(function (it) {
if (!it || !it.url) { return; }
var a = document.createElement('a');
a.href = it.url; a.target = '_blank'; a.textContent = it.name || '下载';
a.style.display = 'block'; a.style.margin = '2px 0';
td.appendChild(a);
});
return td;
}
function delRenderer(instance, td, row) {
td.innerHTML = '';
td.style.textAlign = 'center';
var btn = document.createElement('button');
btn.type = 'button'; btn.className = 'btn btn-xs btn-danger';
btn.textContent = '删';
btn.onclick = function () { deleteRow(row); };
td.appendChild(btn);
return td;
}
// ---- 列定义 ----
var colDefs = [];
FIXED.forEach(function (key) {
colDefs.push({ data: key, title: FIXED_LABELS[key] || key, width: 120 });
});
COLUMNS.forEach(function (col) {
colDefs.push({ data: col.key, title: col.label || col.key, width: 140 });
});
IMAGE_COLS.forEach(function (key) {
var d = colDefs.filter(function (c) { return c.data === key; })[0];
if (d) { d.renderer = imageRenderer; d.readOnly = true; d.width = 120; }
});
FILE_COLS.forEach(function (key) {
var d = colDefs.filter(function (c) { return c.data === key; })[0];
if (d) { d.renderer = fileRenderer; d.readOnly = true; d.width = 160; }
});
// 操作列
colDefs.push({ data: '_delete', title: '操作', width: 60, readOnly: true, renderer: delRenderer });
// 初始隐藏列索引
var hiddenIndexes = HIDDEN.map(function (k) { return ALL_KEYS.indexOf(k); }).filter(function (i) { return i >= 0; });
// ---- 初始化 Handsontable ----
var hotContainer = document.getElementById('bomHot');
function calcHeight() {
var top = jQuery(hotContainer).offset().top;
var winH = jQuery(window).height();
var h = winH - top - 24;
return h < 300 ? 300 : h;
}
var hot = new Handsontable(hotContainer, {
data: data,
columns: colDefs,
colHeaders: true,
rowHeaders: false,
width: '100%',
height: calcHeight(),
licenseKey: 'non-commercial-and-evaluation',
stretchH: 'all',
manualColumnResize: true,
manualRowResize: true,
filters: true,
dropdownMenu: true,
search: true,
contextMenu: ['row_above', 'row_below', 'remove_row', '---------', 'copy', 'cut', 'paste', '---------', 'undo', 'redo'],
copyPaste: true,
undo: true,
hiddenColumns: { columns: hiddenIndexes, indicators: true },
afterChange: function (changes, source) {
if (!changes || source === 'loadData' || source === 'updateData' || source === 'alter') { return; }
var rows = {};
changes.forEach(function (ch) {
if (ch[2] === ch[3]) { return; }
rows[ch[0]] = true;
});
Object.keys(rows).forEach(function (r) { saveRow(parseInt(r, 10)); });
}
});
// 表格铺满屏幕:窗口尺寸变化时重算高度
jQuery(window).on('resize', function () { hot.updateSettings({ height: calcHeight() }); });
// ---- 保存行 ----
function saveRow(row) {
var d = hot.getSourceDataAtRow(row);
if (!d) { return; }
var payload = {};
ALL_KEYS.forEach(function (k) { payload[k] = d[k] || ''; });
if (d._id) { payload.itemId = d._id; }
api('POST', BASE + '/bom/api/' + BOM_ID + '/item', payload, function (res) {
if (res.itemId && !d._id) { d._id = res.itemId; }
});
}
// ---- 删除行 ----
function deleteRow(row) {
if (!confirm("{{ __('text.confirm_delete') }}")) { return; }
var d = hot.getSourceDataAtRow(row);
if (!d._id) { hot.alter('remove_row', row); return; }
api('DELETE', BASE + '/bom/api/item/' + d._id, null, function () { hot.alter('remove_row', row); });
}
// ---- 新增行 ----
jQuery('#btnAddRow').on('click', function () {
var empty = { _id: null };
ALL_KEYS.forEach(function (k) { empty[k] = ''; });
data.push(empty);
hot.updateData(data);
hot.selectCell(data.length - 1, 0);
});
// ---- 列显隐 ----
function openColumnSettings() {
var html = '';
FIXED.forEach(function (key) {
html += '<label style="display:block;font-weight:normal;margin-bottom:4px;"><input type="checkbox" class="col-check" data-key="' + key + '"' + (HIDDEN.indexOf(key) === -1 ? ' checked' : '') + '> ' + (FIXED_LABELS[key] || key) + '</label>';
});
COLUMNS.forEach(function (col) {
html += '<label style="display:block;font-weight:normal;margin-bottom:4px;"><input type="checkbox" class="col-check" data-key="' + col.key + '"' + (HIDDEN.indexOf(col.key) === -1 ? ' checked' : '') + '> ' + (col.label || col.key) + '</label>';
});
jQuery('#columnChecklist').html(html || '<em class="text-muted">暂无列</em>');
jQuery('#columnSettingsModal').show();
}
jQuery('#btnColumnSettings').on('click', openColumnSettings);
jQuery('#btnColumnSettingsClose').on('click', function () { jQuery('#columnSettingsModal').hide(); });
jQuery('#btnHideEmpty').on('click', function () {
jQuery('#columnChecklist .col-check').each(function () {
var key = jQuery(this).data('key');
if (FILLED.indexOf(key) === -1) { jQuery(this).prop('checked', false); }
});
});
jQuery('#btnShowAll').on('click', function () {
jQuery('#columnChecklist .col-check').prop('checked', true);
});
jQuery('#btnColumnSettingsSave').on('click', function () {
var hiddenKeys = [];
jQuery('#columnChecklist .col-check').each(function () {
if (!jQuery(this).prop('checked')) { hiddenKeys.push(jQuery(this).data('key')); }
});
var idx = hiddenKeys.map(function (k) { return ALL_KEYS.indexOf(k); }).filter(function (i) { return i >= 0; });
hot.updateSettings({ hiddenColumns: { columns: idx, indicators: true } });
api('POST', BASE + '/bom/api/' + BOM_ID + '/column-visibility', { keys: hiddenKeys }, function () {
jQuery('#columnSettingsModal').hide();
HIDDEN = hiddenKeys;
});
});
// ---- 增加列 ----
jQuery('#btnAddColumn').on('click', function () {
var key = prompt("{{ __('label.column_key', '列标识(英文或中文,用于存储)') }}", "");
if (!key) { return; }
var label = prompt("{{ __('label.column_label', '列显示名') }}", key);
if (label === null) { return; }
api('POST', BASE + '/bom/api/' + BOM_ID + '/column', { key: key, label: label }, function () { location.reload(); });
});
// ---- Teable 源管理(弹窗) ----
jQuery('#btnSourceManager').on('click', function () { jQuery('#sourceManagerModal').show(); });
jQuery('#btnSourceManagerClose').on('click', function () { jQuery('#sourceManagerModal').hide(); });
jQuery('#btnAddSource').on('click', function () {
api('POST', BASE + '/bom/api/' + BOM_ID + '/source', {
name: jQuery('#srcName').val(),
baseUrl: jQuery('#srcBaseUrl').val(),
tableId: jQuery('#srcTableId').val(),
token: jQuery('#srcToken').val()
}, function () { location.reload(); });
});
jQuery('#sourceList').on('click', '.btnDelSource', function () {
if (!confirm("{{ __('text.confirm_delete') }}")) { return; }
api('DELETE', BASE + '/bom/api/source/' + jQuery(this).data('id'), null, function () { location.reload(); });
});
// ---- Teable 从数据源导入 ----
jQuery('#btnTeableImport').on('click', function () {
if (!confirm("{{ __('text.teable_import_confirm', '从 Teable 拉取最新数据并导入为明细?') }}")) { return; }
api('POST', BASE + '/bom/api/' + BOM_ID + '/import/teable', {}, function (res) {
showAlert("{{ __('text.imported', '已导入') }} " + (res.imported || 0) + " {{ __('label.rows', '行') }}", 'success');
setTimeout(function () { location.reload(); }, 800);
});
});
// ---- Teable API 粘贴导入 ----
jQuery('#btnTeablePaste').on('click', function () {
jQuery('#teablePasteText').val('');
jQuery('#pasteName').val(''); jQuery('#pasteBaseUrl').val(''); jQuery('#pasteTableId').val(''); jQuery('#pasteToken').val('');
jQuery('#teableParseResult').hide().empty();
jQuery('#teablePasteModal').show();
jQuery('#teablePasteText').trigger('focus');
});
jQuery('#btnTeablePasteClose').on('click', function () { jQuery('#teablePasteModal').hide(); });
jQuery('#btnTeableParse').on('click', function () {
var text = jQuery('#teablePasteText').val();
jQuery('#btnTeableParse').prop('disabled', true);
api('POST', BASE + '/bom/api/teable/parse', { text: text }, function (res) {
var d = res.data || {};
if (d.baseUrl) { jQuery('#pasteBaseUrl').val(d.baseUrl); }
if (d.tableId) { jQuery('#pasteTableId').val(d.tableId); }
if (d.token) { jQuery('#pasteToken').val(d.token); }
if (d.name) { jQuery('#pasteName').val(d.name); }
jQuery('#teableParseResult').html('<strong>解析完成</strong>,已回填到上方输入框,可手动修改后导入。').show();
}).always(function () { jQuery('#btnTeableParse').prop('disabled', false); });
});
jQuery('#btnTeablePasteGo').on('click', function () {
var baseUrl = jQuery('#pasteBaseUrl').val().trim();
var tableId = jQuery('#pasteTableId').val().trim();
var token = jQuery('#pasteToken').val().trim();
var name = jQuery('#pasteName').val().trim();
if (!baseUrl || !tableId || !token) { showAlert('请先点「解析」,或手动填写 Base URL / Table ID / Token', 'warning'); return; }
var text = jQuery('#teablePasteText').val() ||
('# Table: ' + (name || tableId) + '\n' +
'curl "' + baseUrl + '/api/table/' + tableId + '/record?fieldKeyType=name" \\\n' +
' -H "Authorization: Bearer ' + token + '"');
jQuery('#btnTeablePasteGo').prop('disabled', true);
api('POST', BASE + '/bom/api/' + BOM_ID + '/import/teable-paste', { text: text }, function (res) {
jQuery('#teablePasteModal').hide();
showAlert('已导入 ' + (res.imported || 0) + ' 行', 'success');
setTimeout(function () { location.reload(); }, 800);
}).always(function () { jQuery('#btnTeablePasteGo').prop('disabled', false); });
});
// ---- Excel ----
jQuery('#btnExcelTemplate').on('click', function () { window.location.href = BASE + '/bom/api/' + BOM_ID + '/export-template'; });
jQuery('#btnExcelImport').on('click', function () { jQuery('#excelFile').click(); });
jQuery('#excelFile').on('change', function () {
var file = this.files[0];
if (!file) { return; }
var fd = new FormData();
fd.append('file', file);
jQuery.ajax({
url: BASE + '/bom/api/' + BOM_ID + '/import/excel',
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrf() },
data: fd, processData: false, contentType: false, dataType: 'json'
}).done(function (res) {
showAlert("{{ __('text.imported', '已导入') }} " + (res.imported || 0) + " {{ __('label.rows', '行') }}", 'success');
setTimeout(function () { location.reload(); }, 800);
}).fail(function (xhr) {
var m = xhr.responseJSON && xhr.responseJSON.message ? xhr.responseJSON.message : ('HTTP ' + xhr.status);
showAlert(m, 'danger');
});
jQuery(this).val('');
});
jQuery('#btnExcelExport').on('click', function () { window.location.href = BASE + '/bom/api/' + BOM_ID + '/export?format=xlsx'; });
jQuery('#btnExcelExportCsv').on('click', function () { window.location.href = BASE + '/bom/api/' + BOM_ID + '/export?format=csv'; });
});
</script>
@endpush
@endonce
@endsection