tcsTable.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. /**
  2. * TcsTable - 通用表格组件
  3. * 提供表格数据生成、渲染、分页和筛选功能
  4. */
  5. class TcsTable {
  6. /**
  7. * 构造函数
  8. * @param {Object} options - 表格配置选项
  9. * @param {Array} data - 表格数据
  10. */
  11. constructor(options, data) {
  12. this.options = Object.assign(
  13. {
  14. tableId: "data-table", // 表格元素ID
  15. paginationInfoId: "pagination-info", // 分页信息元素ID
  16. paginationControlsId: "pagination-controls", // 分页控件元素ID
  17. pageSizeId: "current-page-size", // 每页显示条数元素ID
  18. pageSizeItemClass: "page-size-item", // 每页显示条数选项类名
  19. defaultPageSize: 10, // 默认每页显示条数
  20. maxVisiblePages: 5, // 最大可见页码数
  21. columns: [], // 列配置
  22. },
  23. options
  24. );
  25. this.data = data || []; // 原始数据
  26. this.filteredData = [...this.data]; // 筛选后的数据
  27. this.currentPage = 1; // 当前页码
  28. this.itemsPerPage = this.options.defaultPageSize; // 每页显示条数
  29. // 注册内置的表头渲染器
  30. this.headerRenderers = {
  31. // 复选框表头渲染器
  32. checkbox: (column, th) => {
  33. th.innerHTML = '<input class="form-check-input m-0 align-middle" type="checkbox" id="select-all" aria-label="全选">';
  34. // 绑定全选事件
  35. setTimeout(() => {
  36. const selectAll = document.getElementById('select-all');
  37. if (selectAll) {
  38. selectAll.addEventListener('change', () => {
  39. const checkboxes = document.querySelectorAll('.row-checkbox');
  40. checkboxes.forEach(checkbox => {
  41. checkbox.checked = selectAll.checked;
  42. });
  43. });
  44. // 监听表格体中的复选框变化,更新全选框状态
  45. document.addEventListener('change', (e) => {
  46. if (e.target && e.target.classList.contains('row-checkbox')) {
  47. this.updateSelectAllCheckboxState();
  48. }
  49. });
  50. }
  51. }, 0);
  52. return th;
  53. },
  54. // 可以添加更多内置渲染器...
  55. };
  56. // 允许用户注册自定义渲染器
  57. if (options.headerRenderers) {
  58. Object.assign(this.headerRenderers, options.headerRenderers);
  59. }
  60. // 初始化表格
  61. this.init();
  62. }
  63. /**
  64. * 初始化表格
  65. */
  66. init() {
  67. // 初始化表头
  68. this.initTableHeader();
  69. // 渲染表格
  70. this.renderTable();
  71. // 绑定分页事件
  72. this.bindPaginationEvents();
  73. // 绑定每页显示条数事件
  74. this.bindPageSizeEvents();
  75. }
  76. /**
  77. * 初始化表头
  78. */
  79. initTableHeader() {
  80. const tableHead = document.querySelector(`#${this.options.tableId} thead tr`);
  81. if (!tableHead) return;
  82. // 清空表头
  83. tableHead.innerHTML = "";
  84. // 创建表头单元格
  85. this.options.columns.forEach(column => {
  86. const th = document.createElement("th");
  87. // 添加列的自定义类名
  88. if (column.className) {
  89. th.className = column.className;
  90. }
  91. // 使用自定义渲染器(如果有)
  92. if (column.headerRenderer && this.headerRenderers[column.headerRenderer]) {
  93. this.headerRenderers[column.headerRenderer](column, th);
  94. } else {
  95. th.textContent = column.title;
  96. }
  97. tableHead.appendChild(th);
  98. });
  99. }
  100. /**
  101. * 更新全选框状态
  102. */
  103. updateSelectAllCheckboxState() {
  104. const selectAllCheckbox = document.getElementById('select-all');
  105. const checkboxes = document.querySelectorAll('.row-checkbox');
  106. const checkedCheckboxes = document.querySelectorAll('.row-checkbox:checked');
  107. if (selectAllCheckbox) {
  108. // 如果所有复选框都被选中,则全选框也被选中
  109. selectAllCheckbox.checked = checkboxes.length > 0 && checkboxes.length === checkedCheckboxes.length;
  110. // 如果部分复选框被选中,则全选框处于不确定状态
  111. selectAllCheckbox.indeterminate = checkedCheckboxes.length > 0 && checkboxes.length !== checkedCheckboxes.length;
  112. }
  113. }
  114. /**
  115. * 获取选中行的ID
  116. * @returns {Array} 选中行的ID数组
  117. */
  118. getSelectedRowIds() {
  119. const selectedIds = [];
  120. const checkedCheckboxes = document.querySelectorAll('.row-checkbox:checked');
  121. checkedCheckboxes.forEach(checkbox => {
  122. selectedIds.push(checkbox.getAttribute('data-id'));
  123. });
  124. return selectedIds;
  125. }
  126. /**
  127. * 渲染表格
  128. */
  129. renderTable() {
  130. const tableBody = document.querySelector(`#${this.options.tableId} tbody`);
  131. if (!tableBody) return;
  132. tableBody.innerHTML = ""; // 清空表格
  133. const totalItems = this.filteredData.length;
  134. const totalPages = Math.ceil(totalItems / this.itemsPerPage);
  135. // 计算当前页的数据范围
  136. const startIndex = (this.currentPage - 1) * this.itemsPerPage;
  137. const endIndex = Math.min(startIndex + this.itemsPerPage, totalItems);
  138. const currentPageData = this.filteredData.slice(startIndex, endIndex);
  139. // 填充表格数据
  140. currentPageData.forEach((rowData) => {
  141. const row = document.createElement("tr");
  142. // 遍历列配置,生成单元格
  143. this.options.columns.forEach((column) => {
  144. const cell = document.createElement("td");
  145. // 添加列的自定义类名
  146. if (column.className) {
  147. cell.className = column.className;
  148. }
  149. // 获取单元格值
  150. let value = rowData[column.field];
  151. // 如果值为空且有默认值,则使用默认值
  152. if ((value === undefined || value === null || value === "") && column.defaultValue !== undefined) {
  153. value = column.defaultValue;
  154. }
  155. // 如果有格式化函数,则使用格式化函数处理值
  156. if (column.formatter && typeof column.formatter === "function") {
  157. cell.innerHTML = column.formatter(value, rowData);
  158. } else {
  159. cell.textContent = value;
  160. }
  161. row.appendChild(cell);
  162. });
  163. tableBody.appendChild(row);
  164. });
  165. // 更新分页信息
  166. this.updatePagination(totalItems, totalPages);
  167. }
  168. /**
  169. * 更新分页信息和控件
  170. * @param {Number} totalItems - 总条目数
  171. * @param {Number} totalPages - 总页数
  172. */
  173. updatePagination(totalItems, totalPages) {
  174. // 更新分页信息文本
  175. const paginationInfo = document.getElementById(this.options.paginationInfoId);
  176. if (paginationInfo) {
  177. paginationInfo.textContent = `显示第 ${
  178. totalItems === 0 ? 0 : (this.currentPage - 1) * this.itemsPerPage + 1
  179. } 到第 ${Math.min(
  180. this.currentPage * this.itemsPerPage,
  181. totalItems
  182. )} 条记录,共计 ${totalItems} 条记录`;
  183. }
  184. // 更新每页显示条数
  185. const currentPageSize = document.getElementById(this.options.pageSizeId);
  186. if (currentPageSize) {
  187. currentPageSize.textContent = this.itemsPerPage;
  188. }
  189. // 更新分页控件
  190. const paginationControls = document.getElementById(
  191. this.options.paginationControlsId
  192. );
  193. if (!paginationControls) return;
  194. paginationControls.innerHTML = "";
  195. // 上一页按钮
  196. const prevDisabled = this.currentPage === 1 || totalItems === 0;
  197. const prevItem = document.createElement("li");
  198. prevItem.className = `page-item ${prevDisabled ? "disabled" : ""}`;
  199. prevItem.innerHTML = `
  200. <a class="page-link" href="#" data-page="${this.currentPage - 1}" ${
  201. prevDisabled ? 'tabindex="-1" aria-disabled="true"' : ""
  202. }>
  203. <i class="ti ti-chevron-left"></i>
  204. </a>
  205. `;
  206. paginationControls.appendChild(prevItem);
  207. // 页码按钮
  208. const maxVisiblePages = this.options.maxVisiblePages;
  209. let startPage = Math.max(
  210. 1,
  211. this.currentPage - Math.floor(maxVisiblePages / 2)
  212. );
  213. let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
  214. if (endPage - startPage < maxVisiblePages - 1 && startPage > 1) {
  215. startPage = Math.max(1, endPage - maxVisiblePages + 1);
  216. }
  217. // 添加页码
  218. for (let i = 1; i <= totalPages; i++) {
  219. // 显示第一页、最后一页和当前页附近的页码
  220. if (
  221. i === 1 ||
  222. i === totalPages ||
  223. (i >= startPage && i <= endPage)
  224. ) {
  225. const pageItem = document.createElement("li");
  226. pageItem.className = `page-item ${
  227. i === this.currentPage ? "active" : ""
  228. }`;
  229. pageItem.innerHTML = `<a class="page-link" href="#" data-page="${i}">${i}</a>`;
  230. paginationControls.appendChild(pageItem);
  231. }
  232. // 添加省略号
  233. else if (i === startPage - 1 || i === endPage + 1) {
  234. const ellipsisItem = document.createElement("li");
  235. ellipsisItem.className = "page-item disabled";
  236. ellipsisItem.innerHTML = '<a class="page-link" href="#">...</a>';
  237. paginationControls.appendChild(ellipsisItem);
  238. }
  239. }
  240. // 下一页按钮
  241. const nextDisabled = this.currentPage === totalPages || totalItems === 0;
  242. const nextItem = document.createElement("li");
  243. nextItem.className = `page-item ${nextDisabled ? "disabled" : ""}`;
  244. nextItem.innerHTML = `
  245. <a class="page-link" href="#" data-page="${this.currentPage + 1}" ${
  246. nextDisabled ? 'tabindex="-1" aria-disabled="true"' : ""
  247. }>
  248. <i class="ti ti-chevron-right"></i>
  249. </a>
  250. `;
  251. paginationControls.appendChild(nextItem);
  252. }
  253. /**
  254. * 绑定分页事件
  255. */
  256. bindPaginationEvents() {
  257. const paginationControls = document.getElementById(
  258. this.options.paginationControlsId
  259. );
  260. if (!paginationControls) return;
  261. paginationControls.addEventListener("click", (e) => {
  262. e.preventDefault();
  263. // 检查点击的是否是分页链接
  264. const pageLink = e.target.closest(".page-link");
  265. if (!pageLink) return;
  266. // 检查链接是否被禁用
  267. if (pageLink.getAttribute("aria-disabled") === "true") return;
  268. // 获取目标页码
  269. const targetPage = parseInt(pageLink.getAttribute("data-page"));
  270. if (isNaN(targetPage)) return;
  271. // 更新当前页码
  272. this.currentPage = targetPage;
  273. // 重新渲染表格
  274. this.renderTable();
  275. });
  276. }
  277. /**
  278. * 绑定每页显示条数事件
  279. */
  280. bindPageSizeEvents() {
  281. const pageSizeItems = document.querySelectorAll(`.${this.options.pageSizeItemClass}`);
  282. pageSizeItems.forEach((item) => {
  283. item.addEventListener("click", (e) => {
  284. e.preventDefault();
  285. // 获取新的每页显示条数
  286. const newItemsPerPage = parseInt(item.getAttribute("data-size"));
  287. if (isNaN(newItemsPerPage)) return;
  288. // 更新每页显示条数
  289. this.itemsPerPage = newItemsPerPage;
  290. this.currentPage = 1; // 重置为第一页
  291. // 重新渲染表格
  292. this.renderTable();
  293. });
  294. });
  295. }
  296. /**
  297. * 筛选数据
  298. * @param {Function} filterFn - 筛选函数
  299. */
  300. filter(filterFn) {
  301. if (typeof filterFn !== "function") {
  302. this.filteredData = [...this.data];
  303. } else {
  304. this.filteredData = this.data.filter(filterFn);
  305. }
  306. this.currentPage = 1; // 重置为第一页
  307. this.renderTable(); // 重新渲染表格
  308. }
  309. /**
  310. * 搜索数据
  311. * @param {String} keyword - 搜索关键字
  312. * @param {Array} fields - 要搜索的字段
  313. */
  314. search(keyword, fields) {
  315. if (!keyword) {
  316. this.filteredData = [...this.data];
  317. } else {
  318. const lowerKeyword = keyword.toLowerCase();
  319. this.filteredData = this.data.filter((item) => {
  320. return fields.some((field) => {
  321. const value = item[field];
  322. if (value === undefined || value === null) return false;
  323. // 处理对象类型的值
  324. if (typeof value === "object" && value.text) {
  325. return value.text.toLowerCase().includes(lowerKeyword);
  326. }
  327. return String(value).toLowerCase().includes(lowerKeyword);
  328. });
  329. });
  330. }
  331. this.currentPage = 1; // 重置为第一页
  332. this.renderTable(); // 重新渲染表格
  333. }
  334. /**
  335. * 重置筛选
  336. */
  337. resetFilter() {
  338. this.filteredData = [...this.data];
  339. this.currentPage = 1; // 重置为第一页
  340. this.renderTable(); // 重新渲染表格
  341. }
  342. /**
  343. * 更新数据
  344. * @param {Array} data - 新数据
  345. */
  346. updateData(data) {
  347. this.data = data || [];
  348. this.filteredData = [...this.data];
  349. this.currentPage = 1; // 重置为第一页
  350. this.renderTable(); // 重新渲染表格
  351. }
  352. /**
  353. * 添加数据
  354. * @param {Object|Array} data - 要添加的数据
  355. */
  356. addData(data) {
  357. if (Array.isArray(data)) {
  358. this.data = [...this.data, ...data];
  359. } else {
  360. this.data.push(data);
  361. }
  362. this.filteredData = [...this.data];
  363. this.renderTable(); // 重新渲染表格
  364. }
  365. /**
  366. * 删除数据
  367. * @param {Function} predicate - 判断函数
  368. */
  369. removeData(predicate) {
  370. if (typeof predicate !== "function") return;
  371. this.data = this.data.filter((item) => !predicate(item));
  372. this.filteredData = [...this.data];
  373. this.renderTable(); // 重新渲染表格
  374. }
  375. /**
  376. * 生成模拟订单数据
  377. * @param {Number} count - 数据条数
  378. * @returns {Array} - 生成的数据
  379. */
  380. static generateOrderData(count = 100) {
  381. const statusOptions = [
  382. { text: "进行中", class: "bg-success" },
  383. { text: "待处理", class: "bg-warning" },
  384. { text: "已完成", class: "bg-info" },
  385. { text: "已取消", class: "bg-danger" },
  386. { text: "已超时", class: "bg-secondary" },
  387. ];
  388. const locations = ["A区", "B区", "C区", "D区", "E区", "F区"];
  389. const goods = [
  390. "原材料",
  391. "半成品",
  392. "成品",
  393. "包装材料",
  394. "零部件",
  395. "工具",
  396. ];
  397. const orders = [];
  398. for (let i = 1; i <= count; i++) {
  399. // 生成订单ID
  400. const id = `ORD-${String(i).padStart(3, "0")}`;
  401. // 生成订单时间(过去30天内的随机时间)
  402. const now = new Date();
  403. const randomDays = Math.floor(Math.random() * 30);
  404. const randomHours = Math.floor(Math.random() * 24);
  405. const randomMinutes = Math.floor(Math.random() * 60);
  406. const orderDate = new Date(now);
  407. orderDate.setDate(now.getDate() - randomDays);
  408. orderDate.setHours(now.getHours() - randomHours);
  409. orderDate.setMinutes(now.getMinutes() - randomMinutes);
  410. const orderTime = `${orderDate.getFullYear()}-${String(
  411. orderDate.getMonth() + 1
  412. ).padStart(2, "0")}-${String(orderDate.getDate()).padStart(
  413. 2,
  414. "0"
  415. )} ${String(orderDate.getHours()).padStart(2, "0")}:${String(
  416. orderDate.getMinutes()
  417. ).padStart(2, "0")}`;
  418. // 随机选择起点和终点
  419. const from = `${
  420. locations[Math.floor(Math.random() * locations.length)]
  421. }-${Math.floor(Math.random() * 20) + 1}号仓位`;
  422. let to;
  423. do {
  424. to = `${
  425. locations[Math.floor(Math.random() * locations.length)]
  426. }-${Math.floor(Math.random() * 20) + 1}号仓位`;
  427. } while (to === from); // 确保起点和终点不同
  428. // 随机选择货物
  429. const good = goods[Math.floor(Math.random() * goods.length)];
  430. // 生成期限(订单时间后的1-24小时)
  431. const deadlineDate = new Date(orderDate);
  432. deadlineDate.setHours(
  433. orderDate.getHours() + Math.floor(Math.random() * 24) + 1
  434. );
  435. const deadline = `${deadlineDate.getFullYear()}-${String(
  436. deadlineDate.getMonth() + 1
  437. ).padStart(2, "0")}-${String(deadlineDate.getDate()).padStart(
  438. 2,
  439. "0"
  440. )} ${String(deadlineDate.getHours()).padStart(2, "0")}:${String(
  441. deadlineDate.getMinutes()
  442. ).padStart(2, "0")}`;
  443. // 随机选择状态
  444. const status =
  445. statusOptions[Math.floor(Math.random() * statusOptions.length)];
  446. // 生成开始和结束时间
  447. let startTime = "";
  448. let endTime = "";
  449. if (status.text !== "待处理") {
  450. // 如果不是待处理状态,则有开始时间
  451. const startDate = new Date(orderDate);
  452. startDate.setMinutes(
  453. startDate.getMinutes() + Math.floor(Math.random() * 60)
  454. );
  455. startTime = `${startDate.getFullYear()}-${String(
  456. startDate.getMonth() + 1
  457. ).padStart(2, "0")}-${String(startDate.getDate()).padStart(
  458. 2,
  459. "0"
  460. )} ${String(startDate.getHours()).padStart(2, "0")}:${String(
  461. startDate.getMinutes()
  462. ).padStart(2, "0")}`;
  463. // 如果是已完成或已取消或已超时状态,则有结束时间
  464. if (
  465. status.text === "已完成" ||
  466. status.text === "已取消" ||
  467. status.text === "已超时"
  468. ) {
  469. const endDate = new Date(startDate);
  470. endDate.setMinutes(
  471. endDate.getMinutes() + Math.floor(Math.random() * 120)
  472. );
  473. endTime = `${endDate.getFullYear()}-${String(
  474. endDate.getMonth() + 1
  475. ).padStart(2, "0")}-${String(endDate.getDate()).padStart(
  476. 2,
  477. "0"
  478. )} ${String(endDate.getHours()).padStart(2, "0")}:${String(
  479. endDate.getMinutes()
  480. ).padStart(2, "0")}`;
  481. }
  482. }
  483. // 创建订单对象
  484. const order = {
  485. id,
  486. orderTime,
  487. from,
  488. to,
  489. good,
  490. deadline,
  491. startTime,
  492. endTime,
  493. status,
  494. };
  495. orders.push(order);
  496. }
  497. return orders;
  498. }
  499. /**
  500. * 生成模拟车辆数据
  501. * @param {Number} count - 数据条数
  502. * @returns {Array} - 生成的数据
  503. */
  504. static generateVehicleData(count = 100) {
  505. const statusOptions = [
  506. { text: "空闲", class: "bg-success" },
  507. { text: "任务中", class: "bg-warning" },
  508. { text: "充电中", class: "bg-info" },
  509. { text: "维修中", class: "bg-danger" },
  510. { text: "离线", class: "bg-secondary" },
  511. ];
  512. const vehicleTypes = ["AGV-小车", "AGV-叉车", "AGV-拖车", "AGV-搬运车", "AGV-牵引车"];
  513. const locations = ["A区", "B区", "C区", "D区", "E区", "F区"];
  514. const vehicles = [];
  515. for (let i = 1; i <= count; i++) {
  516. // 生成车辆ID
  517. const id = `AGV-${String(i).padStart(3, "0")}`;
  518. // 随机选择车辆类型
  519. const type = vehicleTypes[Math.floor(Math.random() * vehicleTypes.length)];
  520. // 随机生成电量 (0-100)
  521. const battery = Math.floor(Math.random() * 101);
  522. // 随机选择位置
  523. const location = `${
  524. locations[Math.floor(Math.random() * locations.length)]
  525. }-${Math.floor(Math.random() * 20) + 1}号位置`;
  526. // 随机选择状态
  527. const status =
  528. statusOptions[Math.floor(Math.random() * statusOptions.length)];
  529. // 随机生成运行时间 (0-10000小时)
  530. const runtime = Math.floor(Math.random() * 10000);
  531. // 创建车辆对象
  532. const vehicle = {
  533. id,
  534. type,
  535. battery,
  536. location,
  537. status,
  538. runtime: `${runtime}小时`,
  539. lastMaintenance: `${Math.floor(Math.random() * 365)}天前`,
  540. };
  541. vehicles.push(vehicle);
  542. }
  543. return vehicles;
  544. }
  545. }
  546. // 如果在Node.js环境中,导出模块
  547. if (typeof module !== 'undefined' && module.exports) {
  548. module.exports = TcsTable;
  549. }