material-delivery-plan.html 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>物料交货计划 - SCOR供应链模型</title>
  7. <link rel="stylesheet" href="styles.css">
  8. <!-- 引入Vue 3 -->
  9. <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
  10. <!-- 引入SheetJS用于Excel解析 -->
  11. <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
  12. </head>
  13. <body>
  14. <div id="app">
  15. <div class="container">
  16. <h1>物料交货计划管理系统</h1>
  17. <!-- 操作区域 -->
  18. <div class="upload-section">
  19. <label for="file-upload" class="upload-btn">
  20. 上传Excel数据
  21. <input
  22. type="file"
  23. id="file-upload"
  24. accept=".xlsx, .xls"
  25. style="display: none;"
  26. @change="handleFileUpload">
  27. </label>
  28. <span id="file-name" class="file-name">{{ fileName || '未选择文件' }}</span>
  29. <button
  30. id="refresh-btn"
  31. class="analyze-btn"
  32. @click="refreshFromERP"
  33. >
  34. 从ERP刷新数据
  35. </button>
  36. <button
  37. id="save-btn"
  38. class="analyze-btn"
  39. :disabled="!hasUnsavedChanges"
  40. @click="saveChanges"
  41. >
  42. 保存修改
  43. </button>
  44. </div>
  45. <!-- 过滤和搜索区域 -->
  46. <div class="filter-section">
  47. <div class="filter-controls">
  48. <select v-model="filterFactoryCode" class="filter-input">
  49. <option value="">所有工厂</option>
  50. <option v-for="factory in factories" :key="factory" :value="factory">{{ factory }}</option>
  51. </select>
  52. <select v-model="filterSupplier" class="filter-input">
  53. <option value="">所有供应商</option>
  54. <option v-for="supplier in suppliers" :key="supplier" :value="supplier">{{ supplier }}</option>
  55. </select>
  56. <input
  57. type="text"
  58. v-model="searchKeyword"
  59. class="filter-input"
  60. placeholder="搜索物料编码/描述/订单号/图号/供应商"
  61. >
  62. </div>
  63. </div>
  64. <!-- 交货计划列表 -->
  65. <div class="delivery-plan-section">
  66. <h2>物料交货计划列表</h2>
  67. <div v-if="isLoading" class="loading-indicator">
  68. <p>数据加载中...</p>
  69. </div>
  70. <div v-else-if="deliveryPlanData.length === 0" class="placeholder">
  71. <p>暂无数据,请上传Excel文件或从ERP刷新数据</p>
  72. </div>
  73. <div v-else class="delivery-plan-table-container">
  74. <table class="data-table">
  75. <thead>
  76. <tr>
  77. <th>工厂代码</th>
  78. <th>物料编码</th>
  79. <th>物料描述</th>
  80. <th>图号</th>
  81. <th>版本</th>
  82. <th>采购订单号</th>
  83. <th>订单行</th>
  84. <th>供应商代码</th>
  85. <th>采购员</th>
  86. <th>供应类型</th>
  87. <th>订单数量</th>
  88. <th>入库数量</th>
  89. <th>在途数量</th>
  90. <th>待交数量</th>
  91. <th>交货数量</th>
  92. <th>交货日期</th>
  93. <th>供应商回复数量</th>
  94. <th>供应商回复交期</th>
  95. <th>备注</th>
  96. <th>操作</th>
  97. </tr>
  98. </thead>
  99. <tbody>
  100. <tr v-for="(row, index) in filteredData" :key="index" :class="{ 'modified-row': isRowModified(row) }">
  101. <td>{{ row.工厂代码 || '-' }}</td>
  102. <td>{{ row.物料编码 || '-' }}</td>
  103. <td>{{ row.物料描述 || '-' }}</td>
  104. <td>{{ row.图号 || '-' }}</td>
  105. <td>{{ row.版本 || '-' }}</td>
  106. <td>{{ row.采购订单号 || '-' }}</td>
  107. <td>{{ row.订单行 || '-' }}</td>
  108. <td>{{ row.供应商代码 || '-' }}</td>
  109. <td>{{ row.采购员 || '-' }}</td>
  110. <td>{{ row.供应类型 || '-' }}</td>
  111. <td>{{ formatNumber(row.订单数量) }}</td>
  112. <td>{{ formatNumber(row.入库数量) }}</td>
  113. <td>{{ formatNumber(row.在途数量) }}</td>
  114. <td>{{ formatNumber(row.待交数量) }}</td>
  115. <td>{{ formatNumber(row.交货数量) }}</td>
  116. <td>{{ formatDate(row.交货日期) }}</td>
  117. <td>
  118. <input
  119. type="number"
  120. :value="row.供应商回复数量 || ''"
  121. @input="updateSupplierReplyQuantity(row, $event)"
  122. class="editable-input supplier-reply-quantity"
  123. min="0"
  124. step="1"
  125. >
  126. </td>
  127. <td>
  128. <input
  129. type="date"
  130. v-model="row.供应商回复交期"
  131. class="editable-input"
  132. @change="markAsModified(row)"
  133. >
  134. </td>
  135. <td>
  136. <input
  137. type="text"
  138. v-model="row.备注"
  139. class="editable-input"
  140. @change="markAsModified(row)"
  141. >
  142. </td>
  143. <td>
  144. <button class="action-btn save-btn" @click="saveRow(row)">保存</button>
  145. <button class="action-btn cancel-btn" @click="cancelEdit(row)">取消</button>
  146. </td>
  147. </tr>
  148. </tbody>
  149. </table>
  150. <div class="table-footer">
  151. <p>共 {{ filteredData.length }} 条记录 / 总计 {{ deliveryPlanData.length }} 条</p>
  152. </div>
  153. </div>
  154. </div>
  155. <!-- 统计信息区域 -->
  156. <div class="stats-section" v-if="deliveryPlanData.length > 0">
  157. <h2>交货计划统计</h2>
  158. <div class="stats-cards">
  159. <div class="stat-card">
  160. <div class="stat-value">{{ formatNumber(totalOrderQuantity) }}</div>
  161. <div class="stat-label">总订单数量</div>
  162. </div>
  163. <div class="stat-card">
  164. <div class="stat-value">{{ formatNumber(totalDeliveredQuantity) }}</div>
  165. <div class="stat-label">已交货数量</div>
  166. </div>
  167. <div class="stat-card">
  168. <div class="stat-value">{{ formatNumber(totalPendingQuantity) }}</div>
  169. <div class="stat-label">待交货数量</div>
  170. </div>
  171. <div class="stat-card">
  172. <div class="stat-value">{{ formatNumber(totalInTransitQuantity) }}</div>
  173. <div class="stat-label">在途数量</div>
  174. </div>
  175. <div class="stat-card">
  176. <div class="stat-value">{{ onTimeRate }}%</div>
  177. <div class="stat-label">按时交货率</div>
  178. <div class="stat-formula">公式: 按时交货记录数/总记录数×100%</div>
  179. </div>
  180. </div>
  181. </div>
  182. </div>
  183. </div>
  184. <script>
  185. const { createApp, ref, reactive, computed } = Vue;
  186. createApp({
  187. setup() {
  188. // 响应式数据
  189. const fileName = ref('');
  190. const selectedFile = ref(null);
  191. const deliveryPlanData = ref([]);
  192. const originalData = ref([]);
  193. const isLoading = ref(false);
  194. const hasUnsavedChanges = ref(false);
  195. const modifiedRows = ref(new Set());
  196. // 过滤和搜索
  197. const filterFactoryCode = ref('');
  198. const filterSupplier = ref('');
  199. const searchKeyword = ref('');
  200. // 计算属性 - 工厂列表
  201. const factories = computed(() => {
  202. const uniqueFactories = new Set(deliveryPlanData.value.map(row => row.工厂代码).filter(Boolean));
  203. return Array.from(uniqueFactories).sort();
  204. });
  205. // 计算属性 - 供应商列表
  206. const suppliers = computed(() => {
  207. const uniqueSuppliers = new Set(deliveryPlanData.value.map(row => row.供应商代码).filter(Boolean));
  208. return Array.from(uniqueSuppliers).sort();
  209. });
  210. // 计算属性 - 过滤后的数据
  211. const filteredData = computed(() => {
  212. return deliveryPlanData.value.filter(row => {
  213. // 工厂代码过滤
  214. if (filterFactoryCode.value && row.工厂代码 !== filterFactoryCode.value) {
  215. return false;
  216. }
  217. // 供应商过滤
  218. if (filterSupplier.value && row.供应商代码 !== filterSupplier.value) {
  219. return false;
  220. }
  221. // 搜索关键词
  222. if (searchKeyword.value) {
  223. const keyword = searchKeyword.value.toLowerCase();
  224. return (
  225. (row.物料编码 && row.物料编码.toLowerCase().includes(keyword)) ||
  226. (row.物料描述 && row.物料描述.toLowerCase().includes(keyword)) ||
  227. (row.采购订单号 && row.采购订单号.toLowerCase().includes(keyword)) ||
  228. (row.图号 && row.图号.toLowerCase().includes(keyword)) ||
  229. (row.供应商代码 && row.供应商代码.toLowerCase().includes(keyword))
  230. );
  231. }
  232. return true;
  233. });
  234. });
  235. // 计算属性 - 统计数据
  236. const totalOrderQuantity = computed(() => {
  237. return deliveryPlanData.value.reduce((sum, row) => sum + (parseFloat(row.订单数量) || 0), 0);
  238. });
  239. const totalDeliveredQuantity = computed(() => {
  240. return deliveryPlanData.value.reduce((sum, row) => sum + (parseFloat(row.入库数量) || 0), 0);
  241. });
  242. const totalPendingQuantity = computed(() => {
  243. return deliveryPlanData.value.reduce((sum, row) => sum + (parseFloat(row.待交数量) || 0), 0);
  244. });
  245. // 计算属性 - 在途数量总计
  246. const totalInTransitQuantity = computed(() => {
  247. return deliveryPlanData.value.reduce((sum, row) => sum + (parseFloat(row.在途数量) || 0), 0);
  248. });
  249. const onTimeRate = computed(() => {
  250. const totalLines = deliveryPlanData.value.length;
  251. if (totalLines === 0) return 0;
  252. const onTimeLines = deliveryPlanData.value.filter(row => {
  253. if (!row.供应商回复交期 || !row.交货日期) return false;
  254. return new Date(row.供应商回复交期) <= new Date(row.交货日期);
  255. }).length;
  256. return ((onTimeLines / totalLines) * 100).toFixed(2);
  257. });
  258. // 处理文件上传
  259. const handleFileUpload = (event) => {
  260. const file = event.target.files[0];
  261. if (file) {
  262. fileName.value = file.name;
  263. selectedFile.value = file;
  264. parseExcelFile(file);
  265. }
  266. };
  267. // 解析Excel文件
  268. const parseExcelFile = (file) => {
  269. isLoading.value = true;
  270. const reader = new FileReader();
  271. // 添加文件读取进度反馈
  272. reader.onprogress = function(e) {
  273. if (e.lengthComputable) {
  274. const percentComplete = Math.round((e.loaded / e.total) * 100);
  275. console.log(`文件加载进度: ${percentComplete}%`);
  276. }
  277. };
  278. // 处理文件读取错误
  279. reader.onerror = function() {
  280. console.error('文件读取失败');
  281. alert('无法读取文件,请确保文件格式正确且没有被占用。');
  282. isLoading.value = false;
  283. };
  284. reader.onload = function(e) {
  285. try {
  286. const data = new Uint8Array(e.target.result);
  287. // 增强兼容性配置
  288. const workbook = XLSX.read(data, {
  289. type: 'array',
  290. cellDates: true, // 自动识别日期
  291. cellText: false, // 保持数值类型
  292. cellNF: false, // 禁用数字格式
  293. raw: true // 获取原始值
  294. });
  295. // 获取第一个工作表
  296. if (workbook.SheetNames.length === 0) {
  297. throw new Error('Excel文件中没有找到工作表');
  298. }
  299. const firstSheetName = workbook.SheetNames[0];
  300. const worksheet = workbook.Sheets[firstSheetName];
  301. // 转换为JSON格式,增加头部行处理选项
  302. const jsonData = XLSX.utils.sheet_to_json(worksheet, {
  303. header: 1, // 先获取原始行数据,处理可能的表头问题
  304. raw: false // 转换为适当的JavaScript类型
  305. });
  306. // 处理表头和数据
  307. if (jsonData.length === 0) {
  308. throw new Error('Excel文件中没有数据');
  309. }
  310. // 获取表头行
  311. const headerRow = jsonData[0];
  312. // 准备转换为对象数组
  313. const processedData = [];
  314. // 从第二行开始处理数据
  315. for (let i = 1; i < jsonData.length; i++) {
  316. const row = jsonData[i];
  317. const rowObj = {};
  318. // 遍历表头,将数据映射到对应的字段
  319. headerRow.forEach((header, index) => {
  320. if (header && header.trim() !== '') {
  321. // 转换列索引为字母(如0->A, 1->B, ..., 12->M)
  322. const colIndex = String.fromCharCode(65 + index);
  323. // 同时保存原始列名和字母列名,增强兼容性
  324. rowObj[header] = row[index];
  325. rowObj[colIndex] = row[index];
  326. }
  327. });
  328. if (Object.keys(rowObj).length > 0) {
  329. processedData.push(rowObj);
  330. }
  331. }
  332. // 处理数据,确保所有必需字段存在
  333. deliveryPlanData.value = processedData.map(row => {
  334. // 创建行的深拷贝以支持修改检测
  335. const newRow = JSON.parse(JSON.stringify(row));
  336. // 重命名和转换字段,增加更多别名支持
  337. const aliases = {
  338. '物料号': '物料编码',
  339. '物料编码': '物料编码',
  340. 'Material': '物料编码',
  341. 'Material Code': '物料编码',
  342. '订单行号': '订单行',
  343. '行号': '订单行',
  344. 'Line': '订单行',
  345. '已交数量': '入库数量',
  346. '已交货数量': '入库数量',
  347. 'Delivered Qty': '入库数量',
  348. '供应商回复数量': '供应商回复数量',
  349. 'Reply Qty': '供应商回复数量',
  350. '交货日期': '交货日期',
  351. 'Delivery Date': '交货日期',
  352. '供应商回复交期': '供应商回复交期',
  353. 'Reply Date': '供应商回复交期'
  354. };
  355. // 应用别名映射
  356. Object.keys(aliases).forEach(alias => {
  357. if (row[alias] !== undefined && newRow[aliases[alias]] === undefined) {
  358. newRow[aliases[alias]] = row[alias];
  359. }
  360. });
  361. // 确保在途数量从Excel的M列获取,增加更多可能的列名
  362. if (row['M'] !== undefined) newRow.在途数量 = row['M'];
  363. if (row['在途数量'] !== undefined) newRow.在途数量 = row['在途数量'];
  364. if (row['In Transit'] !== undefined) newRow.在途数量 = row['In Transit'];
  365. // 计算待交数量 = 订单数量 - 入库数量 - 在途数量
  366. const orderQty = parseFloat(newRow.订单数量 || 0);
  367. const deliveredQty = parseFloat(newRow.入库数量 || 0);
  368. const inTransitQty = parseFloat(newRow.在途数量 || 0);
  369. newRow.待交数量 = orderQty - deliveredQty - inTransitQty;
  370. // 确保数字字段是数字类型
  371. ['订单数量', '入库数量', '在途数量', '待交数量', '交货数量', '供应商回复数量'].forEach(field => {
  372. if (newRow[field] !== undefined && newRow[field] !== null && newRow[field] !== '') {
  373. newRow[field] = parseFloat(newRow[field]);
  374. }
  375. });
  376. // 处理日期格式,确保与Excel数据一致
  377. if (newRow.交货日期) {
  378. newRow.交货日期 = formatDateForStorage(newRow.交货日期);
  379. }
  380. if (newRow.供应商回复交期) {
  381. newRow.供应商回复交期 = formatDateForStorage(newRow.供应商回复交期);
  382. }
  383. return newRow;
  384. });
  385. // 保存原始数据用于比较和取消操作
  386. originalData.value = JSON.parse(JSON.stringify(deliveryPlanData.value));
  387. // 重置状态
  388. hasUnsavedChanges.value = false;
  389. modifiedRows.value.clear();
  390. // 显示成功消息
  391. if (deliveryPlanData.value.length > 0) {
  392. console.log(`成功解析Excel文件,共${deliveryPlanData.value.length}条数据`);
  393. } else {
  394. console.warn('Excel文件已解析,但没有找到有效数据行');
  395. alert('Excel文件已解析,但没有找到有效数据行,请检查文件格式。');
  396. }
  397. } catch (error) {
  398. console.error('Excel解析错误:', error);
  399. alert(`解析Excel文件时发生错误: ${error.message}\n\n请确保文件格式正确,并包含必需的数据字段。`);
  400. deliveryPlanData.value = [];
  401. } finally {
  402. isLoading.value = false;
  403. }
  404. };
  405. try {
  406. reader.readAsArrayBuffer(file);
  407. } catch (error) {
  408. console.error('文件读取启动失败:', error);
  409. alert('无法启动文件读取,请稍后重试。');
  410. isLoading.value = false;
  411. }
  412. };
  413. // 从ERP刷新数据(模拟)
  414. const refreshFromERP = () => {
  415. isLoading.value = true;
  416. // 模拟API请求延迟
  417. setTimeout(() => {
  418. // 这里应该是实际的API调用,现在使用模拟数据
  419. const mockERPData = generateMockERPData();
  420. deliveryPlanData.value = mockERPData;
  421. originalData.value = JSON.parse(JSON.stringify(mockERPData));
  422. hasUnsavedChanges.value = false;
  423. modifiedRows.value.clear();
  424. fileName.value = '';
  425. selectedFile.value = null;
  426. isLoading.value = false;
  427. }, 1000);
  428. };
  429. // 生成模拟ERP数据
  430. const generateMockERPData = () => {
  431. const factories = ['F001', 'F002', 'F003'];
  432. const materials = [
  433. { id: 'M001', desc: '电子元件A', drawing: 'DWG001', version: 'V1.0' },
  434. { id: 'M002', desc: '机械零件B', drawing: 'DWG002', version: 'V2.1' },
  435. { id: 'M003', desc: '包装材料C', drawing: 'DWG003', version: 'V1.5' },
  436. { id: 'M004', desc: '原材料D', drawing: 'DWG004', version: 'V3.0' },
  437. { id: 'M005', desc: '配件E', drawing: 'DWG005', version: 'V2.2' }
  438. ];
  439. const buyers = ['张三', '李四', '王五', '赵六'];
  440. const supplyTypes = ['常规采购', '紧急采购', '寄售', 'VMI'];
  441. const mockData = [];
  442. for (let i = 1; i <= 20; i++) {
  443. const factory = factories[Math.floor(Math.random() * factories.length)];
  444. const material = materials[Math.floor(Math.random() * materials.length)];
  445. const orderQuantity = Math.floor(Math.random() * 1000) + 100;
  446. const deliveredQuantity = Math.floor(Math.random() * orderQuantity);
  447. const inTransitQuantity = Math.floor(Math.random() * (orderQuantity - deliveredQuantity));
  448. const pendingQuantity = orderQuantity - deliveredQuantity - inTransitQuantity; // 待交数量=订单数量-入库数量-在途数量
  449. const deliveryQuantity = Math.floor(Math.random() * pendingQuantity) + 1;
  450. const buyer = buyers[Math.floor(Math.random() * buyers.length)];
  451. const supplyType = supplyTypes[Math.floor(Math.random() * supplyTypes.length)];
  452. // 生成交货日期(未来30天内)
  453. const deliveryDate = new Date();
  454. deliveryDate.setDate(deliveryDate.getDate() + Math.floor(Math.random() * 30) + 1);
  455. mockData.push({
  456. 工厂代码: factory,
  457. 物料编码: material.id,
  458. 物料描述: material.desc,
  459. 图号: material.drawing,
  460. 版本: material.version,
  461. 采购订单号: `PO${factory}-${String(i).padStart(5, '0')}`,
  462. 订单行: Math.floor(Math.random() * 10) + 1,
  463. 供应商代码: `S${String(Math.floor(Math.random() * 100) + 1).padStart(3, '0')}`,
  464. 采购员: buyer,
  465. 供应类型: supplyType,
  466. 订单数量: orderQuantity,
  467. 入库数量: deliveredQuantity,
  468. 在途数量: inTransitQuantity,
  469. 待交数量: pendingQuantity,
  470. 交货数量: deliveryQuantity,
  471. 交货日期: deliveryDate.toISOString().split('T')[0],
  472. 供应商回复数量: null,
  473. 供应商回复交期: null,
  474. 备注: '',
  475. id: `ROW-${i}`
  476. });
  477. }
  478. return mockData;
  479. };
  480. // 更新供应商回复数量
  481. const updateSupplierReplyQuantity = (row, event) => {
  482. const value = event.target.value;
  483. // 允许空字符串或数字
  484. if (value === '' || (value !== '' && !isNaN(parseFloat(value)))) {
  485. row.供应商回复数量 = value === '' ? null : parseFloat(value);
  486. markAsModified(row);
  487. } else {
  488. // 如果输入的不是有效数字,清空输入
  489. event.target.value = '';
  490. }
  491. };
  492. // 标记行为已修改
  493. const markAsModified = (row) => {
  494. modifiedRows.value.add(row.id || row.采购订单号 + '-' + row.订单行号);
  495. hasUnsavedChanges.value = true;
  496. };
  497. // 检查行是否已修改
  498. const isRowModified = (row) => {
  499. return modifiedRows.value.has(row.id || row.采购订单号 + '-' + row.订单行号);
  500. };
  501. // 保存单行修改
  502. const saveRow = (row) => {
  503. const rowKey = row.id || row.采购订单号 + '-' + row.订单行号;
  504. // 这里应该是实际的API调用,现在仅更新状态
  505. console.log('保存行修改:', row);
  506. // 在实际应用中,这里会调用API保存数据
  507. // 保存成功后更新原始数据
  508. const originalRowIndex = originalData.value.findIndex(
  509. r => (r.id || r.采购订单号 + '-' + r.订单行号) === rowKey
  510. );
  511. if (originalRowIndex !== -1) {
  512. originalData.value[originalRowIndex] = JSON.parse(JSON.stringify(row));
  513. }
  514. modifiedRows.value.delete(rowKey);
  515. hasUnsavedChanges.value = modifiedRows.value.size > 0;
  516. };
  517. // 取消编辑
  518. const cancelEdit = (row) => {
  519. const rowKey = row.id || row.采购订单号 + '-' + row.订单行号;
  520. // 查找原始数据
  521. const originalRow = originalData.value.find(
  522. r => (r.id || r.采购订单号 + '-' + r.订单行号) === rowKey
  523. );
  524. if (originalRow) {
  525. // 恢复原始数据
  526. Object.keys(originalRow).forEach(key => {
  527. row[key] = originalRow[key];
  528. });
  529. }
  530. modifiedRows.value.delete(rowKey);
  531. hasUnsavedChanges.value = modifiedRows.value.size > 0;
  532. };
  533. // 保存所有修改
  534. const saveChanges = () => {
  535. // 获取所有修改过的行
  536. const modifiedData = deliveryPlanData.value.filter(row =>
  537. isRowModified(row)
  538. );
  539. if (modifiedData.length === 0) {
  540. alert('没有需要保存的修改');
  541. return;
  542. }
  543. // 这里应该是实际的API调用,现在仅显示信息
  544. console.log('保存所有修改:', modifiedData);
  545. // 保存成功后更新原始数据
  546. modifiedData.forEach(row => {
  547. const rowKey = row.id || row.采购订单号 + '-' + row.订单行号;
  548. const originalRowIndex = originalData.value.findIndex(
  549. r => (r.id || r.采购订单号 + '-' + r.订单行号) === rowKey
  550. );
  551. if (originalRowIndex !== -1) {
  552. originalData.value[originalRowIndex] = JSON.parse(JSON.stringify(row));
  553. }
  554. modifiedRows.value.delete(rowKey);
  555. });
  556. hasUnsavedChanges.value = false;
  557. alert('修改已成功保存');
  558. };
  559. // 格式化数字
  560. const formatNumber = (num) => {
  561. if (num === null || num === undefined || isNaN(num)) return '-';
  562. return parseFloat(num).toLocaleString('zh-CN');
  563. };
  564. // 格式化日期显示
  565. const formatDate = (dateString) => {
  566. if (!dateString) return '-';
  567. try {
  568. let date;
  569. // 检查是否是Excel日期数值(从1900年1月1日开始的天数)
  570. if (!isNaN(dateString) && typeof dateString === 'number') {
  571. // Excel从1900年1月1日开始,但JavaScript从1970年1月1日开始
  572. // 计算从1900-01-01到1970-01-01的毫秒数,加上Excel天数的毫秒数
  573. const excelEpoch = new Date(1900, 0, 1).getTime();
  574. const msPerDay = 24 * 60 * 60 * 1000;
  575. // 注意:Excel错误地认为1900年是闰年,所以需要减去1天
  576. const correctedDays = dateString < 60 ? dateString - 1 : dateString - 2;
  577. date = new Date(excelEpoch + correctedDays * msPerDay);
  578. } else if (typeof dateString === 'string') {
  579. // 处理YYYY-MM-DD格式
  580. if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
  581. return dateString;
  582. }
  583. // 处理其他字符串格式
  584. date = new Date(dateString);
  585. } else {
  586. // 尝试直接使用
  587. date = new Date(dateString);
  588. }
  589. // 检查日期是否有效
  590. if (isNaN(date.getTime())) return dateString; // 如果无法解析,返回原始值
  591. // 格式化日期为YYYY-MM-DD格式
  592. const year = date.getFullYear();
  593. const month = String(date.getMonth() + 1).padStart(2, '0');
  594. const day = String(date.getDate()).padStart(2, '0');
  595. return `${year}-${month}-${day}`;
  596. } catch (e) {
  597. console.error('日期格式化错误:', e);
  598. return dateString; // 如果出错,返回原始值
  599. }
  600. };
  601. // 格式化日期为存储格式(确保与Excel数据一致)
  602. const formatDateForStorage = (dateString) => {
  603. if (!dateString) return null;
  604. try {
  605. // 尝试直接解析日期字符串
  606. let date;
  607. // 如果已经是YYYY-MM-DD格式,直接返回
  608. if (typeof dateString === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
  609. return dateString;
  610. }
  611. // 检查是否是Excel日期数值(从1900年1月1日开始的天数)
  612. if (!isNaN(dateString) && typeof dateString === 'number') {
  613. // Excel从1900年1月1日开始,但JavaScript从1970年1月1日开始
  614. // 计算从1900-01-01到1970-01-01的毫秒数,加上Excel天数的毫秒数
  615. const excelEpoch = new Date(1900, 0, 1).getTime();
  616. const msPerDay = 24 * 60 * 60 * 1000;
  617. // 注意:Excel错误地认为1900年是闰年,所以需要减去1天
  618. const correctedDays = dateString < 60 ? dateString - 1 : dateString - 2;
  619. date = new Date(excelEpoch + correctedDays * msPerDay);
  620. } else if (typeof dateString === 'string') {
  621. // 处理ISO格式日期
  622. if (dateString.includes('T')) {
  623. date = new Date(dateString);
  624. } else {
  625. // 处理其他字符串格式
  626. date = new Date(dateString);
  627. }
  628. } else {
  629. // 尝试直接使用
  630. date = new Date(dateString);
  631. }
  632. // 检查日期是否有效
  633. if (isNaN(date.getTime())) return dateString; // 如果无法解析,返回原始值
  634. // 格式化日期为YYYY-MM-DD格式
  635. const year = date.getFullYear();
  636. const month = String(date.getMonth() + 1).padStart(2, '0');
  637. const day = String(date.getDate()).padStart(2, '0');
  638. return `${year}-${month}-${day}`;
  639. } catch (e) {
  640. console.error('日期存储格式化错误:', e);
  641. return dateString; // 如果出错,返回原始值
  642. }
  643. };
  644. return {
  645. fileName,
  646. selectedFile,
  647. deliveryPlanData,
  648. isLoading,
  649. hasUnsavedChanges,
  650. filterFactoryCode,
  651. filterSupplier,
  652. searchKeyword,
  653. factories,
  654. suppliers,
  655. filteredData,
  656. totalOrderQuantity,
  657. totalDeliveredQuantity,
  658. totalPendingQuantity,
  659. totalInTransitQuantity,
  660. onTimeRate,
  661. handleFileUpload,
  662. refreshFromERP,
  663. saveChanges,
  664. markAsModified,
  665. isRowModified,
  666. saveRow,
  667. cancelEdit,
  668. formatNumber,
  669. formatDate,
  670. updateSupplierReplyQuantity,
  671. formatDateForStorage
  672. };
  673. }
  674. }).mount('#app');
  675. </script>
  676. </body>
  677. </html>