Browse Source

feat: 添加物品明细标签,优化单据明细与物品明细展示逻辑

xieyong 15 hours ago
parent
commit
2c73d960aa

+ 380 - 149
src/views/saleManage/saleOrder/accountstatement/components/accountDetailTable.vue

@@ -1,164 +1,194 @@
 <template>
   <div class="account-detail-table">
-    <el-form ref="detailForm" :model="{ flatList }">
-      <ele-pro-table
-        ref="detailTable"
-        row-key="id"
-        :needPage="false"
-        :columns="columns"
-        max-height="500px"
-        :datasource="flatList"
-        :cache-key="cacheKeyUrl"
-        :selection.sync="selection"
-        class="time-form"
-        @header-dragend="handleHeaderDragend"
-      >
-        <template v-slot:toolbar>
-          <div class="detail-toolbar">
-            <setAllValue :disabled="!selection.length" inputType="number" title="税率" valueKey="taxRate" @success="confirmBatchSet" />
-            <setAllValue :disabled="!selection.length" inputType="number" title="单价" valueKey="singlePrice" @success="confirmBatchSet" />
-            <setAllValue :disabled="!selection.length" inputType="number" title="折让比例" valueKey="discountRatio" @success="confirmBatchSet" />
-            <!-- <el-button
-              v-if="!isView"
-              type="primary"
-              size="small"
-              @click="batchSetTaxRate"
-              >批量设置税率</el-button
-            >
-            <el-button
-              v-if="!isView"
-              type="primary"
-              size="small"
-              @click="batchSetPrice"
-              >批量设置单价</el-button
-            >
-            <el-button
-              v-if="!isView"
-              type="primary"
-              size="small"
-              @click="batchSetDiscount"
-              >批量设置折让比例</el-button
-            > -->
-            <!-- <div class="summary-info">
-              <span
-                >本次总对账金额:<b>{{ totalStatementAmount }}</b> 元</span
-              >
-              <span
-                >本次总对账数量:<b>{{ totalStatementCount }}</b></span
-              >
-            </div> -->
-          </div>
-        </template>
-        <template v-slot:statementCount="scope">
-          <el-form-item
-            :prop="'flatList.' + scope.$index + '.statementCount'"
-            :rules="{
-              validator: (rule, value, cb) =>
-                validateStatementCount(rule, value, cb, scope.row),
-              trigger: 'change'
-            }"
+    <el-tabs v-model="activeTab" class="detail-tabs">
+      <!-- 单据明细:按单据展开的可编辑明细,提交以这里的行数据为准 -->
+      <el-tab-pane label="单据明细" name="detail">
+        <el-form ref="detailForm" :model="{ flatList }">
+          <ele-pro-table
+            ref="detailTable"
+            row-key="id"
+            :needPage="false"
+            :columns="columns"
+            max-height="500px"
+            :datasource="flatList"
+            :cache-key="cacheKeyUrl"
+            :selection.sync="selection"
+            class="time-form"
+            @header-dragend="handleHeaderDragend"
           >
