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

649 lines
32 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/spreadjs/gc.spread.sheets.excel2013white.12.0.0.css">
<div class="pageheader" style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px;">
<div style="display:flex; align-items:center; gap:10px;">
<div class="pageicon"><span class="fa fa-fw fa-list-check"></span></div>
<div class="pagetitle" style="margin:0;">
<h1 style="margin:0;">{{ $bom['productName'] ?? $bom['bomNo'] ?? __('menu.bom') }}</h1>
<p style="margin:0;">{{ __('text.bom_subtitle', 'BOM 明细') }} — {{ $bom['bomNo'] ?? '' }}</p>
</div>
</div>
<div style="white-space:nowrap;">
<button type="button" class="btn btn-primary" id="btnEdit"><i class="fa fa-pencil"></i> 编辑</button>
<button type="button" class="btn btn-success" id="btnSave"><i class="fa fa-save"></i> 保存</button>
{{-- 一级菜单:导入导出 --}}
<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-exchange"></i> 导入导出 <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li class="dropdown-header"><i class="fa fa-cloud-download"></i> Teable 导入</li>
<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><a href="javascript:void(0)" id="btnSourceManager"><i class="fa fa-database"></i> 数据源管理</a></li>
<li class="divider"></li>
<li class="dropdown-header"><i class="fa fa-upload"></i> Excel 导入</li>
<li><a href="javascript:void(0)" id="btnExcelImport"><i class="fa fa-upload"></i> 导入数据</a></li>
<li><a href="javascript:void(0)" id="btnExcelTemplate"><i class="fa fa-download"></i> 模板导出</a></li>
<li class="divider"></li>
<li class="dropdown-header"><i class="fa fa-file-export"></i> 数据导出</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>
<a href="{{ BASE_URL }}/bom/show" class="btn btn-default"><i class="fa fa-arrow-left"></i> {{ __('links.back', '返回') }}</a>
</div>
</div>
<div class="maincontent">
<div class="maincontentinner">
<div id="bomAlert" class="alert" style="display:none;"></div>
<div id="bomError" class="alert alert-danger" style="display:none;"></div>
{{-- SpreadJS 容器 --}}
<div id="bomHot" style="width:100%;height:600px;border:1px solid #e5e5e5;"></div>
</div>
</div>
{{-- 附件页内预览弹窗(图片 <img> / PDF <iframe> / 其它下载链接) --}}
<div id="bomAttachModal" style="display:none;position:fixed;inset:0;background:rgba(15,23,42,0.66);z-index:99999;align-items:center;justify-content:center;padding:24px;">
<div style="position:relative;width:min(1100px,96vw);height:min(820px,92vh);background:#fff;border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,0.4);display:flex;flex-direction:column;overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid #e5e7eb;background:#f8fafc;flex:none;">
<span id="bomAttachTitle" style="font-size:14px;font-weight:600;color:#0f172a;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">附件预览</span>
<a id="bomAttachClose" role="button" aria-label="Close" style="flex:none;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:6px;cursor:pointer;color:#475569;font-size:18px;line-height:1;text-decoration:none;"><i class="fa fa-times"></i></a>
</div>
<div id="bomAttachBody" style="flex:1;width:100%;overflow:hidden;background:#fff;display:flex;flex-direction:column;"></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="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/spreadjs/gc.spread.sheets.all.12.0.0.min.js"></script>
<script src="{{ BASE_URL }}/dist/spreadjs/gc.spread.excelio.12.0.0.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 DEL_COL = ALL_KEYS.length;
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 showError(msg) {
jQuery('#bomError').text(msg).show();
}
// ---- 附件页内预览弹窗 ----
var attachModal = {
$el: null,
$body: null,
$title: null,
close: null,
open: function (content, title) {
if (!attachModal.$el) {
attachModal.$el = jQuery('#bomAttachModal');
attachModal.$body = jQuery('#bomAttachBody');
attachModal.$title = jQuery('#bomAttachTitle');
}
attachModal.$body.empty().append(content);
attachModal.$title.text(title || '附件预览');
attachModal.$el.css('display', 'flex');
},
hide: function () {
if (attachModal.$el) { attachModal.$el.hide(); }
if (attachModal.$body) { attachModal.$body.empty(); }
}
};
jQuery(document).on('click', '#bomAttachClose', function () { attachModal.hide(); });
jQuery(document).on('click', '#bomAttachModal', function (e) {
if (e.target === this) { attachModal.hide(); }
});
jQuery(document).on('keydown', function (e) {
if (e.key === 'Escape' && attachModal.$el && attachModal.$el.is(':visible')) { attachModal.hide(); }
});
// 判断附件类型image -> 图片预览pdf -> iframe其它 -> 下载链接
function detectAttachKind(item) {
if (!item) { return 'file'; }
var name = (item.name || '').toLowerCase();
var url = (item.url || '').toLowerCase();
if (item.image === true) { return 'image'; }
if (/\.pdf($|\?|#)/.test(name) || /\.pdf($|\?|#)/.test(url)) { return 'pdf'; }
if (/\.(png|jpe?g|gif|webp|bmp|svg)($|\?|#)/.test(name)) { return 'image'; }
return 'file';
}
function openAttachPreview(item) {
var url = item.url || '';
var name = item.name || '';
if (!url) { return; }
var kind = detectAttachKind(item);
if (kind === 'image') {
var img = jQuery('<img>', { src: url, alt: name, style: 'max-width:100%;max-height:100%;object-fit:contain;margin:auto;padding:12px;' });
attachModal.open(img, name || '图片预览');
} else if (kind === 'pdf') {
var frame = jQuery('<iframe>', {
src: url,
title: name || 'PDF',
style: 'flex:1;width:100%;border:0;background:#fff;'
});
attachModal.open(frame, name || 'PDF 预览');
} else {
// 其它文件:弹窗内提供下载链接
var box = jQuery('<div>', { style: 'display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:16px;padding:24px;text-align:center;' });
var icon = jQuery('<div>', { style: 'font-size:48px;color:#94a3b8;' }).html('<i class="fa fa-file"></i>');
var nm = jQuery('<div>', { style: 'font-size:14px;color:#334155;word-break:break-all;max-width:80%;' }).text(name || url);
var dl = jQuery('<a>', {
href: url,
target: '_blank',
rel: 'noopener',
class: 'btn btn-primary',
style: 'display:inline-block;'
}).html('<i class="fa fa-download"></i> 下载 / 打开文件');
box.append(icon, nm, dl);
attachModal.open(box, name || '文件');
}
}
// 全局错误捕获:任何 JS 异常都在页面上显式显示,不再静默失效
window.onerror = function (msg, src, line) {
try { jQuery('#bomError').text('JS 错误: ' + msg + ' (第 ' + line + ' 行)').show(); } catch (e) {}
return false;
};
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');
});
}
// ---- 按 demo 模式license key 内联(去掉 license.js 引入) ----
try {
GC.Spread.Sheets.LicenseKey = "GrapeCity-Internal-Use-Only,*.grapecity.com|*.componentone.com,E347353251763156#B0MtLULRHcBd5ZrY5QrMXRalHW5Ejc75Wb58EV4E4LoV6aDJldEBjaZRGUXNXNhJjZ0plSyMnR8YHZI54a7oUMsFTNYVUMOhFOPx6UKtib9J7KZdzVs5GMxIlM5olVj3USyMHUVlEThtkdk5GdRB7dIhlV6MHZz3GNWdVbCdmZpt4NGtSYZV6Ywcme6kEdItydTVjUGF4Z4RmSZdUbQNkW93ybC3ySSdmd6onQ0dlYOFWanxWMwsWczdUMv2yRi3yQRtEcL9WT4N4VSRnNUlnbhF5UTFHMOV6YGJ7R42Ge5VjUuNnNoRkboNGZVJ5V58mdzYlc7g6cGRlI0IyUiwiIBJDRCZDOzEjI0ICSiwSOzcjM4ITOyEjM0IicfJye35XX3JSSGljQiojIDJCLiITMuYHITpEIkFWZyB7UiojIOJyebpjIkJHUiwiIyITOxATMgYTMwEDOxAjMiojI4J7QiwiIt36YuUmbvRnbl96bw56bj9iKs46bj9Se4l6YlBXYydmLqIiOiMXbEJCLikHdpNUZwFmcHJiOiEmTDJCLlVnc4pjIsZXRiwiI6UTMzYzNxUjMzUzM7QzMiojIklkIs4XZzxWYmpjIyNHZisnOiwmbBJye0ICRiwiI34zdI5kWS9WR79mdYJ7LRRENGNmZUp6TBN4LiJ4Sxd7Rr4UOyp7bIJzR0RFTzNHOlxmRBFGanV7SIVVWoNWbhVFS5QHcS96SvonNrEnWwklWZ9UQ9UTVQ3iSC5kYJhxe";
} catch (e) { showError('License 设置失败: ' + e.message); }
// ---- 附件解析/显示 ----
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 [];
}
// 图片缓存url -> Image异步加载后触发重绘
var attachCache = {};
// 图片列 cellType画缩略图加载失败/未就绪时显示文件名
function ImageCellType() {}
try { ImageCellType.prototype = new GC.Spread.Sheets.CellTypes.Base(); } catch (e) {}
ImageCellType.prototype.paint = function (ctx, value, x, y, w, h, style, context) {
var arr = parseAttach(value);
var first = arr.length > 0 ? arr[0] : null;
if (first && first.url) {
var img = attachCache[first.url];
if (!img) {
img = new Image();
img.onload = function () { try { if (spread) { spread.repaint(); } } catch (e) {} };
img.onerror = function () {};
img.src = first.url;
attachCache[first.url] = img;
}
if (img.complete && img.naturalWidth > 0) {
try {
var scale = (h - 4) / img.naturalHeight;
var dw = img.naturalWidth * scale;
var dh = h - 4;
ctx.drawImage(img, x + 2, y + 2, dw, dh);
return;
} catch (e) {}
}
}
// 兜底:画文本(不依赖 Base.prototype.paint
ctx.save();
ctx.fillStyle = '#333';
ctx.textBaseline = 'middle';
try { ctx.font = (style && style.font) ? style.font : '12px Arial'; } catch (e) {}
ctx.fillText(first ? (first.name || '') : (value || ''), x + 2, y + h / 2);
ctx.restore();
};
// 文件列 cellType显示可点击的文件名蓝色链接样式
function LinkCellType() {}
try { LinkCellType.prototype = new GC.Spread.Sheets.CellTypes.Base(); } catch (e) {}
LinkCellType.prototype.paint = function (ctx, value, x, y, w, h, style, context) {
var arr = parseAttach(value);
var names = arr.map(function (it) { return it && it.name ? it.name : ''; }).filter(Boolean).join(', ');
ctx.save();
ctx.fillStyle = '#1a73e8';
ctx.textBaseline = 'middle';
try { ctx.font = (style && style.font) ? style.font : '12px Arial'; } catch (e) {}
ctx.fillText(names || (value || ''), x + 2, y + h / 2);
ctx.restore();
};
// ---- 数据 ----
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;
});
// ---- 初始化 SpreadJS ----
var host = document.getElementById('bomHot');
function calcHeight() {
var rect = host.getBoundingClientRect();
var h = window.innerHeight - rect.top - 20;
return h < 500 ? 500 : h;
}
// SpreadJS 需要明确的 px 宽高:宽度从父容器读取,否则 100% 未解析会导致 canvas 宽度为 0显示不全
function setHostSize() {
var pw = host.parentElement ? host.parentElement.clientWidth : 0;
if (pw > 0) { host.style.width = pw + 'px'; }
host.style.height = calcHeight() + 'px';
}
setHostSize();
var spread;
try {
spread = new GC.Spread.Sheets.Workbook(host, { sheetCount: 1 });
} catch (e) {
showError('SpreadJS 初始化失败: ' + e.message);
return;
}
var sheet = spread.getActiveSheet();
sheet.options.colHeaderVisible = true;
// 默认只读:用 editable 标志 + EditStarting 拦截(不用 isProtected避免其解锁不生效的坑
var editable = false;
// 横向/纵向滚动条显式开启,支持左右拖动阅读(列多时横向滚动)
// 注意12.0.0 无 scrollByPixel逐条 try 避免一个失败导致后续全部失效
try { spread.options.showHorizontalScrollbar = true; } catch (e) {}
try { spread.options.showVerticalScrollbar = true; } catch (e) {}
try { spread.options.scrollbarMaxAlign = true; } catch (e) {}
try { spread.options.scrollbarShowMax = true; } catch (e) {}
sheet.setColumnCount(ALL_KEYS.length + 1);
sheet.setRowCount(Math.max(data.length, 1));
// 表头
FIXED.forEach(function (key, i) { sheet.setValue(0, i, FIXED_LABELS[key] || key, GC.Spread.Sheets.SheetArea.colHeader); });
COLUMNS.forEach(function (col, i) { sheet.setValue(0, FIXED.length + i, col.label || col.key, GC.Spread.Sheets.SheetArea.colHeader); });
sheet.setValue(0, DEL_COL, '操作', GC.Spread.Sheets.SheetArea.colHeader);
// 列宽
FIXED.forEach(function (_, i) { sheet.setColumnWidth(i, 120); });
COLUMNS.forEach(function (_, i) { sheet.setColumnWidth(FIXED.length + i, 140); });
sheet.setColumnWidth(DEL_COL, 60);
// 删除按钮 cell type用对象配置避免链式调用报错
var delBtn;
try {
delBtn = new GC.Spread.Sheets.CellTypes.Button();
delBtn.text('删');
delBtn.buttonBackColor('#d9534f');
delBtn.textColor('#ffffff');
} catch (e) {
delBtn = null;
}
// ---- 加载数据 ----
var imageCellType = new ImageCellType();
var linkCellType = new LinkCellType();
var loading = false; // setArray 会触发 CellChanged加载期间禁止保存
function loadData() {
try {
loading = true;
// 数据区保留原始 JSON附件列由 cellType 渲染图片/链接
var arr = data.map(function (rowData) {
return ALL_KEYS.map(function (k) {
var v = rowData[k];
return (v === undefined || v === null) ? '' : v;
});
});
if (arr.length > 0) {
sheet.setRowCount(arr.length);
sheet.setArray(0, 0, arr);
}
// 图片列 -> ImageCellType文件列 -> LinkCellType
data.forEach(function (_, r) {
ALL_KEYS.forEach(function (k, c) {
if (IMAGE_COLS.indexOf(k) !== -1) { sheet.getCell(r, c).cellType(imageCellType); }
else if (FILE_COLS.indexOf(k) !== -1) { sheet.getCell(r, c).cellType(linkCellType); }
});
if (delBtn) { sheet.getCell(r, DEL_COL).cellType(delBtn); }
});
} catch (e) {
showError('loadData 异常: ' + e.message);
} finally {
loading = false;
}
}
loadData();
// ---- 列显隐 ----
function applyHidden() {
ALL_KEYS.forEach(function (k, c) {
if (HIDDEN.indexOf(k) !== -1) { sheet.hideColumn(c); } else { sheet.showColumn(c); }
});
}
applyHidden();
// ---- 强制刷新resize 是官方重算布局并重绘整个工作簿的关键方法,含数据区 canvas ----
function refreshSpread() {
try { spread.resize(); } catch (e) {}
try { spread.invalidateLayout(); } catch (e) {}
try { spread.repaint(); } catch (e) {}
try { spread.refresh(); } catch (e) {}
}
// rAF 在浏览器完成布局后触发,比 setTimeout 可靠;连续多帧确保列头渲染
requestAnimationFrame(function () {
setHostSize();
refreshSpread();
requestAnimationFrame(refreshSpread);
});
setTimeout(refreshSpread, 100);
setTimeout(refreshSpread, 300);
jQuery(window).on('load', function () {
setHostSize();
refreshSpread();
});
jQuery(window).on('resize', function () {
setHostSize();
refreshSpread();
});
// 容器尺寸变化时自动重绘(最可靠:任何布局抖动都会触发)
if (typeof ResizeObserver !== 'undefined') {
var ro = new ResizeObserver(function () {
refreshSpread();
});
ro.observe(host);
}
// ---- 保存行 ----
function getRowData(row) {
var payload = {};
ALL_KEYS.forEach(function (k, c) {
var v = sheet.getValue(row, c);
payload[k] = (v === undefined || v === null) ? '' : v;
});
return payload;
}
function saveRow(row) {
if (row < 0 || row >= data.length) { return; }
var payload = getRowData(row);
if (data[row]._id) { payload.itemId = data[row]._id; }
api('POST', BASE + '/bom/api/' + BOM_ID + '/item', payload, function (res) {
if (res.itemId && !data[row]._id) { data[row]._id = res.itemId; }
});
}
// ---- 编辑 / 保存 按钮:默认只读防误操作 ----
jQuery('#btnEdit').on('click', function () {
editable = true; // 解锁
showAlert('表格已解锁,可编辑数据;完成后点「保存」提交', 'info');
});
jQuery('#btnSave').on('click', function () {
var saved = 0;
// 遍历所有行,对比 sheet 当前值与原始数据,有差异才保存
data.forEach(function (rowData, r) {
var changed = false;
ALL_KEYS.forEach(function (k, c) {
var v = sheet.getValue(r, c);
var sv = (v === undefined || v === null) ? '' : String(v);
var orig = (rowData[k] === undefined || rowData[k] === null) ? '' : String(rowData[k]);
if (sv !== orig) { changed = true; }
});
if (changed) { saveRow(r); saved++; }
});
editable = false; // 重新锁定
if (saved > 0) {
// 保存后刷新本地原始数据,避免下次重复提交
data.forEach(function (rowData, r) {
ALL_KEYS.forEach(function (k, c) {
var v = sheet.getValue(r, c);
rowData[k] = (v === undefined || v === null) ? '' : v;
});
});
showAlert('已保存 ' + saved + ' 行,表格已重新锁定(只读)', 'success');
} else {
showAlert('没有需要保存的改动,表格已重新锁定(只读)', 'info');
}
});
// 拦截编辑未解锁时禁止进入编辑态args.cancel 才是正确参数)
try {
spread.bind(GC.Spread.Sheets.Events.EditStarting, function (sender, args) {
if (!editable) {
showAlert('表格当前为只读,请先点「编辑」按钮', 'warning');
args.cancel = true;
}
});
} catch (e) {}
// 删除按钮
spread.bind(GC.Spread.Sheets.Events.ButtonClicked, function (e, args) {
if (args.col === DEL_COL && args.row >= 0) { deleteRow(args.row); }
});
// 附件列(图片/文件)点击 → 页内弹窗预览(图片 <img>、PDF <iframe>、其它下载链接)
spread.bind(GC.Spread.Sheets.Events.CellClick, function (e, info) {
if (info.sheetArea !== GC.Spread.Sheets.SheetArea.viewport) { return; }
var key = ALL_KEYS[info.col];
if (IMAGE_COLS.indexOf(key) === -1 && FILE_COLS.indexOf(key) === -1) { return; }
var val = sheet.getValue(info.row, info.col);
var arr = parseAttach(val);
if (!arr || arr.length === 0) { return; }
// 多附件:只点文件名打开第一个;如需逐一查看可后续扩展成列表
var first = arr[0];
if (first && first.url) {
openAttachPreview(first);
}
});
function deleteRow(row) {
if (!confirm("{{ __('text.confirm_delete') }}")) { return; }
if (row < 0 || row >= data.length) { return; }
var d = data[row];
if (!d._id) {
sheet.deleteRows(row, 1);
data.splice(row, 1);
return;
}
api('DELETE', BASE + '/bom/api/item/' + d._id, null, function () {
sheet.deleteRows(row, 1);
data.splice(row, 1);
});
}
// ---- 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