浏览代码

feat: 信息价分类总表

vian 1 年之前
父节点
当前提交
5b9a1efae9

+ 15 - 0
modules/all_models/infoPriceClass.js

@@ -0,0 +1,15 @@
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const collectionName = 'infoPriceClass';
+
+const modelSchema = {
+  ID: { type: String, required: true, index: true },
+  class: { type: Number, unique: true, index: true }, // 类别
+  classCode: String, // 别名编码
+  code: String, // 编码
+  name: String, // 材料名称
+  specs: String, // 规格型号
+  unit: String, // 单位
+};
+mongoose.model(collectionName, new Schema(modelSchema, { versionKey: false, collection: collectionName }));

+ 45 - 0
modules/price_info_class/controllers/index.js

@@ -0,0 +1,45 @@
+import BaseController from "../../common/base/base_controller";
+const facade = require('../facade/index');
+const config = require("../../../config/config.js");
+
+class PriceInfoClassController extends BaseController {
+    async main(req, res) {
+        const renderData = {
+            title: '材料信息价类别总表',
+            userAccount: req.session.managerData.username,
+            userID: req.session.managerData.userID,
+            LicenseKey: config.getLicenseKey(process.env.NODE_ENV),
+        };
+        await facade.setIDForData();
+        res.render("maintain/price_info_class/html/main.html", renderData);
+    }
+
+    // 获取分页数据
+    async getPagingData(req, res) {
+        try {
+            const { page, pageSize, searchStr } = JSON.parse(req.body.data);
+            const data = await facade.getPagingData(page, pageSize, searchStr);
+            res.json({ error: 0, message: 'getData success', data });
+        } catch (err) {
+            console.log(err);
+        }
+    }
+
+    // 编辑
+    async editClassData(req, res) {
+        try {
+            const { postData } = JSON.parse(req.body.data);
+            await facade.editClassData(postData);
+            res.json({ error: 0, message: 'editPrice success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+
+}
+
+module.exports = {
+    priceInfoClassController: new PriceInfoClassController()
+};

+ 109 - 0
modules/price_info_class/facade/index.js

@@ -0,0 +1,109 @@
+const mongoose = require('mongoose');
+const uuidV1 = require('uuid/v1');
+
+const priceInfoClassModel = mongoose.model('infoPriceClass');
+
+
+// 旧数据没有ID,初次加载需要给数据加上ID
+const setIDForData = async () => {
+    const item = await priceInfoClassModel.findOne({}).lean();
+    if (item.ID) {
+        return;
+    }
+    const bulks = [];
+    const items = await priceInfoClassModel.find({}, '-_id class ID').lean();
+    items.forEach(item => {
+        if (!item.ID) {
+            bulks.push({
+                updateOne: {
+                    filter: { class: item.class },
+                    update: { $set: { ID: uuidV1() } }
+                }
+            });
+        }
+    });
+    if (bulks.length) {
+        await priceInfoClassModel.bulkWrite(bulks);
+    }
+}
+
+
+// 获取分页数据
+const getPagingData = async (page, pageSize, searchStr) => {
+    let query = {};
+    if (searchStr) {
+        const nameReg = new RegExp(searchStr);
+        query = {
+            $or: [{ classCode: searchStr }, { name: { $regex: nameReg } }]
+        }
+    }
+    const totalCount = await priceInfoClassModel.count(query);
+    const items = await priceInfoClassModel.find(query).lean().sort({ class: 1 }).skip(page * pageSize).limit(pageSize);
+    return { items, totalCount };
+}
+
+const UpdateType = {
+    UPDATE: 'update',
+    DELETE: 'delete',
+    CREATE: 'create',
+};
+
+// 编辑表格
+async function editClassData(postData) {
+    const classList = [];
+    const bulks = [];
+    postData.forEach(data => {
+        if (data.data && data.data.class) {
+            classList.push(data.data.class);
+        }
+        if (data.type === UpdateType.UPDATE) {
+            bulks.push({
+                updateOne: {
+                    filter: { ID: data.ID },
+                    update: { ...data.data }
+                }
+            });
+        } else if (data.type === UpdateType.DELETE) {
+            bulks.push({
+                deleteOne: {
+                    filter: { ID: data.ID }
+                }
+            });
+        } else {
+            bulks.push({
+                insertOne: {
+                    document: data.data
+                }
+            });
+        }
+    });
+
+    if (classList.length) {
+        const classItems = await priceInfoClassModel.find({ class: { $in: classList } }, '-_id class').lean();
+        if (classItems) {
+            const existClassList = [];
+            const classMap = {};
+            classItems.forEach(item => {
+                classMap[item.class] = 1;
+            });
+            classList.forEach(classVal => {
+                if (classMap[classVal]) {
+                    existClassList.push(classVal);
+                }
+            });
+            if (existClassList.length) {
+                throw new Error(`类别${existClassList.join(',')},已存在。`)
+            }
+        }
+    }
+
+    if (bulks.length) {
+        await priceInfoClassModel.bulkWrite(bulks);
+    }
+}
+
+module.exports = {
+    setIDForData,
+    getPagingData,
+    editClassData
+}

+ 17 - 0
modules/price_info_class/routes/index.js

@@ -0,0 +1,17 @@
+/**
+ * Created by zhang on 2018/9/3.
+ */
+
+const express = require("express");
+const router = express.Router();
+const { priceInfoClassController } = require('../controllers/index');
+
+module.exports = function (app) {
+    router.get("/main", priceInfoClassController.auth, priceInfoClassController.init, priceInfoClassController.main);
+    router.post("/getPagingData", priceInfoClassController.auth, priceInfoClassController.init, priceInfoClassController.getPagingData);
+    router.post("/editClassData", priceInfoClassController.auth, priceInfoClassController.init, priceInfoClassController.editClassData);
+
+    app.use("/priceInfoClass", router);
+};
+
+

+ 91 - 0
web/maintain/price_info_class/css/index.css

@@ -0,0 +1,91 @@
+html {
+    height: 100%;
+}
+
+body {
+    height: 100%;
+    font-size: 0.9rem;
+}
+
+.dropdown-menu {
+    font-size: 0.9rem
+}
+
+/*自定义css*/
+
+.header {
+    position: relative;
+    background: #e1e1e1
+}
+
+.header .header-logo {
+    background: #ff6501;
+    color: #fff;
+    float: left;
+    padding-top: .25rem;
+    padding-bottom: .25rem;
+    margin-right: 1rem;
+    font-size: 1.25rem;
+    line-height: inherit
+}
+
+.top-msg {
+    position: fixed;
+    top: 0;
+    width: 100%;
+    z-index: 999
+}
+
+.in-1 {
+    padding-left: 0rem!important
+}
+
+.in-2 {
+    padding-left: 1rem!important
+}
+
+.in-3 {
+    padding-left: 1.5rem!important
+}
+
+.in-4 {
+    padding-left: 2rem!important
+}
+
+.in-5 {
+    padding-left: 2.5rem!important
+}
+
+.in-6 {
+    padding-left: 3rem!important
+}
+
+.disabled {
+    pointer-events: none;
+    opacity: .65;
+    color: #666;
+}
+
+.wrapper {
+    position: absolute;
+    top: 38px;
+    bottom: 0;
+    width: 100%;
+}
+
+.search {
+    padding: 5px 10px;
+    /* background-color: #f7f7f7; */
+}
+
+.search-input {
+    width: 250px;
+}
+
+.main {
+    height: calc(100% - 78px);
+}
+
+.sheet {
+    height: 100%;
+}

+ 53 - 0
web/maintain/price_info_class/html/main.html

@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <title>材料信息价总表</title>
+    <link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css">
+    <link rel="stylesheet" href="/web/maintain/price_info_summary/css/index.css">
+    <link rel="stylesheet" href="/lib/font-awesome/font-awesome.min.css">
+    <link rel="stylesheet" href="/lib/spreadjs/sheets/css/gc.spread.sheets.sc.css" type="text/css">
+    <link rel="stylesheet" href="/lib/jquery-contextmenu/jquery.contextMenu.css" type="text/css">
+</head>
+
+<body>
+    <div class="header">
+        <nav class="navbar navbar-toggleable-lg navbar-light bg-faded p-0 ">
+            <span class="header-logo px-2">材料信息价总表</span>
+            <a class="lock" data-locked="true" href="javascript:void(0);" title="解锁"><i
+                class="fa fa-unlock-alt"></i></a>
+        </nav>
+    </div>
+    <div class="search">
+        <input class="form-control form-control-sm search-input" id="summary-search" type="text" value="" placeholder="输入回车进行搜索">
+    </div>
+
+    <div class="main">
+        <div class="sheet" id="summary-spread"></div>
+    </div>
+    <!-- JS. -->
+    <script src="/lib/jquery/jquery.min.js"></script>
+    <script src="/lib/jquery-contextmenu/jquery.contextMenu.min.js"></script>
+    <script src="/lib/jquery-contextmenu/jquery.ui.position.js"></script>
+    <script src="/lib/tether/tether.min.js"></script>
+    <script src="/lib/bootstrap/bootstrap.min.js"></script>
+    <script src="/public/web/PerfectLoad.js"></script>
+    <script src="/lib/spreadjs/sheets/gc.spread.sheets.all.11.1.2.min.js"></script>
+    <script>GC.Spread.Sheets.LicenseKey = '<%- LicenseKey %>';</script>
+    <script src="/public/web/uuid.js"></script>
+    <script src="/lib/lodash/lodash.js"></script>
+    <script src="/public/web/scMathUtil.js"></script>
+    <script src="/public/web/treeDataHelper.js"></script>
+    <script src="/public/web/common_ajax.js"></script>
+    <script src="/public/web/lock_util.js"></script>
+    <script src="/public/web/id_tree.js"></script>
+    <script src="/public/web/tools_const.js"></script>
+    <script src="/public/web/tree_sheet/tree_sheet_controller.js"></script>
+    <script src="/public/web/tree_sheet/tree_sheet_helper.js"></script>
+    <script src="/public/web/sheet/sheet_common.js"></script>
+    <script src="/web/maintain/price_info_class/js/summarySheet.js"></script>
+    <script src="/web/maintain/price_info_class/js/index.js"></script>
+</body>
+</html>

+ 21 - 0
web/maintain/price_info_class/js/index.js

@@ -0,0 +1,21 @@
+
+
+$(document).ready(() => {
+    //init();
+    // $('[data-toggle="tooltip"]').tooltip();
+    // AREA_BOOK.handleSelectionChanged(0);
+    // 锁定、解锁
+    lockUtil.displayLock($('.lock'), lockUtil.getLocked());
+    $('.lock').click(function () {
+        window.location.search = `?locked=${!lockUtil.getLocked()}`;
+    });
+
+    // 搜索
+    $('#summary-search').bind('keydown', function (event) {
+        if (event.keyCode === 13) {
+            // 回车搜索
+            const searchStr = $(this).val();
+            SUMMARY_BOOK.handleSearch(searchStr);
+        }
+    })
+});

+ 258 - 0
web/maintain/price_info_class/js/summarySheet.js

@@ -0,0 +1,258 @@
+
+function setAlign(sheet, headers) {
+  const fuc = () => {
+    headers.forEach(({ hAlign, vAlign }, index) => {
+      sheetCommonObj.setAreaAlign(sheet.getRange(-1, index, -1, 1), hAlign, vAlign)
+    });
+  };
+  sheetCommonObj.renderSheetFunc(sheet, fuc);
+}
+
+function setFormatter(sheet, headers) {
+  const fuc = () => {
+    headers.forEach(({ formatter }, index) => {
+      if (formatter) {
+        sheet.setFormatter(-1, index, formatter);
+      }
+    });
+  };
+  sheetCommonObj.renderSheetFunc(sheet, fuc);
+}
+
+function initSheet(dom, setting) {
+  const workBook = sheetCommonObj.buildSheet(dom, setting);
+  const sheet = workBook.getSheet(0);
+  setAlign(sheet, setting.header);
+  setFormatter(sheet, setting.header);
+  return workBook;
+}
+
+function showData(sheet, data, headers, emptyRows) {
+  /* const style = new GC.Spread.Sheets.Style();
+  style.wordWrap = true; */
+  const fuc = () => {
+    sheet.setRowCount(data.length);
+    data.forEach((item, row) => {
+      headers.forEach(({ dataCode }, col) => {
+        //sheet.setStyle(row, col, style, GC.Spread.Sheets.SheetArea.viewport);
+        sheet.setValue(row, col, item[dataCode] || '');
+      });
+      sheet.autoFitRow(row);
+    });
+    if (emptyRows) {
+      sheet.addRows(data.length, emptyRows);
+    }
+    //sheet.autoFitRow(0);
+  };
+  sheetCommonObj.renderSheetFunc(sheet, fuc);
+}
+
+// 获取当前表中行数据
+function getRowData(sheet, row, headers) {
+  const item = {};
+  headers.forEach(({ dataCode }, index) => {
+    const value = sheet.getValue(row, index) || '';
+    if (value) {
+      item[dataCode] = value;
+    }
+  });
+  return item;
+}
+
+// 获取表数据和缓存数据的不同数据
+function getRowDiffData(curRowData, cacheRowData, headers) {
+  let item = null;
+  headers.forEach(({ dataCode }) => {
+    const curValue = curRowData[dataCode];
+    const cacheValue = cacheRowData[dataCode];
+    if (!cacheValue && !curValue) {
+      return;
+    }
+    if (cacheValue !== curValue) {
+      if (!item) {
+        item = {};
+      }
+      item[dataCode] = curValue || '';
+    }
+  });
+  return item;
+}
+
+const UpdateType = {
+  UPDATE: 'update',
+  DELETE: 'delete',
+  CREATE: 'create',
+};
+
+const TIME_OUT = 20000;
+
+const SUMMARY_BOOK = (() => {
+  const locked = lockUtil.getLocked();
+  const setting = {
+    header: [
+      { headerName: 'class', headerWidth: 100, dataCode: 'class', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+      { headerName: '别名编码', headerWidth: 100, dataCode: 'classCode', dataType: 'String', hAlign: 'left', vAlign: 'center', formatter: "@" },
+      { headerName: '材料名称', headerWidth: 350, dataCode: 'name', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '规格型号', headerWidth: 200, dataCode: 'specs', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '单位', headerWidth: 80, dataCode: 'unit', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+    ],
+  };
+  // 初始化表格
+  const workBook = initSheet($('#summary-spread')[0], setting);
+  workBook.options.allowUserDragDrop = true;
+  workBook.options.allowUserDragFill = true;
+  lockUtil.lockSpreads([workBook], locked);
+  const sheet = workBook.getSheet(0);
+
+  // 当前数据缓存
+  const cache = [];
+  // 清空
+  function clear() {
+    cache.length = 0;
+    sheet.setRowCount(0);
+  }
+
+  let loading = false;
+  // 当前页面数据总量
+  let totalCount = 0;
+  // 当前页数
+  let curPage = 0;
+
+  // 搜索内容
+  let searchStr = '';
+
+  // 加载分页数据
+  const loadPageData = async (page) => {
+    curPage = page;
+    loading = true;
+    const data = await ajaxPost('/priceInfoClass/getPagingData', { page, searchStr, pageSize: 100 }, TIME_OUT);
+    totalCount = data.totalCount;
+    cache.push(...data.items);
+    showData(sheet, cache, setting.header, 5);
+    loading = false;
+  }
+
+  // 搜索
+  const handleSearch = (val) => {
+    if (val) {
+      // 处理特殊字符,否则正则搜不到
+      searchStr = val
+        .replace(/\\/g, '\\\\')
+        .replace(/\[/g, '\\[')
+        .replace(/\]/g, '\\]')
+        .replace(/\(/g, '\\(')
+        .replace(/\)/g, '\\)')
+        .replace(/\+/g, '\\+')
+        .replace(/\?/g, '\\?')
+        .replace(/\*/g, '\\*')
+        .replace(/\$/g, '\\$')
+        .replace(/\^/g, '\\^')
+    } else {
+      searchStr = '';
+    }
+    clear();
+    loadPageData(0);
+  }
+
+
+  // 无限滚动加载
+  const onTopRowChanged = (sender, args) => {
+    const bottomRow = args.sheet.getViewportBottomRow(1);
+    console.log(cache.length, totalCount, loading, cache.length - 1, bottomRow)
+    if (cache.length >= totalCount || loading) {
+      return;
+    }
+    if (cache.length - 1 <= bottomRow) {
+      loadPageData(curPage + 1, searchStr);
+    }
+  }
+  sheet.bind(GC.Spread.Sheets.Events.TopRowChanged, _.debounce(onTopRowChanged, 100));
+
+  // 编辑处理
+  async function handleEdit(changedCells) {
+    $.bootstrapLoading.start();
+    const postData = []; // 请求用
+    // 更新缓存用
+    const updateData = [];
+    const deleteData = [];
+    const insertData = [];
+    try {
+      changedCells.forEach(({ row }) => {
+        if (cache[row]) {
+          const rowData = getRowData(sheet, row, setting.header);
+          if (Object.keys(rowData).length) { // 还有数据,更新
+            const diffData = getRowDiffData(rowData, cache[row], setting.header);
+            if (diffData) {
+              postData.push({ type: UpdateType.UPDATE, ID: cache[row].ID, data: diffData });
+              updateData.push({ row, data: diffData });
+            }
+          } else { // 该行无数据了,删除
+            postData.push({ type: UpdateType.DELETE, ID: cache[row].ID });
+            deleteData.push(cache[row]);
+          }
+        } else { // 新增
+          const rowData = getRowData(sheet, row, setting.header);
+          if (Object.keys(rowData).length) {
+            rowData.ID = uuid.v1();
+            postData.push({ type: UpdateType.CREATE, data: rowData });
+            insertData.push(rowData);
+          }
+        }
+      });
+      if (postData.length) {
+        await ajaxPost('/priceInfoClass/editClassData', { postData }, 1000 * 60 * 2);
+        // 更新缓存,先更新然后删除,最后再新增,防止先新增后缓存数据的下标与更新、删除数据的下标对应不上
+        updateData.forEach(item => {
+          Object.assign(cache[item.row], item.data);
+        });
+        deleteData.forEach(item => {
+          const index = cache.indexOf(item);
+          if (index >= 0) {
+            cache.splice(index, 1);
+          }
+        });
+        insertData.forEach(item => cache.push(item));
+        if (deleteData.length || insertData.length) {
+          showData(sheet, cache, setting.header, 5);
+        }
+      }
+    } catch (err) {
+      // 恢复各单元格数据
+      showData(sheet, cache, setting.header, 5);
+    }
+    $.bootstrapLoading.end();
+  }
+  sheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (e, info) {
+    const changedCells = [{ row: info.row }];
+    handleEdit(changedCells);
+  });
+  sheet.bind(GC.Spread.Sheets.Events.RangeChanged, function (e, info) {
+    const changedRows = [];
+    let preRow;
+    info.changedCells.forEach(({ row }) => {
+      if (row !== preRow) {
+        changedRows.push({ row });
+      }
+      preRow = row;
+    });
+    handleEdit(changedRows);
+  });
+
+  const initData = async () => {
+    try {
+      $.bootstrapLoading.start();
+      await loadPageData(0);
+    } catch (error) {
+      console.log(error);
+      alert(error.message);
+    }
+    $.bootstrapLoading.end();
+  }
+
+  initData();
+
+  return {
+    sheet,
+    handleSearch,
+  }
+})()