-            <el-input
-              v-model="scope.row.statementCount"
-              type="number"
-              :disabled="true"
-              @input="handleCountChange(scope.row, scope.$index)"
-            />
-          </el-form-item>
-        </template>
+            <template v-slot:toolbar>
+              <div class="detail-toolbar">
+                <setAllValue :disabled="!selection.length" inputType="number" title="税率" valueKey="taxRate" @success="confirmBatchSet" />
+                <setAllValue :disabled="!selection.length" inputType="number" title="单价" valueKey="singlePrice" @success="confirmBatchSet" />
+                <setAllValue :disabled="!selection.length" inputType="number" title="折让比例" valueKey="discountRatio" @success="confirmBatchSet" />
+                <!-- <el-button
+                  v-if="!isView"
+                  type="primary"
+                  size="small"
+                  @click="batchSetTaxRate"
+                  >批量设置税率</el-button
+                >
+                <el-button
+                  v-if="!isView"
+                  type="primary"
+                  size="small"
+                  @click="batchSetPrice"
+                  >批量设置单价</el-button
+                >
+                <el-button
+                  v-if="!isView"
+                  type="primary"
+                  size="small"
+                  @click="batchSetDiscount"
+                  >批量设置折让比例</el-button
+                > -->
+                <!-- <div class="summary-info">
+                  <span
+                    >本次总对账金额:<b>{{ totalStatementAmount }}</b> 元</span
+                  >
+                  <span
+                    >本次总对账数量:<b>{{ totalStatementCount }}</b></span
+                  >
+                </div> -->
+              </div>
+            </template>
+            <template v-slot:statementCount="scope">
+              <el-form-item
+                :prop="'flatList.' + scope.$index + '.statementCount'"
+                :rules="{
+                  validator: (rule, value, cb) =>
+                    validateStatementCount(rule, value, cb, scope.row),
+                  trigger: 'change'
+                }"
+              >
+                <el-input
+                  v-model="scope.row.statementCount"
+                  type="number"
+                  :disabled="true"
+                  @input="handleCountChange(scope.row, scope.$index)"
+                />
+              </el-form-item>
+            </template>
 
-        <template v-slot:technologyRouteName="scope">
-          <el-input
-            v-model="scope.row.technologyRouteName"
-            placeholder="请选择"
-            :disabled="isView"
-            @click.native="openVersion(scope.$index)"
-          />
-        </template>
+            <template v-slot:technologyRouteName="scope">
+              <el-input
+                v-model="scope.row.technologyRouteName"
+                placeholder="请选择"
+                :disabled="isView"
+                @click.native="openVersion(scope.$index)"
+              />
+            </template>
 
-        <template v-slot:singlePrice="scope">
-          <el-form-item
-            :prop="'flatList.' + scope.$index + '.singlePrice'"
-            :rules="{
-              required: true,
-              message: '请输入单价',
-              trigger: 'change'
-            }"
-          >
-            <el-input
-              v-model="scope.row.singlePrice"
-              type="number"
-              :disabled="isView"
-              @input="handlePriceChange(scope.row, scope.$index)"
-            />
-            <!-- <div v-if="isPriceInvalid(scope.row)" class="price-warning">
-              单价无效,请核对商品价格
-            </div> -->
-          </el-form-item>
-        </template>
+            <template v-slot:singlePrice="scope">
+              <el-form-item
+                :prop="'flatList.' + scope.$index + '.singlePrice'"
+                :rules="{
+                  required: true,
+                  message: '请输入单价',
+                  trigger: 'change'
+                }"
+              >
+                <el-input
+                  v-model="scope.row.singlePrice"
+                  type="number"
+                  :disabled="isView"
+                  @input="handlePriceChange(scope.row, scope.$index)"
+                />
+                <!-- <div v-if="isPriceInvalid(scope.row)" class="price-warning">
+                  单价无效,请核对商品价格
+                </div> -->
+              </el-form-item>
+            </template>
 
-        <template v-slot:taxRate="scope">
-          <el-form-item
-            :prop="'flatList.' + scope.$index + '.taxRate'"
-            :rules="{
-              required: isTaxRate == 1,
-              message: '请输入税率',
-              trigger: 'change'
-            }"
-          >
-            <el-input
-              v-model="scope.row.taxRate"
-              type="number"
-              :disabled="isView"
-              class="tax-rate-input"
-              @input="handleTaxChange(scope.row, scope.$index)"
-            >
-              <template slot="append">%</template>
-            </el-input>
-          </el-form-item>
-        </template>
+            <template v-slot:taxRate="scope">
+              <el-form-item
+                :prop="'flatList.' + scope.$index + '.taxRate'"
+                :rules="{
+                  required: isTaxRate == 1,
+                  message: '请输入税率',
+                  trigger: 'change'
+                }"
+              >
+                <el-input
+                  v-model="scope.row.taxRate"
+                  type="number"
+                  :disabled="isView"
+                  class="tax-rate-input"
+                  @input="handleTaxChange(scope.row, scope.$index)"
+                >
+                  <template slot="append">%</template>
+                </el-input>
+              </el-form-item>
+            </template>
 
