OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
648
app/Domain/Bom/Templates/detail.blade.php.bak-pre-designer
Normal file
648
app/Domain/Bom/Templates/detail.blade.php.bak-pre-designer
Normal file
@@ -0,0 +1,648 @@
|
||||
@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 curl -X GET "https://table.universal-onebot.com/api/table/tblXXX/record?fieldKeyType=name" \ -H "Authorization: Bearer teable_...""></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;">×</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
|
||||
Reference in New Issue
Block a user