-        <template v-slot:discountRatio="scope">
-          <el-form-item
-            :prop="'flatList.' + scope.$index + '.discountRatio'"
-            :rules="{
-              required: true,
-              message: '请输入折让比例',
-              trigger: 'change'
-            }"
-          >
-            <el-input
-              v-model="scope.row.discountRatio"
-              type="number"
-              :min="0"
-              :max="100"
-              :disabled="isView"
-              @input="handleDiscountChange(scope.row, scope.$index)"
-            />
-          </el-form-item>
-        </template>
+            <template v-slot:discountRatio="scope">
+              <el-form-item
+                :prop="'flatList.' + scope.$index + '.discountRatio'"
+                :rules="{
+                  required: true,
+                  message: '请输入折让比例',
+                  trigger: 'change'
+                }"
+              >
+                <el-input
+                  v-model="scope.row.discountRatio"
+                  type="number"
+                  :min="0"
+                  :max="100"
+                  :disabled="isView"
+                  @input="handleDiscountChange(scope.row, scope.$index)"
+                />
+              </el-form-item>
+            </template>
 
-        <template v-slot:statementAmount="scope">
-          <span>{{ scope.row.statementAmount }}</span>
-        </template>
+            <template v-slot:statementAmount="scope">
+              <span>{{ scope.row.statementAmount }}</span>
+            </template>
 
-        <template v-slot:headerRequired="{ column }">
+            <template v-slot:headerRequired="{ column }">
               <span class="is-required">{{ column.label }}</span>
             </template>
 
-        <template v-slot:action="scope">
-          <el-link
-            v-if="!isView && queryDimension == 2"
-            type="danger"
-            :underline="false"
-            icon="el-icon-delete"
-            @click="removeRow(scope.$index)"
-            >删除</el-link
-          >
-          <span v-else>--</span>
-        </template>
-      </ele-pro-table>
-    </el-form>
+            <template v-slot:action="scope">
+              <el-link
+                v-if="!isView && queryDimension == 2"
+                type="danger"
+                :underline="false"
+                icon="el-icon-delete"
+                @click="removeRow(scope.$index)"
+                >删除</el-link
+              >
+              <span v-else>--</span>
+            </template>
+          </ele-pro-table>
+        </el-form>
+      </el-tab-pane>
+
+      <!-- 物品明细:按物品编码汇总,只读展示数量,不参与提交 -->
+      <el-tab-pane label="物品明细" name="item">
+        <ele-pro-table
+          ref="itemTable"
+          row-key="groupKey"
+          :needPage="false"
+          :columns="itemColumns"
+          max-height="500px"
+          :datasource="itemGroupList"
+          :cache-key="itemCacheKey"
+          show-summary
+          :summary-method="itemSummaries"
+          class="time-form"
+          @header-dragend="onScopedColumnDragend('item')"
+        >
+          <template v-slot:toolbar>
+            <div class="detail-toolbar">
+              <span class="item-summary">
+                共 <b>{{ itemGroupList.length }}</b> 种物品,本次对账数量合计:<b>{{ totalStatementCount }}</b>
+              </span>
+            </div>
+          </template>
+        </ele-pro-table>
+      </el-tab-pane>
+    </el-tabs>
 
     <!-- 批量设置弹窗 -->
     <el-dialog
@@ -226,6 +256,8 @@ import tabMixins from '@/mixins/tableColumnsMixin';
     data() {
       return {
         cacheKeyUrl: 'eom-202608171423-account-detail-table',
+        // 当前激活的页签:detail=单据明细(可编辑,提交依据),item=物品明细(只读汇总)
+        activeTab: 'detail',
         isTaxRate: 0,
         flatList: [],
         selection: [],
@@ -448,12 +480,173 @@ import tabMixins from '@/mixins/tableColumnsMixin';
           columns = columns.filter((item) => item.prop !== 'productOperateTime');
         }
         return this.applyColumnWidth(columns);
+      },
+      // 物品明细表的 cacheKey:与主表列配置隔离,避免两张表互相覆盖
+      itemCacheKey() {
+        return this.cacheKeyUrl + '_item';
+      },
+      // 物品明细列:不含任何价格/金额列
+      // (同一物品编码在不同单据上价格可能不同,汇总金额没有意义)
+      itemColumns() {
+        // 依赖 columnsVersion,列宽缓存变化时能触发重算
+        this.columnsVersion;
+        const columns = [
+          {
+            width: 60,
+            label: '序号',
+            type: 'index',
+            columnKey: 'index',
+            align: 'center',
+            fixed: 'left'
+          },
+          {
+            minWidth: 140,
+            prop: 'productCode',
+            label: '产品编码',
+            align: 'center',
+            showOverflowTooltip: true
+          },
+          {
+            minWidth: 160,
+            prop: 'productName',
+            label: '产品名称',
+            align: 'center',
+            showOverflowTooltip: true
+          },
+          {
+            minWidth: 100,
+            prop: 'specification',
+            label: '规格',
+            align: 'center',
+            showOverflowTooltip: true
+          },
+          {
+            minWidth: 100,
+            prop: 'modelType',
+            label: '型号',
+            align: 'center',
+            showOverflowTooltip: true
+          },
+          {
+            minWidth: 80,
+            prop: 'color',
+            label: '颜色',
+            align: 'center',
+            showOverflowTooltip: true
+          },
+          {
+            minWidth: 100,
+            prop: 'measuringUnit',
+            label: '计量单位',
+            align: 'center'
+          },
+          {
+            minWidth: 140,
+            prop: 'batchNo',
+            label: '批次号',
+            align: 'center',
+            showOverflowTooltip: true
+          },
+          {
+            minWidth: 140,
+            prop: 'technologyRouteName',
+            label: '工艺路线',
+            align: 'center',
+            showOverflowTooltip: true
+          },
+          {
+            minWidth: 140,
+            prop: 'statementCount',
+            label: '本次对账数量',
+            align: 'center'
+          },
+          {
+            minWidth: 120,
+            prop: 'totalCount',
+            label: this.queryDimension == 3 ? '收货数量' : this.queryDimension == 1 ? '发货数量' : '调拨数量',
+            align: 'center'
+          },
+          {
+            minWidth: 120,
+            prop: 'outInCount',
+            label: '出库数量',
+            align: 'center'
+          },
+          {
+            minWidth: 120,
+            prop: 'sendTotalWeight',
+            label: '出库重量',
+            align: 'center'
+          },
+          {
+            minWidth: 100,
+            prop: 'weightUnit',
+            label: '重量单位',
+            align: 'center'
+          }
+        ];
+        return this.applyScopedColumnWidth(columns, 'item');
+      },
+      // 物品明细数据:由单据明细按物品编码汇总而来
+      // 只读展示,不参与校验与提交;单据明细里改了数量,这里会跟着变
+      itemGroupList() {
+        const map = new Map();
+        const order = [];
+        this.flatList.forEach((row, index) => {
+          const code = (row.productCode || '').trim();
+          // 没有物品编码的行不参与合并,各自单独一行
+          const key = code || `__row_${index}`;
+          if (!map.has(key)) {
+            map.set(key, {
+              groupKey: key,
+              productCode: code,
+              productName: row.productName || '',
+              specification: row.specification || '',
+              modelType: row.modelType || '',
+              color: row.color || '',
+              measuringUnit: row.measuringUnit || '',
+              weightUnit: row.weightUnit || '',
+              batchNoList: [],
+              technologyRouteNameList: [],
+              statementCount: 0,
+              totalCount: 0,
+              outInCount: 0,
+              sendTotalWeight: 0
+            });
+            order.push(key);
+          }
+          const group = map.get(key);
+          group.statementCount += +row.statementCount || 0;
+          group.totalCount += +row.totalCount || 0;
+          group.outInCount += +row.outInCount || 0;
+          group.sendTotalWeight += +row.sendTotalWeight || 0;
+          // 批次号/工艺路线组内可能不一致,去重后合并展示
+          this.pushDistinct(group.batchNoList, row.batchNo);
+          this.pushDistinct(group.technologyRouteNameList, row.technologyRouteName);
+        });
+        return order.map((key) => {
+          const group = map.get(key);
+          // 求和后收敛小数,避免浮点误差
+          group.statementCount = this.roundNumber(group.statementCount);
+          group.totalCount = this.roundNumber(group.totalCount);
+          group.outInCount = this.roundNumber(group.outInCount);
+          group.sendTotalWeight = this.roundNumber(group.sendTotalWeight);
+          group.batchNo = group.batchNoList.join('、');
+          group.technologyRouteName = group.technologyRouteNameList.join('、');
+          delete group.batchNoList;
+          delete group.technologyRouteNameList;
+          return group;
+        });
       }
     },
     watch: {
       datasource: {
         handler(newVal, oldVal) {
           this.$nextTick(() => {
+            // 区分「重新查询拿到新数据」和「行内编辑回写」:
+            // 编辑回写的是同一批对象引用,只有新查询才把页签切回单据明细
+            const isFresh = (newVal || []).some((item) => !this.flatList.includes(item));
+            if (isFresh) this.activeTab = 'detail';
             // 直接基于原始 productList 扁平行初始化派生字段(单价/税率/折让比例默认值及本次对账金额)
             const list = (newVal || []).map((item) => this.buildRow(item, item.orderNo || '', false));
             this.flatList = list;
@@ -469,6 +662,8 @@ import tabMixins from '@/mixins/tableColumnsMixin';
       }
     },
     created() {
+      // 副表(物品明细)的列宽缓存单独一份,不与主表互相覆盖
+      this.loadScopedColumnWidth('item');
       parameterGetByCode({ code: 'eom_saleOrder_order-taxRate' }).then(
         (res) => {
           this.isTaxRate = res.value;
@@ -476,6 +671,30 @@ import tabMixins from '@/mixins/tableColumnsMixin';
       );
     },
     methods: {
+      // 数组去重追加(批次号/工艺路线合并展示用)
+      pushDistinct(list, value) {
+        if (value === undefined || value === null || value === '') return;
+        const text = String(value);
+        if (!list.includes(text)) list.push(text);
+      },
+      // 数值收敛到 4 位小数
+      roundNumber(value) {
+        const num = +value || 0;
+        return Math.round(num * 10000) / 10000;
+      },
+      // 物品明细合计行:数量列求和
+      itemSummaries({ columns, data }) {
+        const sumProps = ['statementCount', 'totalCount', 'outInCount', 'sendTotalWeight'];
+        return columns.map((column) => {
+          const prop = column.property;
+          if (prop === 'productCode') return '合计';
+          if (sumProps.includes(prop)) {
+            const total = data.reduce((pre, cur) => pre + (+cur[prop] || 0), 0);
+            return String(this.roundNumber(total));
+          }
+          return '';
+        });
+      },
       buildRow(item, orderNo, isReturn) {
         const outboundCount = +item.totalCount || 0;
         // 退货单数量为负
@@ -850,6 +1069,18 @@ import tabMixins from '@/mixins/tableColumnsMixin';
 
 <style scoped lang="scss">
   .account-detail-table {
+    .detail-tabs {
+      ::v-deep .el-tabs__header {
+        margin-bottom: 10px;
+      }
+    }
+    .item-summary {
+      font-size: 13px;
+      color: #606266;
+      b {
+        color: #f56c6c;
+      }
+    }
     .detail-toolbar {
       display: flex;
       justify-content: flex-start;