已添加8个文件
已修改4个文件
5144 ■■■■■ 文件已修改
src/api/basicData/customerFile.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesLedger.js 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesProcessFlowConfig.js 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 1453 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/OtherAmountMaintenanceButton.vue 278 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/ProcessFlowConfigSelectDialog.vue 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue 525 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/processCardPrint.js 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js 276 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/salesLabelPrint.js 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/components/salesOrderPrint.js 443 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 1329 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFile.js
@@ -9,6 +9,41 @@
        params: query
    })
}
// æŸ¥è¯¢åœ°åŒºåˆ—表
export function listCustomerRegions(query) {
    return request({
        url: '/customerRegions/list',
        method: 'get',
        params: query
    })
}
// æ–°å¢žåœ°åŒº
export function addCustomerRegions(data) {
    return request({
        url: '/customerRegions/add',
        method: 'post',
        data
    })
}
// ä¿®æ”¹åœ°åŒº
export function updateCustomerRegions(data) {
    return request({
        url: '/customerRegions/update',
        method: 'put',
        data
    })
}
// åˆ é™¤åœ°åŒº
export function delCustomerRegions(id) {
    return request({
        url: '/customerRegions/' + id,
        method: 'delete'
    })
}
// æŸ¥è¯¢å®¢æˆ·æ¡£æ¡ˆè¯¦ç»†
export function getCustomer(id) {
    return request({
src/api/salesManagement/salesLedger.js
@@ -151,4 +151,56 @@
    url: `/salesLedgerProductProcess/delete/${id}`,
    method: "delete",
  });
}
// é”€å”®å°è´¦-绑定工艺路线
export function saleProcessBind(data) {
  return request({
    url: "/sales/ledger/saleProcessBind",
    method: "post",
    data,
  });
}
// é”€å”®å°è´¦-查询订单已绑定工艺路线
export function getSaleProcessBindInfo(salesLedgerId) {
  return request({
    url: `/sales/ledger/salesProcess/${salesLedgerId}`,
    method: "get",
  });
}
// æ‰“印生产流程卡(成品)
export function getProcessCard(salesLedgerId) {
  return request({
    url: `/sales/ledger/processCard/${salesLedgerId}`,
    method: "get",
  });
}
// æ‰“印销售订单
export function getSalesOrder(salesLedgerId) {
  return request({
    url: `/sales/ledger/salesOrders/${salesLedgerId}`,
    method: "get",
  });
}
// æ‰“印销售发货单
export function getSalesInvoices(query) {
  const data =
    query && typeof query === "object" ? query : { salesLedgerId: query };
  return request({
    url: "/sales/ledger/salesInvoices",
    method: "post",
    data,
  });
}
// æ‰“印销售标签
export function getSalesLabel(salesLedgerId) {
  return request({
    url: `/sales/ledger/salesLabel/${salesLedgerId}`,
    method: "get",
  });
}
src/api/salesManagement/salesProcessFlowConfig.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
// å·¥è‰ºæµç¨‹é…ç½®ï¼ˆæ¨¡æ¿ï¼‰ä¸Žç»‘定到销售台账产品的接口
import request from "@/utils/request";
// å·¥è‰ºè·¯çº¿åˆ†é¡µåˆ—表
export function salesProcessFlowConfigList(query = {}) {
  return request({
    url: "/processRoute/page",
    method: "get",
    params: query,
  });
}
// å·¥è‰ºè·¯çº¿è¯¦æƒ…(兼容旧调用)
export function salesProcessFlowConfigGetById(configId) {
  return request({
    url: `/processRoute/${configId}`,
    method: "get",
  });
}
// æ–°å¢ž/编辑工艺路线
export function salesProcessFlowConfigUpsert(data) {
  return request({
    url: "/processRoute",
    method: "post",
    data: data,
  });
}
// åˆ é™¤å·¥è‰ºè·¯çº¿
export function salesProcessFlowConfigDelete(configId) {
  return request({
    url: `/processRoute/${configId}`,
    method: "delete",
  });
}
// è®¾ç½®é»˜è®¤å·¥è‰ºè·¯çº¿
export function salesProcessFlowConfigSetDefault(configId) {
  return request({
    url: `/processRoute/default/${configId}`,
    method: "put",
  });
}
// æŸ¥è¯¢å·¥è‰ºè·¯çº¿ä¸‹çš„工序
export function salesProcessFlowConfigItemList(routeId) {
  return request({
    url: "/processRouteItem/list",
    method: "get",
    params: { routeId },
  });
}
// æ–°å¢ž/修改工序
export function salesProcessFlowConfigItemUpsert(data) {
  return request({
    url: "/processRouteItem/",
    method: "post",
    data,
  });
}
// å·¥åºæŽ’序接口
export function salesProcessFlowConfigItemSort(data) {
  return request({
    url: "/processRouteItem/sort",
    method: "post",
    data,
  });
}
// åˆ é™¤å·¥åº
export function salesProcessFlowConfigItemDelete(itemId) {
  return request({
    url: `/processRouteItem/batchDelete/${itemId}`,
    method: "delete",
  });
}
// å°†æŸå¥—配置应用到具体产品(同步更新)
export function salesLedgerProductSetProcessFlowConfig(data) {
  return request({
    url: "/salesLedgerProduct/setProcessFlowConfig",
    method: "post",
    data: data,
  });
}
src/views/basicData/customerFile/index.vue
@@ -1,52 +1,158 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">客户名称:</span>
        <el-input v-model="searchForm.customerName"
                  style="width: 240px;margin-right: 10px"
                  placeholder="请输入"
                  @change="handleQuery"
                  clearable
                  :prefix-icon="Search" />
        <span class="search_title">客户分类:</span>
        <el-select v-model="searchForm.customerType"
                   placeholder="请选择"
                   style="width: 240px"
                   clearable
                   @change="handleQuery">
          <el-option label="零售客户"
                     value="零售客户" />
          <el-option label="进销商客户"
                     value="进销商客户" />
        </el-select>
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">搜索</el-button>
    <div class="customer-split">
      <div class="left-panel">
        <div class="left-header">
          <div class="left-title">地区</div>
          <div class="left-actions">
            <el-button type="primary"
                       size="small"
                       @click="openAddRegionDialog">新增</el-button>
            <el-button size="small"
                       @click="openEditRegionDialog">修改</el-button>
            <el-button type="danger"
                       plain
                       size="small"
                       @click="handleDeleteRegion">删除</el-button>
          </div>
        </div>
        <div class="left-search">
          <el-input v-model="regionKeyword"
                    placeholder="查询地区"
                    clearable
                    :prefix-icon="Search" />
        </div>
        <div class="left-list">
          <el-skeleton v-if="regionsLoading"
                       :rows="8"
                       animated />
          <template v-else>
            <div class="region-item"
                 :class="{ active: selectedRegionId === 0 }"
                 @click="selectRegion('')">全部</div>
            <el-tree ref="regionTreeRef"
                     :data="regionTreeData"
                     node-key="id"
                     :props="regionTreeProps"
                     :filter-node-method="filterRegionNode"
                     highlight-current
                     default-expand-all
                     :expand-on-click-node="false"
                     @node-click="handleRegionNodeClick" />
            <div v-if="regionTreeData.length === 0"
                 class="empty-tip">暂无地区</div>
          </template>
        </div>
      </div>
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增客户</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="info"
                   plain
                   icon="Upload"
                   @click="handleImport">导入</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
      <div class="right-panel">
        <div class="toolbar-card">
          <div class="search_form right-search-form">
          <div class="search-fields">
            <span class="search_title">客户名称:</span>
            <el-input v-model="searchForm.customerName"
                      style="width: 240px;margin-right: 10px"
                      placeholder="请输入"
                      @change="handleQuery"
                      clearable
                      :prefix-icon="Search" />
            <span class="search_title">客户分类:</span>
            <el-select v-model="searchForm.customerType"
                       placeholder="请选择"
                       style="width: 240px"
                       clearable
                       @change="handleQuery">
              <el-option label="零售客户"
                         value="零售客户" />
              <el-option label="进销商客户"
                         value="进销商客户" />
            </el-select>
            <el-button type="primary"
                       @click="handleQuery"
                       style="margin-left: 10px">搜索</el-button>
          </div>
          <div class="toolbar-divider"></div>
          <div class="action-buttons">
            <el-button type="primary"
                       @click="openForm('add')">新增客户</el-button>
            <el-button @click="handleOut">导出</el-button>
            <el-button type="info"
                       plain
                       icon="Upload"
                       @click="handleImport">导入</el-button>
            <el-button type="danger"
                       plain
                       @click="handleDelete">删除</el-button>
          </div>
          </div>
        </div>
        <div class="table_list table-card">
          <PIMTable rowKey="id"
                    :column="tableColumn"
                    :tableData="tableData"
                    :page="page"
                    :isSelection="true"
                    @selection-change="handleSelectionChange"
                    :tableLoading="tableLoading"
                    @pagination="pagination"></PIMTable>
        </div>
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"></PIMTable>
    </div>
    <el-dialog v-model="addRegionDialogVisible"
               title="新增地区"
               width="420px"
               @close="closeAddRegionDialog">
      <el-form :model="addRegionForm"
               label-width="90px">
        <el-form-item label="上级地区">
          <el-cascader v-model="addRegionForm.parentPath"
                       :options="regionTreeData"
                       :props="regionCascaderProps"
                       clearable
                       filterable
                       placeholder="不选则为顶级地区" />
        </el-form-item>
        <el-form-item label="地区名称">
          <el-input v-model="addRegionForm.regionsName"
                    placeholder="请输入"
                    clearable />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitAddRegion">确认</el-button>
          <el-button @click="closeAddRegionDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog v-model="editRegionDialogVisible"
               title="修改地区"
               width="420px"
               @close="closeEditRegionDialog">
      <el-form :model="editRegionForm"
               label-width="90px">
        <el-form-item label="上级地区">
          <el-input :value="editRegionParentLabel"
                    disabled />
        </el-form-item>
        <el-form-item label="地区名称">
          <el-input v-model="editRegionForm.regionsName"
                    placeholder="请输入"
                    clearable />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitEditRegion">确认</el-button>
          <el-button @click="closeEditRegionDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'add' ? '新增客户信息' : '编辑客户信息'"
               width="70%"
@@ -76,19 +182,47 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="公司地址:"
            <el-form-item label="客户地区:"
                          prop="regions">
              <el-cascader v-model="formRegionPath"
                           :options="regionTreeData"
                           :props="regionCascaderProps"
                           clearable
                           filterable
                           style="width: 100%"
                           placeholder="请选择"
                           @change="handleFormRegionChange" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户地址:"
                          prop="companyAddress">
              <el-input v-model="form.companyAddress"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="公司电话:"
            <el-form-item label="客户电话:"
                          prop="companyPhone">
              <el-input v-model="form.companyPhone"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户分类:"
                          prop="customerType">
              <el-select v-model="form.customerType"
                         placeholder="请选择"
                         clearable>
                <el-option label="零售客户"
                           value="零售客户" />
                <el-option label="进销商客户"
                           value="进销商客户" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -117,19 +251,6 @@
              <el-input v-model="form.bankCode"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户分类:"
                          prop="customerType">
              <el-select v-model="form.customerType"
                         placeholder="请选择"
                         clearable>
                <el-option label="零售客户"
                           value="零售客户" />
                <el-option label="进销商客户"
                           value="进销商客户" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -240,130 +361,10 @@
        </div>
      </template>
    </el-dialog>
    <!-- å›žè®¿æé†’对话框 -->
    <el-dialog title="回访提醒"
               v-model="reminderDialogVisible"
               width="500px"
               @close="closeReminderDialog">
      <el-form :model="reminderForm"
               label-width="100px"
               :rules="reminderRules"
               ref="reminderFormRef">
        <el-form-item label="客户名称:">
          <el-input v-model="reminderForm.customerName"
                    disabled />
        </el-form-item>
        <el-form-item label="提醒开关:">
          <el-switch v-model="reminderForm.reminderSwitch" />
        </el-form-item>
        <el-form-item label="提醒内容:"
                      prop="reminderContent">
          <el-input v-model="reminderForm.reminderContent"
                    type="textarea"
                    :maxlength="100"
                    show-word-limit
                    placeholder="请输入提醒内容" />
        </el-form-item>
        <el-form-item label="提醒时间:"
                      prop="reminderTime">
          <el-date-picker v-model="reminderForm.reminderTime"
                          type="datetime"
                          value-format="YYYY-MM-DD HH:mm:ss"
                          format="YYYY-MM-DD HH:mm:ss"
                          placeholder="请选择提醒时间"
                          style="width: 100%" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitReminderForm">确认</el-button>
          <el-button @click="closeReminderDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- æ·»åŠ /修改洽谈进度对话框 -->
    <el-dialog :title="negotiationForm.editIndex !== undefined ? '修改进度' : '添加进度'"
               v-model="negotiationDialogVisible"
               width="600px"
               @close="closeNegotiationDialog">
      <el-form :model="negotiationForm"
               label-width="100px"
               :rules="negotiationRules"
               ref="negotiationFormRef">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="跟进方式:"
                          prop="followUpMethod">
              <el-select v-model="negotiationForm.followUpMethod"
                         placeholder="请选择"
                         style="width: 100%">
                <el-option label="电话"
                           value="电话" />
                <el-option label="邮件"
                           value="邮件" />
                <el-option label="上门"
                           value="上门" />
                <el-option label="微信"
                           value="微信" />
                <el-option label="其他"
                           value="其他" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="跟进程度:"
                          prop="followUpLevel">
              <el-select v-model="negotiationForm.followUpLevel"
                         placeholder="请选择"
                         style="width: 100%">
                <el-option label="潜在客户"
                           value="潜在客户" />
                <el-option label="初次拜访"
                           value="初次拜访" />
                <el-option label="多次拜访"
                           value="多次拜访" />
                <el-option label="意向客户"
                           value="意向客户" />
                <el-option label="已签约客户"
                           value="已签约客户" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="跟进时间:"
                          prop="followUpTime">
              <el-date-picker v-model="negotiationForm.followUpTime"
                              type="datetime"
                              value-format="YYYY-MM-DD HH:mm:ss"
                              format="YYYY-MM-DD HH:mm:ss"
                              placeholder="请选择"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="跟进人:">
              <el-input v-model="negotiationForm.followerUserName"
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="内容:"
                      prop="content">
          <el-input v-model="negotiationForm.content"
                    type="textarea"
                    :rows="4"
                    placeholder="请输入" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitNegotiationForm">确认</el-button>
          <el-button @click="closeNegotiationDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <!--
    å›žè®¿æé†’ / æ´½è°ˆè¿›åº¦åŠŸèƒ½ï¼ˆå…¥å£åŠå¼¹çª—ï¼‰å·²æŒ‰éœ€æ±‚æ³¨é‡Šã€‚
    å¦‚需恢复:放开操作列入口 + å–消本段注释 + æ¢å¤ script ä¸­ç›¸å…³å˜é‡/方法/接口引入。
    -->
    <!-- å®¢æˆ·è¯¦æƒ…对话框 -->
    <el-dialog title="客户详情"
               v-model="detailDialogVisible"
@@ -396,7 +397,7 @@
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">公司电话:</span>
                <span class="info-label">客户电话:</span>
                <span class="info-value">{{ detailForm.companyPhone }}</span>
              </div>
            </el-col>
@@ -404,52 +405,60 @@
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">公司地址:</span>
                <span class="info-label">客户地区:</span>
                <span class="info-value">{{ detailForm.regionsName || detailForm.regions }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">客户地址:</span>
                <span class="info-value">{{ detailForm.companyAddress }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">银行基本户:</span>
                <span class="info-value">{{ detailForm.basicBankAccount }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">银行账号:</span>
                <span class="info-value">{{ detailForm.bankAccount }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">开户行号:</span>
                <span class="info-value">{{ detailForm.bankCode }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">联系人:</span>
                <span class="info-value">{{ detailForm.contactPerson }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">联系电话:</span>
                <span class="info-value">{{ detailForm.contactPhone }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">维护人:</span>
                <span class="info-value">{{ detailForm.maintainer }}</span>
              </div>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">维护时间:</span>
@@ -459,169 +468,38 @@
          </el-row>
        </div>
      </div>
      <!-- æ´½è°ˆè¿›åº¦è®°å½• -->
      <div class="detail-section">
        <div class="section-header">
          <h3 class="section-title">洽谈进度记录</h3>
          <el-button type="primary"
                     size="small"
                     @click="openNegotiationDialog(detailForm)">
            æ·»åŠ è¿›åº¦
          </el-button>
        </div>
        <el-table :data="negotiationRecords"
                  border
                  style="width: 100%">
          <el-table-column prop="followUpTime"
                           label="跟进时间"
                           width="160" />
          <el-table-column prop="followUpMethod"
                           label="跟进方式"
                           width="100" />
          <el-table-column prop="followUpLevel"
                           label="跟进程度" />
          <el-table-column prop="followerUserName"
                           label="跟进人"
                           width="100" />
          <el-table-column prop="content"
                           label="内容"
                           show-overflow-tooltip />
          <el-table-column label="附件"
                           width="100"
                           align="center">
            <template #default="{ row }">
              <el-button type="info"
                         link
                         @click="openAttachmentDialog(row)">
                <el-icon>
                  <Paperclip />
                </el-icon>
                é™„ä»¶
                <!-- {{ row.fileList && row.fileList.length > 0 ? row.fileList.length : '上传' }} -->
              </el-button>
            </template>
          </el-table-column>
          <el-table-column label="操作"
                           width="150"
                           align="center">
            <template #default="{ row, $index }">
              <el-button type="primary"
                         link
                         @click="editNegotiationRecord(row, $index)">
                ä¿®æ”¹
              </el-button>
              <el-button type="danger"
                         link
                         @click="deleteNegotiationRecord(row, $index)">
                åˆ é™¤
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <div v-if="negotiationRecords.length === 0"
             class="no-records">
          æš‚无洽谈进度记录
        </div>
      </div>
      <!--
      æ´½è°ˆè¿›åº¦è®°å½•(含附件/修改/删除)已按需求整体注释。
      å¦‚需恢复:取消本段注释,并恢复 script ä¸­ç›¸å…³å˜é‡/方法/接口引入。
      -->
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeDetailDialog">关闭</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件上传弹窗 -->
    <el-dialog title="附件管理"
               v-model="attachmentDialogVisible"
               width="600px"
               @close="closeAttachmentDialog">
      <div class="attachment-section">
        <div class="upload-area">
          <el-upload ref="attachmentUploadRef"
                     :action="getAttachmentUploadUrl()"
                     :headers="attachmentUploadHeaders"
                     :file-list="currentAttachmentList"
                     :on-success="handleAttachmentSuccess"
                     :on-error="handleAttachmentError"
                     :on-remove="handleAttachmentRemove"
                     :before-upload="beforeAttachmentUpload"
                     multiple
                     :limit="10"
                     name="files">
            <el-button type="primary">
              <el-icon>
                <Upload />
              </el-icon>
              ä¸Šä¼ é™„ä»¶
            </el-button>
            <template #tip>
              <div class="el-upload__tip">
                æ”¯æŒä¸Šä¼ å›¾ç‰‡ã€æ–‡æ¡£ç­‰æ–‡ä»¶ï¼Œå•个文件不超过50MB
              </div>
            </template>
          </el-upload>
        </div>
        <div v-if="currentAttachmentList.length > 0"
             class="attachment-list">
          <h4>已上传附件:</h4>
          <el-table :data="currentAttachmentList"
                    border
                    size="small">
            <el-table-column prop="name"
                             label="文件名"
                             show-overflow-tooltip />
            <el-table-column prop="size"
                             label="大小"
                             width="100">
              <template #default="{ row }">
                {{ formatFileSize(row.size) }}
              </template>
            </el-table-column>
            <el-table-column label="操作"
                             width="120"
                             align="center">
              <template #default="{ row, $index }">
                <el-button type="primary"
                           link
                           @click="downloadAttachment(row)">
                  ä¸‹è½½
                </el-button>
                <el-button type="danger"
                           link
                           @click="deleteAttachment(row, $index)">
                  åˆ é™¤
                </el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <div v-else
             class="no-attachment">
          æš‚无附件
        </div>
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeAttachmentDialog">关闭</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- é™„件管理功能已随洽谈进度整体注释 -->
  </div>
</template>
<script setup>
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
  import { onMounted, ref, reactive, getCurrentInstance, toRefs, computed, watch } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import {
    addCustomer,
    delCustomer,
    getCustomer,
    listCustomer,
    listCustomerRegions,
    addCustomerRegions,
    updateCustomerRegions,
    delCustomerRegions,
    updateCustomer,
    addCustomerFollow,
    updateCustomerFollow,
    delCustomerFollow,
    addReturnVisit,
    getReturnVisit,
    // addCustomerFollow,
    // updateCustomerFollow,
    // delCustomerFollow,
    // addReturnVisit,
    // getReturnVisit,
  } from "@/api/basicData/customerFile.js";
  import { ElMessageBox } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
@@ -630,54 +508,55 @@
  const { proxy } = getCurrentInstance();
  const userStore = useUserStore();
  // å›žè®¿æé†’相关
  const reminderDialogVisible = ref(false);
  const reminderFormRef = ref();
  const currentCustomerId = ref();
  const reminderForm = reactive({
    customerName: "",
    reminderSwitch: false,
    reminderContent: "",
    reminderTime: "",
  });
  const reminderRules = {
    reminderContent: [
      { required: true, message: "请输入提醒内容", trigger: "blur" },
    ],
    reminderTime: [
      { required: true, message: "请选择提醒时间", trigger: "change" },
    ],
  };
  // æ´½è°ˆè¿›åº¦ç›¸å…³
  const negotiationDialogVisible = ref(false);
  const negotiationFormRef = ref();
  const negotiationForm = reactive({
    customerName: "",
    customerId: "",
    followUpMethod: "",
    followUpLevel: "",
    followUpTime: "",
    followerUserName: "",
    content: "",
  });
  const negotiationRules = {
    followUpMethod: [
      { required: true, message: "请选择跟进方式", trigger: "change" },
    ],
    followUpLevel: [
      { required: true, message: "请选择跟进程度", trigger: "change" },
    ],
    followUpTime: [
      { required: true, message: "请选择跟进时间", trigger: "change" },
    ],
    content: [{ required: true, message: "请输入内容", trigger: "blur" }],
  };
  // å›žè®¿æé†’/洽谈进度相关(已按需求整体注释)
  // const reminderDialogVisible = ref(false);
  // const reminderFormRef = ref();
  // const currentCustomerId = ref();
  // const reminderForm = reactive({
  //   customerName: "",
  //   reminderSwitch: false,
  //   reminderContent: "",
  //   reminderTime: "",
  // });
  // const reminderRules = {
  //   reminderContent: [
  //     { required: true, message: "请输入提醒内容", trigger: "blur" },
  //   ],
  //   reminderTime: [
  //     { required: true, message: "请选择提醒时间", trigger: "change" },
  //   ],
  // };
  //
  // const negotiationDialogVisible = ref(false);
  // const negotiationFormRef = ref();
  // const negotiationForm = reactive({
  //   customerName: "",
  //   customerId: "",
  //   followUpMethod: "",
  //   followUpLevel: "",
  //   followUpTime: "",
  //   followerUserName: "",
  //   content: "",
  // });
  // const negotiationRules = {
  //   followUpMethod: [
  //     { required: true, message: "请选择跟进方式", trigger: "change" },
  //   ],
  //   followUpLevel: [
  //     { required: true, message: "请选择跟进程度", trigger: "change" },
  //   ],
  //   followUpTime: [
  //     { required: true, message: "请选择跟进时间", trigger: "change" },
  //   ],
  //   content: [{ required: true, message: "请输入内容", trigger: "blur" }],
  // };
  // è¯¦æƒ…相关
  const detailDialogVisible = ref(false);
  const detailForm = reactive({
    customerName: "",
    regionsName: "",
    regions: "",
    customerType: "",
    taxpayerIdentificationNumber: "",
    companyPhone: "",
@@ -692,21 +571,13 @@
  });
  const negotiationRecords = ref([]);
  // é™„件相关
  const attachmentDialogVisible = ref(false);
  const attachmentUploadRef = ref();
  const currentAttachmentList = ref([]);
  const currentFollowRecord = ref({});
  const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() };
  // åŠ¨æ€æž„å»ºä¸Šä¼ URL
  const getAttachmentUploadUrl = () => {
    const baseUrl =
      import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload";
    return currentFollowRecord.value.id
      ? `${baseUrl}/${currentFollowRecord.value.id}`
      : baseUrl;
  };
  // é™„件相关(随洽谈进度整体注释)
  // const attachmentDialogVisible = ref(false);
  // const attachmentUploadRef = ref();
  // const currentAttachmentList = ref([]);
  // const currentFollowRecord = ref({});
  // const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() };
  // const getAttachmentUploadUrl = () => {};
  const tableColumn = ref([
    {
@@ -718,6 +589,11 @@
      label: "客户名称",
      prop: "customerName",
      width: 220,
    },
    {
      label: "客户地区",
      prop: "regionsName",
      width: 120,
    },
    {
      label: "纳税人识别码",
@@ -777,7 +653,7 @@
      label: "操作",
      align: "center",
      fixed: "right",
      width: 290,
      width: 120,
      operation: [
        {
          name: "编辑",
@@ -786,20 +662,20 @@
            openForm("edit", row);
          },
        },
                {
                    name: "添加洽谈进度",
                    type: "text",
                    clickFun: row => {
                        openNegotiationDialog(row);
                    },
                },
        {
          name: "回访提醒",
          type: "text",
          clickFun: row => {
            openReminderDialog(row);
          },
        },
        // {
        //   name: "添加洽谈进度",
        //   type: "text",
        //   clickFun: row => {
        //     openNegotiationDialog(row);
        //   },
        // },
        // {
        //   name: "回访提醒",
        //   type: "text",
        //   clickFun: row => {
        //     openReminderDialog(row);
        //   },
        // },
                {
                    name: "详情",
                    type: "text",
@@ -837,11 +713,16 @@
    searchForm: {
      customerName: "",
      customerType: "",
      regions: "",
      regionsId: "",
    },
    form: {
      customerName: "",
      taxpayerIdentificationNumber: "",
      companyAddress: "",
      regions: "",
      regionsId: "",
      regionsld: "",
      companyPhone: "",
      contactPerson: "",
      contactPhone: "",
@@ -858,6 +739,7 @@
        { required: true, message: "请输入", trigger: "blur" },
      ],
      companyAddress: [{ required: true, message: "请输入", trigger: "blur" }],
      regions: [{ required: true, message: "请选择客户地区", trigger: "change" }],
      companyPhone: [{ required: true, message: "请输入", trigger: "blur" }],
      // contactPerson: [{ required: true, message: "请输入", trigger: "blur" }],
      // contactPhone: [{ required: true, message: "请输入", trigger: "blur" }],
@@ -927,6 +809,189 @@
    },
  });
  const { searchForm, form, rules } = toRefs(data);
  // å·¦ä¾§åœ°åŒºæ 
  const regionTreeRef = ref();
  const regionsLoading = ref(false);
  const regionTreeData = ref([]);
  const regionKeyword = ref("");
  const selectedRegionId = ref(0); // 0 è¡¨ç¤ºå…¨éƒ¨
  const selectedRegionNode = ref(null);
  const formRegionPath = ref([]);
  const addRegionDialogVisible = ref(false);
  const editRegionDialogVisible = ref(false);
  const addRegionForm = reactive({ parentPath: [], regionsName: "" });
  const editRegionForm = reactive({ id: undefined, parentId: 0, regionsName: "" });
  const regionTreeProps = { label: "label", children: "children" };
  const regionCascaderProps = {
    value: "id",
    label: "label",
    children: "children",
    checkStrictly: true,
    emitPath: true,
  };
  const regionNodeMap = computed(() => {
    const map = new Map();
    const walk = list => {
      (list || []).forEach(node => {
        map.set(node.id, node);
        walk(node.children || []);
      });
    };
    walk(regionTreeData.value);
    return map;
  });
  const editRegionParentLabel = computed(() => {
    if (!editRegionForm.parentId) return "顶级地区";
    return regionNodeMap.value.get(editRegionForm.parentId)?.label || "未知";
  });
  const normalizeRegionTree = list => {
    return (list || []).map(item => ({
      ...item,
      label: item.label || item.regionsName || "",
      regionsName: item.regionsName || item.label || "",
      children: normalizeRegionTree(item.children || []),
    }));
  };
  const fetchRegions = async () => {
    regionsLoading.value = true;
    try {
      const res = await listCustomerRegions({});
      const list = res?.data ?? res?.rows ?? res ?? [];
      regionTreeData.value = Array.isArray(list) ? normalizeRegionTree(list) : [];
    } catch (e) {
      console.error("地区查询失败:", e);
      regionTreeData.value = [];
    } finally {
      regionsLoading.value = false;
    }
  };
  const filterRegionNode = (value, data) => {
    if (!value) return true;
    return (data.label || "").includes(value);
  };
  const selectRegion = regionName => {
    selectedRegionId.value = 0;
    selectedRegionNode.value = null;
    searchForm.value.regions = regionName || "";
    searchForm.value.regionsId = "";
    handleQuery();
  };
  const handleRegionNodeClick = data => {
    selectedRegionId.value = data.id;
    selectedRegionNode.value = data;
    searchForm.value.regions = data.regionsName || data.label || "";
    searchForm.value.regionsId = data.id;
    handleQuery();
  };
  const openAddRegionDialog = () => {
    addRegionForm.parentPath = [];
    addRegionForm.regionsName = "";
    addRegionDialogVisible.value = true;
  };
  const closeAddRegionDialog = () => {
    addRegionDialogVisible.value = false;
    addRegionForm.parentPath = [];
    addRegionForm.regionsName = "";
  };
  const submitAddRegion = async () => {
    const name = (addRegionForm.regionsName || "").trim();
    if (!name) return proxy.$modal.msgWarning("请输入地区名称");
    const parentPath = addRegionForm.parentPath || [];
    const parentId = parentPath.length ? parentPath[parentPath.length - 1] : 0;
    await addCustomerRegions({ parentId, regionsName: name });
    proxy.$modal.msgSuccess("新增成功");
    await fetchRegions();
    closeAddRegionDialog();
  };
  const openEditRegionDialog = () => {
    if (!selectedRegionNode.value || selectedRegionId.value === 0) {
      return proxy.$modal.msgWarning("请先选择要修改的地区");
    }
    editRegionForm.id = selectedRegionNode.value.id;
    editRegionForm.parentId = selectedRegionNode.value.parentId || 0;
    editRegionForm.regionsName =
      selectedRegionNode.value.regionsName || selectedRegionNode.value.label || "";
    editRegionDialogVisible.value = true;
  };
  const closeEditRegionDialog = () => {
    editRegionDialogVisible.value = false;
    editRegionForm.id = undefined;
    editRegionForm.parentId = 0;
    editRegionForm.regionsName = "";
  };
  const submitEditRegion = async () => {
    const name = (editRegionForm.regionsName || "").trim();
    if (!name) return proxy.$modal.msgWarning("请输入地区名称");
    await updateCustomerRegions({
      id: editRegionForm.id,
      parentId: editRegionForm.parentId,
      regionsName: name,
    });
    proxy.$modal.msgSuccess("修改成功");
    await fetchRegions();
    closeEditRegionDialog();
  };
  const handleDeleteRegion = () => {
    if (!selectedRegionNode.value || selectedRegionId.value === 0) {
      return proxy.$modal.msgWarning("请先选择要删除的地区");
    }
    ElMessageBox.confirm("删除后不可恢复,是否继续?", "删除地区", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(async () => {
        await delCustomerRegions(selectedRegionNode.value.id);
        proxy.$modal.msgSuccess("删除成功");
        selectRegion("");
        await fetchRegions();
      })
      .catch(() => {});
  };
  const handleFormRegionChange = value => {
    const ids = value || [];
    if (!ids.length) {
      form.value.regions = "";
      form.value.regionsId = "";
      form.value.regionsld = "";
      return;
    }
    const lastId = ids[ids.length - 1];
    form.value.regions = regionNodeMap.value.get(lastId)?.regionsName || "";
    form.value.regionsId = lastId;
    form.value.regionsld = lastId;
  };
  const findRegionPathByName = (tree, targetName, parentPath = []) => {
    for (const item of tree || []) {
      const currentPath = [...parentPath, item.id];
      if ((item.regionsName || item.label) === targetName) {
        return currentPath;
      }
      const childResult = findRegionPathByName(item.children || [], targetName, currentPath);
      if (childResult.length) return childResult;
    }
    return [];
  };
  const findRegionPathById = (tree, targetId, parentPath = []) => {
    if (targetId === undefined || targetId === null || targetId === "") return [];
    for (const item of tree || []) {
      const currentPath = [...parentPath, item.id];
      if (String(item.id) === String(targetId)) {
        return currentPath;
      }
      const childResult = findRegionPathById(item.children || [], targetId, currentPath);
      if (childResult.length) return childResult;
    }
    return [];
  };
  const addNewContact = () => {
    formYYs.value.contactList.push({
      contactPerson: "",
@@ -980,6 +1045,9 @@
  const openForm = (type, row) => {
    operationType.value = type;
    form.value = {};
    formRegionPath.value = [];
    form.value.regionsId = "";
    form.value.regionsld = "";
    form.value.maintainer = userStore.nickName;
    formYYs.value.contactList = [
      {
@@ -994,6 +1062,23 @@
    if (type === "edit") {
      getCustomer(row.id).then(res => {
        form.value = { ...res.data };
        const regionIdForEdit = form.value.regionsId || form.value.regionsld;
        formRegionPath.value = findRegionPathById(regionTreeData.value, regionIdForEdit);
        if (!formRegionPath.value.length) {
          formRegionPath.value = findRegionPathByName(
            regionTreeData.value,
            form.value.regionsName || form.value.regions || ""
          );
        }
        const selectedRegionId =
          formRegionPath.value.length > 0
            ? formRegionPath.value[formRegionPath.value.length - 1]
            : "";
        if (selectedRegionId && !form.value.regions) {
          form.value.regions = regionNodeMap.value.get(selectedRegionId)?.regionsName || "";
        }
        form.value.regionsId = form.value.regionsId || selectedRegionId;
        form.value.regionsld = form.value.regionsld || form.value.regionsId || selectedRegionId;
        formYYs.value.contactList = res.data.contactPerson
          .split(",")
          .map((item, index) => {
@@ -1029,6 +1114,10 @@
    form.value.contactPhone = formYYs.value.contactList
      .map(item => item.contactPhone)
      .join(",");
    if (!form.value.regionsId && formRegionPath.value.length) {
      form.value.regionsId = formRegionPath.value[formRegionPath.value.length - 1];
    }
    form.value.regionsld = form.value.regionsId || "";
    addCustomer(form.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
@@ -1043,6 +1132,10 @@
    form.value.contactPhone = formYYs.value.contactList
      .map(item => item.contactPhone)
      .join(",");
    if (!form.value.regionsId && formRegionPath.value.length) {
      form.value.regionsId = formRegionPath.value[formRegionPath.value.length - 1];
    }
    form.value.regionsld = form.value.regionsId || "";
    updateCustomer(form.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
      closeDia();
@@ -1052,6 +1145,7 @@
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    formRegionPath.value = [];
    dialogFormVisible.value = false;
  };
  // å¯¼å‡º
@@ -1106,168 +1200,17 @@
      });
  };
  // æ‰“开回访提醒弹窗
  const openReminderDialog = row => {
    currentCustomerId.value = row.id;
    reminderForm.customerName = row.customerName;
    reminderForm.reminderSwitch = false;
    reminderForm.reminderContent = "";
    reminderForm.reminderTime = "";
    // å°è¯•获取已有的回访提醒
    getReturnVisit(row.id)
      .then(res => {
        if (res.code === 200 && res.data) {
          reminderForm.reminderSwitch = res.data.isEnabled === 1;
          reminderForm.reminderContent = res.data.content;
          reminderForm.reminderTime = res.data.reminderTime;
          reminderForm.id = res.data.id;
        }
      })
      .catch(error => {
        console.error("获取回访提醒失败:", error);
      });
    reminderDialogVisible.value = true;
  };
  // å…³é—­å›žè®¿æé†’弹窗
  const closeReminderDialog = () => {
    proxy.resetForm("reminderFormRef");
    reminderDialogVisible.value = false;
  };
  const submitvalue = ref({});
  // æäº¤å›žè®¿æé†’
  const submitReminderForm = () => {
    console.log("提交回访提醒数据:", userStore.id, userStore);
    proxy.$refs.reminderFormRef.validate(valid => {
      if (valid) {
        if (reminderForm.id) {
          submitvalue.value = {
            id: reminderForm.id,
            customerId: currentCustomerId.value,
            isEnabled: reminderForm.reminderSwitch ? 1 : 0,
            content: reminderForm.reminderContent,
            reminderTime: reminderForm.reminderTime,
            remindUserId: userStore.id,
          };
        } else {
          submitvalue.value = {
            customerId: currentCustomerId.value,
            isEnabled: reminderForm.reminderSwitch ? 1 : 0,
            content: reminderForm.reminderContent,
            reminderTime: reminderForm.reminderTime,
            remindUserId: userStore.id,
          };
        }
        console.log("提交回访提醒数据:", submitvalue.value);
        // è°ƒç”¨æŽ¥å£
        addReturnVisit(submitvalue.value)
          .then(res => {
            if (res.code === 200) {
              proxy.$modal.msgSuccess("回访提醒设置成功");
              closeReminderDialog();
            } else {
              proxy.$modal.msgError(res.msg || "设置失败");
            }
          })
          .catch(error => {
            console.error("设置回访提醒失败:", error);
            proxy.$modal.msgError("设置失败");
          });
      }
    });
  };
  // æ‰“开洽谈进度弹窗
  const openNegotiationDialog = row => {
    negotiationForm.customerName = row.customerName;
    negotiationForm.customerId = row.id;
    negotiationForm.followUpMethod = "";
    negotiationForm.followUpLevel = "";
    negotiationForm.followUpTime = "";
    negotiationForm.followerUserName = userStore.nickName; // é»˜è®¤å½“前登录人
    negotiationForm.content = "";
    // {
    //     "customerId": 152,
    //     "followUpMethod": "电话沟通",
    //     "followUpLevel": "没有意向",
    //     "followUpTime": "2026-03-04T15:30:00",
    //     "followerUserName": "管理员账号",
    //     "content": "111"
    // }
    negotiationDialogVisible.value = true;
  };
  // å…³é—­æ´½è°ˆè¿›åº¦å¼¹çª—
  const closeNegotiationDialog = () => {
    proxy.resetForm("negotiationFormRef");
    // æ¸…除编辑状态
    delete negotiationForm.editIndex;
    delete negotiationForm.id;
    negotiationDialogVisible.value = false;
  };
  // æäº¤æ´½è°ˆè¿›åº¦
  const submitNegotiationForm = () => {
    proxy.$refs.negotiationFormRef.validate(valid => {
      if (valid) {
        // åˆ¤æ–­æ˜¯æ–°å¢žè¿˜æ˜¯ä¿®æ”¹
        const isEdit = negotiationForm.editIndex !== undefined;
        if (isEdit) {
          // ä¿®æ”¹æ“ä½œ
          console.log("修改洽谈进度数据:", negotiationForm);
          // è¿™é‡Œå¯ä»¥è°ƒç”¨æ›´æ–°æŽ¥å£
          // å®žé™…项目中需要根据后端接口进行调整
          // ç¤ºä¾‹ï¼šupdateCustomerFollow(negotiationForm).then(res => {
          //   // æ›´æ–°æœ¬åœ°æ•°æ®
          //   const index = negotiationForm.editIndex;
          //   negotiationRecords.value[index] = {
          //     followUpTime: negotiationForm.followUpTime,
          //     followUpMethod: negotiationForm.followUpMethod,
          //     followUpLevel: negotiationForm.followUpLevel,
          //     followerUserName: negotiationForm.followerUserName,
          //     content: negotiationForm.content,
          //     id: negotiationForm.id,
          //   };
          //   proxy.$modal.msgSuccess("修改成功");
          //   closeNegotiationDialog();
          // });
          updateCustomerFollow(negotiationForm).then(res => {
            // æ›´æ–°æœ¬åœ°æ•°æ®
            getCustomer(negotiationForm.customerId).then(res => {
              // æ›´æ–°æœ¬åœ°æ•°æ®
              negotiationRecords.value = res.data.followUpList || [];
            });
          });
          proxy.$modal.msgSuccess("修改成功");
          closeNegotiationDialog();
        } else {
          // æ–°å¢žæ“ä½œ
          console.log("提交洽谈进度数据:", negotiationForm);
          addCustomerFollow(negotiationForm).then(res => {
            // æ·»åŠ æˆåŠŸåŽæ›´æ–°è¯¦æƒ…é¡µé¢çš„è¿›åº¦è®°å½•
            const newRecord = {
              followUpTime: negotiationForm.followUpTime,
              followUpMethod: negotiationForm.followUpMethod,
              followUpLevel: negotiationForm.followUpLevel,
              followerUserName: negotiationForm.followerUserName,
              content: negotiationForm.content,
            };
            negotiationRecords.value.unshift(newRecord);
            proxy.$modal.msgSuccess("提交成功");
            closeNegotiationDialog();
            getList();
          });
        }
      }
    });
  };
  /*
   * å›žè®¿æé†’ / æ´½è°ˆè¿›åº¦åŠŸèƒ½ï¼ˆå…¥å£åŠå¼¹çª—ï¼‰å·²æŒ‰éœ€æ±‚æ³¨é‡Šï¼Œä»¥ä¸‹æ–¹æ³•æš‚ä¸å¯ç”¨ï¼š
   * - openReminderDialog / closeReminderDialog / submitReminderForm
   * - openNegotiationDialog / closeNegotiationDialog / submitNegotiationForm
   */
  // const openReminderDialog = row => {};
  // const closeReminderDialog = () => {};
  // const submitReminderForm = () => {};
  // const openNegotiationDialog = row => {};
  // const closeNegotiationDialog = () => {};
  // const submitNegotiationForm = () => {};
  // æ‰“开详情弹窗
  const openDetailDialog = row => {
@@ -1288,190 +1231,11 @@
    detailDialogVisible.value = false;
  };
  // ä¿®æ”¹æ´½è°ˆè®°å½•
  const editNegotiationRecord = (row, index) => {
    // å°†å½“前记录数据填充到表单
    Object.assign(negotiationForm, {
      customerName: row.customerName,
      customerId: row.customerId,
      followUpMethod: row.followUpMethod,
      followUpLevel: row.followUpLevel,
      followUpTime: row.followUpTime,
      followerUserName: row.followerUserName,
      content: row.content,
      id: row.id, // è®°å½•ID用于更新
      editIndex: index, // è®°å½•索引用于本地更新
    });
    negotiationDialogVisible.value = true;
  };
  // åˆ é™¤æ´½è°ˆè®°å½•
  const deleteNegotiationRecord = (row, index) => {
    ElMessageBox.confirm("确定要删除这条洽谈记录吗?", "删除提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // è¿™é‡Œå¯ä»¥è°ƒç”¨åˆ é™¤æŽ¥å£
        // å®žé™…项目中需要根据后端接口进行调整
        // ç¤ºä¾‹ï¼šdeleteCustomerFollow(row.id).then(() => {
        //   negotiationRecords.value.splice(index, 1);
        //   proxy.$modal.msgSuccess("删除成功");
        // });
        delCustomerFollow(row.id).then(() => {
          // åˆ é™¤æˆåŠŸåŽæ›´æ–°æœ¬åœ°æ•°æ®
          getCustomer(row.customerId).then(res => {
            // æ›´æ–°æœ¬åœ°æ•°æ®
            negotiationRecords.value = res.data.followUpList || [];
          });
          proxy.$modal.msgSuccess("删除成功");
        });
        // æœ¬åœ°åˆ é™¤ï¼ˆæ¨¡æ‹Ÿï¼‰
        negotiationRecords.value.splice(index, 1);
        proxy.$modal.msgSuccess("删除成功");
      })
      .catch(() => {
        proxy.$modal.msg("已取消删除");
      });
  };
  // æ‰“开附件弹窗
  const openAttachmentDialog = row => {
    currentFollowRecord.value = row;
    // è½¬æ¢ä¸ºç¬¦åˆElement Plus fileList格式的数组
    currentAttachmentList.value = (row.fileList || []).map((file, index) => ({
      name: file.fileName,
      url: file.fileUrl,
      size: file.fileSize,
      id: file.id,
      uid: file.id || index,
      status: "success",
    }));
    attachmentDialogVisible.value = true;
  };
  // å…³é—­é™„件弹窗
  const closeAttachmentDialog = () => {
    attachmentDialogVisible.value = false;
    currentFollowRecord.value = {};
    currentAttachmentList.value = [];
  };
  // é™„件上传成功
  const handleAttachmentSuccess = (response, file, fileList) => {
    if (response.code === 200) {
      proxy.$modal.msgSuccess("上传成功");
      // æ›´æ–°å½“前记录的附件列表
      currentAttachmentList.value = fileList.map(item => ({
        name: item.name,
        size: item.size,
        url: item.response?.data?.url || item.url,
        id: item.response?.data?.id,
        uid: item.uid,
        status: "success",
      }));
      // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
      if (currentFollowRecord.value) {
        currentFollowRecord.value.files = [...currentAttachmentList.value];
      }
    } else {
      proxy.$modal.msgError(response.msg || "上传失败");
    }
  };
  // é™„件上传失败
  const handleAttachmentError = (error, file, fileList) => {
    console.error("上传失败:", error);
    proxy.$modal.msgError("上传失败");
  };
  // é™„件移除
  const handleAttachmentRemove = (file, fileList) => {
    currentAttachmentList.value = fileList;
    // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
    if (currentFollowRecord.value) {
      currentFollowRecord.value.files = [...fileList];
    }
  };
  // é™„件上传前校验
  const beforeAttachmentUpload = file => {
    const maxSize = 50 * 1024 * 1024; // 50MB
    if (file.size > maxSize) {
      proxy.$modal.msgError("文件大小不能超过50MB");
      return false;
    }
    return true;
  };
  // æ ¼å¼åŒ–文件大小
  const formatFileSize = size => {
    if (size < 1024) {
      return size + " B";
    } else if (size < 1024 * 1024) {
      return (size / 1024).toFixed(2) + " KB";
    } else {
      return (size / (1024 * 1024)).toFixed(2) + " MB";
    }
  };
  // ä¸‹è½½é™„ä»¶
  const downloadAttachment = row => {
    if (row.url) {
      // proxy.download(row.url, {}, row.name);
      proxy.$download.name(row.url);
    } else {
      proxy.$modal.msgError("下载链接不存在");
    }
  };
  // åˆ é™¤é™„ä»¶
  const deleteAttachment = (row, index) => {
    ElMessageBox.confirm("确定要删除这个附件吗?", "删除提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // è°ƒç”¨åŽç«¯æŽ¥å£åˆ é™¤é™„ä»¶
        const deleteUrl =
          import.meta.env.VITE_APP_BASE_API +
          "/basic/customer-follow/file/" +
          row.id;
        fetch(deleteUrl, {
          method: "DELETE",
          headers: {
            Authorization: "Bearer " + getToken(),
            "Content-Type": "application/json",
          },
        })
          .then(response => response.json())
          .then(res => {
            if (res.code === 200) {
              // åˆ é™¤æˆåŠŸåŽæ›´æ–°æœ¬åœ°æ–‡ä»¶åˆ—è¡¨
              currentAttachmentList.value.splice(index, 1);
              // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
              if (currentFollowRecord.value) {
                currentFollowRecord.value.files = [
                  ...currentAttachmentList.value,
                ];
              }
              proxy.$modal.msgSuccess("删除成功");
            } else {
              proxy.$modal.msgError(res.msg || "删除失败");
            }
          })
          .catch(error => {
            console.error("删除附件失败:", error);
            proxy.$modal.msgError("删除失败");
          });
      })
      .catch(() => {
        proxy.$modal.msg("已取消删除");
      });
  };
  /*
   * æ´½è°ˆè¿›åº¦è®°å½• & é™„件管理相关(已随入口整体注释)
   * - editNegotiationRecord / deleteNegotiationRecord
   * - openAttachmentDialog / closeAttachmentDialog / ...附件相关方法
   */
  // èŽ·å–å½“å‰æ—¥æœŸå¹¶æ ¼å¼åŒ–ä¸º YYYY-MM-DD
  function getCurrentDate() {
@@ -1483,11 +1247,186 @@
  }
  onMounted(() => {
    fetchRegions();
    getList();
  });
  watch(regionKeyword, value => {
    regionTreeRef.value?.filter((value || "").trim());
  });
</script>
<style scoped lang="scss">
  .customer-split {
    display: flex;
    gap: 12px;
  }
  .left-panel {
    width: 260px;
    flex: 0 0 260px;
    background: #fff;
    border-radius: 6px;
    border: 1px solid #ebeef5;
    padding: 12px;
    height: calc(100vh - 140px);
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  .right-panel {
    flex: 1;
    min-width: 0;
  }
  .toolbar-card {
    background: #ffffff;
    border: 1px solid #ebeef5;
    border-radius: 10px;
    padding: 14px 16px;
    margin-bottom: 12px;
    box-shadow: 0 2px 10px rgba(31, 35, 41, 0.04);
  }
  .right-search-form {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    flex-wrap: nowrap;
  }
  .search-fields {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    gap: 0;
    flex: 1;
    min-width: 0;
  }
  .toolbar-divider {
    width: 1px;
    align-self: stretch;
    background: linear-gradient(to bottom, transparent, #e4e7ed 15%, #e4e7ed 85%, transparent);
  }
  .action-buttons {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    padding-left: 2px;
    flex-shrink: 0;
  }
  .action-buttons :deep(.el-button) {
    margin-left: 0;
    min-width: 84px;
  }
  .table-card {
    background: #fff;
    border: 1px solid #ebeef5;
    border-radius: 10px;
    padding: 12px;
    box-shadow: 0 2px 10px rgba(31, 35, 41, 0.03);
  }
  @media (max-width: 1500px) {
    .right-search-form {
      flex-wrap: wrap;
    }
    .search-fields {
      flex-wrap: wrap;
      gap: 8px 0;
    }
    .toolbar-divider {
      display: none;
    }
    .action-buttons {
      width: 100%;
      border-top: 1px dashed #e4e7ed;
      padding-top: 10px;
    }
  }
  .left-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
  }
  .left-title {
    font-weight: 600;
    color: #303133;
  }
  .left-actions {
    display: flex;
    gap: 6px;
  }
  .left-actions :deep(.el-button) {
    margin-left: 0;
    padding: 5px 10px;
  }
  .left-search {
    margin-bottom: 10px;
  }
  .left-list {
    flex: 1;
    overflow: auto;
    padding-right: 4px;
  }
  .region-item {
    padding: 8px 10px;
    border-radius: 6px;
    cursor: pointer;
    color: #303133;
    user-select: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .region-item:hover {
    background: #f5f7fa;
  }
  .region-item.active {
    background: #ecf5ff;
    color: #409eff;
    font-weight: 600;
  }
  .left-list :deep(.el-tree) {
    background: transparent;
  }
  .left-list :deep(.el-tree-node__content) {
    height: 30px;
    border-radius: 6px;
  }
  .left-list :deep(.el-tree-node__content:hover) {
    background: #f5f7fa;
  }
  .empty-tip {
    padding: 16px 10px;
    color: #909399;
    font-size: 13px;
  }
  .detail-section {
    margin-bottom: 20px;
    padding: 15px;
src/views/salesManagement/salesLedger/components/OtherAmountMaintenanceButton.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,278 @@
<template>
  <div style="display: inline-block;">
    <el-button type="primary" plain @click="openDialog">
      å…¶ä»–金额维护
    </el-button>
    <el-dialog
      v-model="visible"
      title="其他金额维护"
      width="80%"
      :close-on-click-modal="false"
      @close="closeDialog"
    >
      <el-row :gutter="20">
        <el-col :span="14">
          <el-table
            :data="records"
            border
            v-loading="loading"
            height="55vh"
          >
            <el-table-column label="编码" prop="code" min-width="120" show-overflow-tooltip />
            <el-table-column label="项目" prop="processName" min-width="180" show-overflow-tooltip />
            <el-table-column label="数量" prop="quantity" min-width="110" :formatter="formattedNumber" />
            <el-table-column label="单价(元)" prop="unitPrice" min-width="130" :formatter="formattedNumber" />
            <el-table-column label="金额(元)" prop="amount" min-width="160" :formatter="formattedNumber" />
            <el-table-column fixed="right" label="操作" width="160" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
                <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <pagination
            v-show="total > 0"
            :total="total"
            layout="total, sizes, prev, pager, next, jumper"
            :page="page.current"
            :limit="page.size"
            @pagination="paginationChange"
          />
        </el-col>
        <el-col :span="10">
          <div style="padding: 8px 0;">
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;">
              <div style="font-weight:600;">
                {{ operationType === "add" ? "新增其他金额" : "编辑其他金额" }}
              </div>
              <el-button
                type="primary"
                plain
                size="small"
                @click="handleAdd"
                :disabled="operationType === 'add'"
              >
                æ–°å¢ž
              </el-button>
            </div>
            <el-form
              :model="form"
              label-width="120px"
              label-position="top"
              :rules="rules"
              ref="formRef"
            >
              <el-form-item label="编码">
                <el-input v-model="form.code" placeholder="请输入编码(可选)" clearable />
              </el-form-item>
              <el-form-item label="项目" prop="processName">
                <el-input v-model="form.processName" placeholder="请输入项目名称" clearable />
              </el-form-item>
              <el-form-item label="数量" prop="quantity">
                <el-input-number
                  v-model="form.quantity"
                  :min="0"
                  :precision="2"
                  style="width:100%"
                  placeholder="请输入数量"
                  clearable
                  @change="recalcAmount"
                />
              </el-form-item>
              <el-form-item label="单价(元)" prop="unitPrice">
                <el-input-number
                  v-model="form.unitPrice"
                  :min="0"
                  :precision="2"
                  style="width:100%"
                  placeholder="请输入单价"
                  clearable
                  @change="recalcAmount"
                />
              </el-form-item>
              <el-form-item label="金额(元)">
                <el-input v-model="form.amount" disabled />
              </el-form-item>
              <div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 8px;">
                <el-button @click="closeDialog">取消</el-button>
                <el-button type="primary" @click="submitForm">保存</el-button>
              </div>
            </el-form>
          </div>
        </el-col>
      </el-row>
    </el-dialog>
  </div>
</template>
<script setup>
import { getCurrentInstance, reactive, ref } from "vue";
import { ElMessageBox } from "element-plus";
import pagination from "@/components/PIMTable/Pagination.vue";
import {
  salesLedgerProductProcessList,
  salesLedgerProductProcessAdd,
  salesLedgerProductProcessUpdate,
  salesLedgerProductProcessDelete,
} from "@/api/salesManagement/salesLedger.js";
const { proxy } = getCurrentInstance();
const visible = ref(false);
const loading = ref(false);
const records = ref([]);
const total = ref(0);
const page = reactive({
  current: 1,
  size: 10,
});
const operationType = ref("add");
const formRef = ref(null);
const form = reactive({
  id: null,
  code: "",
  processName: "",
  quantity: 0,
  unitPrice: 0,
  amount: "0.00",
});
const rules = reactive({
  processName: [{ required: true, message: "请输入项目名称", trigger: "change" }],
  quantity: [{ required: true, message: "请输入数量", trigger: "blur" }],
  unitPrice: [{ required: true, message: "请输入单价", trigger: "blur" }],
});
const formattedNumber = (row, column, cellValue) => {
  return Number(cellValue || 0).toFixed(2);
};
const recalcAmount = () => {
  const quantity = Number(form.quantity ?? 0) || 0;
  const unitPrice = Number(form.unitPrice ?? 0) || 0;
  form.amount = (quantity * unitPrice).toFixed(2);
};
const resetForm = (type = "add") => {
  operationType.value = type;
  form.id = null;
  form.code = "";
  form.processName = "";
  form.quantity = 0;
  form.unitPrice = 0;
  form.amount = "0.00";
};
const fetchList = async () => {
  loading.value = true;
  try {
    const res = await salesLedgerProductProcessList({
      current: page.current,
      size: page.size,
    });
    const list = res?.records ?? res?.data?.records ?? [];
    const sum = res?.total ?? res?.data?.total ?? 0;
    records.value = list.map((item) => {
      const quantity = Number(item.quantity ?? 0) || 0;
      const unitPrice = Number(item.unitPrice ?? 0) || 0;
      const amount = Number(item.amount ?? quantity * unitPrice) || 0;
      return {
        id: item.id,
        code: item.code ?? item.remark ?? "",
        processName: item.processName ?? "",
        quantity,
        unitPrice,
        amount: amount.toFixed(2),
      };
    });
    total.value = sum;
  } finally {
    loading.value = false;
  }
};
const openDialog = () => {
  visible.value = true;
  resetForm("add");
  fetchList();
};
const closeDialog = () => {
  visible.value = false;
  resetForm("add");
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  fetchList();
};
const handleAdd = () => {
  resetForm("add");
};
const handleEdit = (row) => {
  if (!row) return;
  operationType.value = "edit";
  form.id = row.id ?? null;
  form.code = row.code ?? "";
  form.processName = row.processName ?? "";
  form.quantity = Number(row.quantity ?? 0) || 0;
  form.unitPrice = Number(row.unitPrice ?? 0) || 0;
  recalcAmount();
};
const submitForm = () => {
  formRef.value?.validate((valid) => {
    if (!valid) return;
    const payload = {
      processName: form.processName,
      quantity: Number(form.quantity) || 0,
      unitPrice: Number(form.unitPrice) || 0,
      amount: Number(form.amount) || 0,
      remark: form.code,
      code: form.code,
    };
    const req =
      operationType.value === "edit"
        ? salesLedgerProductProcessUpdate({ ...payload, id: form.id })
        : salesLedgerProductProcessAdd(payload);
    req.then(() => {
      proxy.$modal.msgSuccess("保存成功");
      fetchList();
      resetForm("add");
    });
  });
};
const handleDelete = (row) => {
  if (!row?.id) return;
  ElMessageBox.confirm("确认删除该记录?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      return salesLedgerProductProcessDelete(row.id).then(() => {
        proxy.$modal.msgSuccess("删除成功");
        fetchList();
        if (operationType.value === "edit" && form.id === row.id) {
          resetForm("add");
        }
      });
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
</script>
src/views/salesManagement/salesLedger/components/ProcessFlowConfigSelectDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,255 @@
<template>
  <el-dialog
    v-model="visible"
    title="选择工艺路线配置"
    width="1000px"
    :close-on-click-modal="false"
    @close="handleClose"
  >
    <el-row :gutter="20">
      <el-col :span="24">
        <div style="font-weight: 600; margin-bottom: 8px;">配置</div>
        <div style="font-size: 12px; margin-bottom: 8px;">
          <span v-if="boundRouteName" style="color: #67c23a;">已绑定:{{ boundRouteName }}</span>
          <span v-else style="color: #e6a23c;">未绑定</span>
        </div>
        <el-select
          v-model="selectedRouteId"
          filterable
          clearable
          placeholder="请选择工艺路线"
          style="width: 100%;"
          @change="handleRouteChange"
        >
          <el-option
            v-for="cfg in routeList"
            :key="cfg.routeId"
            :label="cfg.processRouteName"
            :value="cfg.routeId"
          />
        </el-select>
        <el-divider style="margin: 16px 0;" />
        <div style="font-weight: 600; margin-bottom: 8px;">步骤预览</div>
        <div style="font-size: 12px; color: #909399; margin-bottom: 10px;">
          æ ¹æ®æ‰€é€‰é…ç½®å±•示流程图
        </div>
      </el-col>
      <el-col :span="24">
        <div class="process-diagram">
          <div v-if="steps.length === 0" class="process-diagram-empty">暂无步骤</div>
          <div
            v-for="(step, idx) in steps"
            :key="String(step.processId) + '_' + idx"
            class="process-diagram-segment"
          >
            <div class="process-diagram-node">
              <div class="process-diagram-index">{{ idx + 1 }}</div>
              <div class="process-diagram-name">{{ step.processName }}</div>
            </div>
            <div v-if="idx < steps.length - 1" class="process-diagram-arrow">→</div>
          </div>
        </div>
        <div v-if="selectedRouteId === null" style="margin-top: 10px; font-size: 12px; color: #909399;">
          è¯·å…ˆé€‰æ‹©ä¸€æ¡å·²ç»´æŠ¤å¥½çš„工艺路线
        </div>
      </el-col>
    </el-row>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" :loading="saving" @click="confirmSelect">
          ç¡®å®š
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { computed, getCurrentInstance, ref, watch } from "vue";
import { salesProcessFlowConfigList, salesProcessFlowConfigItemList } from "@/api/salesManagement/salesProcessFlowConfig.js";
const emit = defineEmits(["update:visible", "confirm"]);
const props = defineProps({
  visible: { type: Boolean, default: false },
  // æ‰“开弹窗时的回显:若业务已绑定工艺路线则传入该 routeId;否则默认展示列表第一条
  defaultRouteId: { type: [Number, String, null], default: null },
  // é¡µé¢æç¤ºï¼šè®¢å•已绑定的工艺路线名称
  boundRouteName: { type: String, default: "" },
});
const { proxy } = getCurrentInstance();
const visible = computed({
  get() {
    return props.visible;
  },
  set(v) {
    emit("update:visible", v);
  },
});
const routeList = ref([]);
const selectedRouteId = ref(null);
const steps = ref([]);
const saving = ref(false);
const normalizeStepsFromApi = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((s, idx) => ({
    stepId: s.stepId ?? s.id ?? null,
    processId: s.processId ?? s.process_id ?? s.id ?? null,
    processName: s.processName ?? s.process_name ?? s.name ?? "",
    sortNo: s.sortNo ?? idx + 1,
  }));
};
const normalizeRouteList = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((r) => ({
    routeId: r.routeId ?? r.id ?? null,
    processRouteName: r.processRouteName ?? r.routeName ?? r.name ?? "",
    isDefault: Boolean(r.isDefault),
  }));
};
const fetchRouteList = async () => {
  // é€‰æ‹©å¼¹çª—:尽量一次性拉全,避免分页影响选择体验
  const res = await salesProcessFlowConfigList({ current: 1, size: 1000 });
  const records = res?.records ?? res?.data?.records ?? res?.data ?? res ?? [];
  routeList.value = normalizeRouteList(records).filter((r) => r.routeId !== null && r.routeId !== undefined && r.routeId !== "");
};
const fetchRouteSteps = async (routeId) => {
  if (!routeId) {
    steps.value = [];
    return;
  }
  const res = await salesProcessFlowConfigItemList(routeId);
  const raw = res?.data ?? res ?? [];
  steps.value = normalizeStepsFromApi(raw);
};
watch(
  () => props.visible,
  async (v) => {
    if (v) {
      try {
        await fetchRouteList();
        // å›žæ˜¾ç»‘定:
        // 1. è‹¥ä¼ å…¥ defaultRouteId,则优先使用它
        // 2. å¦åˆ™ä¼˜å…ˆé€‰ä¸­æ ‡è®°ä¸ºé»˜è®¤(isDefault=true)的工艺路线
        // 3. è‹¥éƒ½æ²¡æœ‰ï¼Œåˆ™å›žé€€ä¸ºç¬¬ä¸€æ¡
        const first = routeList.value?.[0] ?? null;
        const defaultRoute =
          routeList.value.find((r) => r.isDefault) ?? first;
        const desired = props.defaultRouteId ?? (defaultRoute ? defaultRoute.routeId : null);
        selectedRouteId.value = desired ?? null;
        await fetchRouteSteps(selectedRouteId.value);
      } catch {
        proxy?.$modal?.msgError?.("获取工艺路线配置失败");
      }
    }
  }
);
const handleRouteChange = async () => {
  await fetchRouteSteps(selectedRouteId.value);
};
const handleClose = () => {
  emit("update:visible", false);
  saving.value = false;
};
const confirmSelect = async () => {
  if (saving.value) return;
  if (selectedRouteId.value === null || selectedRouteId.value === undefined || selectedRouteId.value === "") {
    proxy?.$modal?.msgWarning?.("请选择工艺路线");
    return;
  }
  saving.value = true;
  try {
    emit("confirm", selectedRouteId.value);
  } catch (e) {
    proxy?.$modal?.msgError?.("确认失败,请稍后重试");
  } finally {
    saving.value = false;
  }
};
</script>
<style scoped>
.process-diagram {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  padding: 10px 0;
}
.process-diagram-segment {
  display: flex;
  align-items: center;
}
.process-diagram-node {
  width: 160px;
  min-width: 160px;
  height: 78px;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  background: #fff;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 10px 12px;
  margin-right: 10px;
  box-sizing: border-box;
}
.process-diagram-index {
  font-size: 12px;
  color: #909399;
  margin-bottom: 4px;
}
.process-diagram-name {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.process-diagram-arrow {
  font-size: 18px;
  color: #909399;
  margin-right: 14px;
  margin-left: -6px;
}
.process-diagram-empty {
  width: 100%;
  text-align: center;
  padding: 40px 0;
  color: #909399;
  border: 1px dashed #ebeef5;
  border-radius: 8px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,525 @@
<template>
  <div style="display: inline-block; margin-left: 8px;">
    <el-button type="primary" plain @click="openDialog">工艺流程</el-button>
    <el-dialog
      v-model="visible"
      title="工艺路线与工序维护"
      width="1280px"
      class="process-route-dialog"
      :close-on-click-modal="false"
      @close="closeDialog"
    >
      <el-row :gutter="16" class="dialog-main">
        <el-col :span="10">
          <div class="left-panel">
            <div style="font-weight: 600; margin-bottom: 10px;">工艺路线</div>
            <div class="route-toolbar route-toolbar-left">
              <el-input
                v-model="routeKeyword"
                placeholder="按工艺路线名称查询"
                clearable
                @keyup.enter="handleRouteQuery"
              />
              <el-button type="primary" @click="handleRouteQuery">查询</el-button>
              <el-input
                v-model="routeNameDraft"
                placeholder="输入名称后新增"
                clearable
              />
              <el-button type="primary" plain @click="createRoute">新增</el-button>
            </div>
            <div class="left-table-wrap">
              <el-table
                :data="routeList"
                border
                row-key="routeId"
                highlight-current-row
                height="100%"
                table-layout="fixed"
                @current-change="handleRouteSelect"
              >
                <el-table-column label="序号" width="56" align="center">
                  <template #default="scope">{{ scope.$index + 1 }}</template>
                </el-table-column>
                <el-table-column label="工艺路线名称" min-width="150" prop="processRouteName" show-overflow-tooltip />
                <el-table-column label="默认" width="56" align="center">
                  <template #default="scope">
                    <el-tag v-if="scope.row.isDefault" type="success" size="small">是</el-tag>
                    <span v-else>-</span>
                  </template>
                </el-table-column>
                <el-table-column label="操作" width="160" align="center">
                  <template #default="scope">
                    <el-button link type="primary" size="small" @click="editRoute(scope.row)">改名</el-button>
                    <el-button link type="primary" size="small" @click="setDefaultRoute(scope.row)">默认</el-button>
                    <el-button link type="danger" size="small" @click="deleteRoute(scope.row)">删除</el-button>
                  </template>
                </el-table-column>
              </el-table>
            </div>
            <pagination
              v-show="routeTotal > 0"
              :total="routeTotal"
              layout="total, sizes, prev, pager, next, jumper"
              :page="routePage.current"
              :limit="routePage.size"
              @pagination="handleRoutePaginationChange"
            />
          </div>
        </el-col>
        <el-col :span="14">
          <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
            <div style="font-weight: 600;">工序维护</div>
            <el-tag v-if="selectedRouteId" type="success" size="small">当前路线:{{ selectedRouteName }}</el-tag>
            <el-tag v-else type="info" size="small">请先选择工艺路线</el-tag>
          </div>
          <div class="route-toolbar">
            <el-input
              v-model="processNameDraft"
              placeholder="输入工序名称后新增"
              style="max-width: 260px;"
              clearable
              :disabled="!selectedRouteId"
            />
            <el-button type="primary" plain :disabled="!selectedRouteId" @click="createProcessItem">新增工序</el-button>
          </div>
          <div class="process-diagram">
            <div v-if="processItems.length === 0" class="process-diagram-empty">暂无工序</div>
            <div
              v-for="(step, idx) in processItems"
              :key="String(step.itemId) + '_' + idx"
              class="process-diagram-segment"
            >
              <div class="process-diagram-node">
                <div class="process-diagram-index">{{ idx + 1 }}</div>
                <div class="process-diagram-name">{{ step.processName }}</div>
              </div>
              <div v-if="idx < processItems.length - 1" class="process-diagram-arrow">→</div>
            </div>
          </div>
          <el-table ref="processTableRef" :data="processItems" border row-key="itemId" height="420px" size="small">
            <el-table-column label="序号" width="80" align="center">
              <template #default="scope">{{ scope.$index + 1 }}</template>
            </el-table-column>
            <el-table-column label="工序名称" min-width="220" prop="processName" show-overflow-tooltip />
            <el-table-column label="操作" width="300" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="editProcessItem(scope.row)">编辑</el-button>
                <el-button link type="danger" size="small" @click="deleteProcessItem(scope.row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
        </el-col>
      </el-row>
    </el-dialog>
  </div>
</template>
<script setup>
import { getCurrentInstance, ref, watch, onBeforeUnmount, nextTick } from "vue";
import { ElMessageBox } from "element-plus";
import Sortable from "sortablejs";
import {
  salesProcessFlowConfigList,
  salesProcessFlowConfigUpsert,
  salesProcessFlowConfigDelete,
  salesProcessFlowConfigSetDefault,
  salesProcessFlowConfigItemList,
  salesProcessFlowConfigItemUpsert,
  salesProcessFlowConfigItemSort,
  salesProcessFlowConfigItemDelete,
} from "@/api/salesManagement/salesProcessFlowConfig.js";
const { proxy } = getCurrentInstance();
const visible = ref(false);
let prevBodyOverflow = "";
let prevBodyOverflowY = "";
const lockBodyScroll = () => {
  // å…œåº•处理:有些场景下 Element Plus ä¸ä¼šå®Œå…¨ç¦æ­¢èƒŒæ™¯é¡µé¢æ»šåЍ
  prevBodyOverflow = document.body.style.overflow || "";
  prevBodyOverflowY = document.body.style.overflowY || "";
  document.body.style.overflow = "hidden";
  document.body.style.overflowY = "hidden";
};
const unlockBodyScroll = () => {
  document.body.style.overflow = prevBodyOverflow;
  document.body.style.overflowY = prevBodyOverflowY;
};
watch(visible, (v) => {
  if (v) lockBodyScroll();
  else unlockBodyScroll();
});
onBeforeUnmount(() => {
  unlockBodyScroll();
});
const routeKeyword = ref("");
const routeNameDraft = ref("");
const processNameDraft = ref("");
const routeList = ref([]);
const routeTotal = ref(0);
const routePage = ref({
  current: 1,
  size: 10,
});
const selectedRouteId = ref(null);
const selectedRouteName = ref("");
const processItems = ref([]);
const processTableRef = ref(null);
let processStepsSortable = null;
let isProcessingDrag = false;
const destroyProcessSortable = () => {
  if (processStepsSortable) {
    processStepsSortable.destroy();
    processStepsSortable = null;
  }
};
const initProcessSortable = () => {
  destroyProcessSortable();
  if (!processTableRef.value) return;
  const tbody = processTableRef.value?.$el?.querySelector(".el-table__body tbody")
    || processTableRef.value?.$el?.querySelector(".el-table__body-wrapper > table > tbody");
  if (!tbody) return;
  processStepsSortable = new Sortable(tbody, {
    animation: 150,
    ghostClass: "sortable-ghost",
    draggable: ".el-table__row",
    handle: ".el-table__row",
    filter: ".el-button, .el-input, .el-select",
    preventOnFilter: true,
    onEnd: async (evt) => {
      if (isProcessingDrag) return;
      const { oldIndex, newIndex } = evt;
      if (oldIndex === newIndex) return;
      if (!selectedRouteId.value) return;
      if (!processItems.value[oldIndex]) return;
      isProcessingDrag = true;
      try {
        const arr = [...processItems.value];
        const moving = arr.splice(oldIndex, 1)[0];
        arr.splice(newIndex, 0, moving);
        processItems.value = arr;
        ensureSortNo();
        if (!moving?.itemId) {
          proxy?.$modal?.msgError?.("当前工序缺少ID,无法排序");
          await fetchProcessItems(selectedRouteId.value);
          return;
        }
          // ä½¿ç”¨ä¸“用排序接口,避免 upsert é€ æˆé‡å¤æ–°å¢ž
          await salesProcessFlowConfigItemSort({
            id: moving.itemId,
            dragSort: newIndex + 1,
          });
        proxy?.$modal?.msgSuccess?.("顺序调整成功");
        await fetchProcessItems(selectedRouteId.value);
      } finally {
        isProcessingDrag = false;
      }
    },
  });
};
const normalizeRouteList = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((r) => ({
    routeId: r.routeId ?? r.id ?? null,
    processRouteName: r.processRouteName ?? r.routeName ?? r.name ?? "",
    isDefault: Boolean(r.isDefault),
  }));
};
const normalizeItemList = (list) => {
  if (!Array.isArray(list)) return [];
  return list.map((i, idx) => ({
    itemId: i.itemId ?? i.id ?? null,
    routeId: i.routeId ?? i.processRouteId ?? selectedRouteId.value,
    processName: i.processName ?? i.name ?? "",
    sortNo: i.sortNo ?? idx + 1,
  }));
};
const fetchRouteList = async () => {
  const res = await salesProcessFlowConfigList({
    current: routePage.value.current,
    size: routePage.value.size,
    processRouteName: routeKeyword.value || undefined,
  });
  const records = res?.records ?? res?.data?.records ?? [];
  const total = res?.total ?? res?.data?.total ?? 0;
  routeTotal.value = Number(total) || 0;
  routeList.value = normalizeRouteList(records);
};
const handleRouteQuery = async () => {
  routePage.value.current = 1;
  await fetchRouteList();
};
const handleRoutePaginationChange = async (obj) => {
  routePage.value.current = obj.page;
  routePage.value.size = obj.limit;
  await fetchRouteList();
};
const fetchProcessItems = async (routeId) => {
  if (!routeId) {
    processItems.value = [];
    destroyProcessSortable();
    return;
  }
  const res = await salesProcessFlowConfigItemList(routeId);
  const raw = res?.data ?? res ?? [];
  processItems.value = normalizeItemList(raw);
  ensureSortNo();
  await nextTick();
  initProcessSortable();
};
const openDialog = async () => {
  visible.value = true;
  selectedRouteId.value = null;
  selectedRouteName.value = "";
  processItems.value = [];
  routePage.value.current = 1;
  await fetchRouteList();
};
const closeDialog = () => {
  visible.value = false;
  selectedRouteId.value = null;
  selectedRouteName.value = "";
  processItems.value = [];
  routeNameDraft.value = "";
  processNameDraft.value = "";
  routeTotal.value = 0;
  destroyProcessSortable();
};
const handleRouteSelect = async (row) => {
  if (!row?.routeId) return;
  selectedRouteId.value = row.routeId;
  selectedRouteName.value = row.processRouteName;
  await fetchProcessItems(selectedRouteId.value);
};
const createRoute = async () => {
  if (!routeNameDraft.value) {
    proxy?.$modal?.msgWarning("请先输入工艺路线名称");
    return;
  }
  const payload = { processRouteName: routeNameDraft.value };
  await salesProcessFlowConfigUpsert(payload);
  proxy?.$modal?.msgSuccess("工艺路线新增成功");
  routeNameDraft.value = "";
  await handleRouteQuery();
};
const editRoute = async (row) => {
  const oldName = row?.processRouteName ?? "";
  const { value } = await ElMessageBox.prompt("请输入新的工艺路线名称", "修改工艺路线", {
    inputValue: oldName,
    confirmButtonText: "确认",
    cancelButtonText: "取消",
  });
  await salesProcessFlowConfigUpsert({
    routeId: row.routeId,
    processRouteName: value,
  });
  proxy?.$modal?.msgSuccess("工艺路线修改成功");
  await fetchRouteList();
  if (selectedRouteId.value === row.routeId) selectedRouteName.value = value;
};
const deleteRoute = async (row) => {
  await ElMessageBox.confirm("确认删除该工艺路线?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  });
  await salesProcessFlowConfigDelete(row.routeId);
  proxy?.$modal?.msgSuccess("删除成功");
  if (selectedRouteId.value === row.routeId) {
    selectedRouteId.value = null;
    selectedRouteName.value = "";
    processItems.value = [];
  }
  // åˆ é™¤åŽè‹¥å½“前页被清空,回退一页
  if (routeList.value.length <= 1 && routePage.value.current > 1) {
    routePage.value.current = routePage.value.current - 1;
  }
  await fetchRouteList();
};
const setDefaultRoute = async (row) => {
  await salesProcessFlowConfigSetDefault(row.routeId);
  proxy?.$modal?.msgSuccess("默认工艺路线设置成功");
  await fetchRouteList();
};
const ensureSortNo = () => {
  processItems.value = processItems.value.map((i, idx) => ({ ...i, sortNo: idx + 1 }));
};
const createProcessItem = async () => {
  if (!selectedRouteId.value) {
    proxy?.$modal?.msgWarning("请先选择工艺路线");
    return;
  }
  if (!processNameDraft.value) {
    proxy?.$modal?.msgWarning("请先输入工序名称");
    return;
  }
  await salesProcessFlowConfigItemUpsert({
    routeId: selectedRouteId.value,
    processName: processNameDraft.value,
    sortNo: processItems.value.length + 1,
  });
  proxy?.$modal?.msgSuccess("工序新增成功");
  processNameDraft.value = "";
  await fetchProcessItems(selectedRouteId.value);
};
const editProcessItem = async (row) => {
  const { value } = await ElMessageBox.prompt("请输入新的工序名称", "修改工序", {
    inputValue: row.processName,
    confirmButtonText: "确认",
    cancelButtonText: "取消",
  });
  await salesProcessFlowConfigItemUpsert({
    id: row.itemId,
    routeId: selectedRouteId.value,
    processName: value,
    sortNo: row.sortNo,
  });
  proxy?.$modal?.msgSuccess("工序修改成功");
  await fetchProcessItems(selectedRouteId.value);
};
const deleteProcessItem = async (row) => {
  await ElMessageBox.confirm("确认删除该工序?", "删除", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  });
  await salesProcessFlowConfigItemDelete(row.itemId);
  proxy?.$modal?.msgSuccess("工序删除成功");
  await fetchProcessItems(selectedRouteId.value);
};
</script>
<style scoped>
.route-toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.route-toolbar-left {
  flex-wrap: nowrap;
}
.route-toolbar-left :deep(.el-input) {
  width: 190px;
}
.process-route-dialog :deep(.el-dialog__body) {
  height: 760px;
  overflow: hidden;
  overflow-y: hidden;
}
.dialog-main {
  height: 100%;
}
.left-panel {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.left-table-wrap {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.process-diagram {
  display: flex;
  align-items: center;
  gap: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  padding: 10px 0;
}
.process-diagram-segment {
  display: flex;
  align-items: center;
}
.process-diagram-node {
  width: 160px;
  min-width: 160px;
  height: 78px;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  background: #fff;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 10px 12px;
  margin-right: 10px;
  box-sizing: border-box;
}
.process-diagram-index {
  font-size: 12px;
  color: #909399;
  margin-bottom: 4px;
}
.process-diagram-name {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.process-diagram-arrow {
  font-size: 18px;
  color: #909399;
  margin-right: 14px;
  margin-left: -6px;
}
.process-diagram-empty {
  width: 100%;
  text-align: center;
  padding: 24px 0;
  color: #909399;
  border: 1px dashed #ebeef5;
  border-radius: 8px;
}
</style>
src/views/salesManagement/salesLedger/components/processCardPrint.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,261 @@
const PRINT_TITLE = "生产流程卡(成品)";
const formatDisplayDate = (value) => {
  if (!value) return "";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
};
const getCurrentDate = () => {
  const date = new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
};
const escapeHtml = (value) =>
  String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
const renderRouteHeader = (routeNodes) => {
  const columns = (Array.isArray(routeNodes) ? routeNodes : [])
    .sort((a, b) => (a?.dragSort ?? 0) - (b?.dragSort ?? 0))
    .map((node) => `<th class="route-col">${escapeHtml(node?.processRouteItemName)}</th>`)
    .join("");
  return columns || '<th class="route-col">工序</th>';
};
const renderRouteRow = (routeNodes) => {
  const columns = (Array.isArray(routeNodes) ? routeNodes : [])
    .sort((a, b) => (a?.dragSort ?? 0) - (b?.dragSort ?? 0))
    .map(() => '<td class="route-col">次品</td>')
    .join("");
  return columns || '<td class="route-col">次品</td>';
};
const renderRouteEmptyCells = (routeNodes) => {
  const columns = (Array.isArray(routeNodes) ? routeNodes : [])
    .sort((a, b) => (a?.dragSort ?? 0) - (b?.dragSort ?? 0))
    .map(() => '<td class="route-col"></td>')
    .join("");
  return columns || '<td class="route-col"></td>';
};
const renderItems = (items, startIndex, routeNodes, totalCols) => {
  const list = Array.isArray(items) ? items : [];
  if (list.length === 0) {
    return `<tr><td colspan="${totalCols}" style="text-align:center;">暂无明细</td></tr>`;
  }
  const routeEmptyCells = renderRouteEmptyCells(routeNodes);
  return list
    .map(
      (item, index) => `
      <tr>
        <td>${startIndex + index + 1}</td>
        <td class="no-wrap">${escapeHtml(item?.floorCode)}</td>
        <td class="no-wrap">${escapeHtml(item?.width)} * ${escapeHtml(item?.height)}</td>
        <td class="no-wrap">${escapeHtml(item?.quantity)}</td>
        <td class="no-wrap">${escapeHtml(item?.area)}</td>
        <td class="no-wrap">${escapeHtml(item?.processRequirement)}</td>
        ${routeEmptyCells}
      </tr>
    `
    )
    .join("");
};
const splitItemsByPage = (items, pageSize) => {
  const list = Array.isArray(items) ? items : [];
  if (list.length === 0) return [[]];
  const pages = [];
  for (let i = 0; i < list.length; i += pageSize) {
    pages.push(list.slice(i, i + pageSize));
  }
  return pages;
};
export const printFinishedProcessCard = (cardData) => {
  const data = cardData ?? {};
  const routeNodes = Array.isArray(data.routeNodes) ? data.routeNodes : [];
  const items = Array.isArray(data.items) ? data.items : [];
  const firstItem = items[0] ?? {};
  const productName = firstItem.productDescription || "";
  const totalCols = 6 + Math.max(routeNodes.length, 1);
  const signLabelCols = 2;
  const signBlankCols = Math.max(totalCols - 5 - signLabelCols, 1);
  const pageSize = 10;
  const itemPages = splitItemsByPage(items, pageSize);
  const totalPages = itemPages.length;
  const printWindow = window.open("", "_blank", "width=1200,height=900");
  if (!printWindow) {
    throw new Error("浏览器拦截了弹窗,请允许弹窗后重试");
  }
  const html = `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>${PRINT_TITLE}</title>
    <style>
      body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #222; }
      .page { width: 198mm; margin: 0 auto; padding: 6mm 3mm 5mm; box-sizing: border-box; page-break-after: always; }
      .page:last-child { page-break-after: auto; }
      .table-wrap { position: relative; }
      .page-mark {
        position: absolute;
        left: 0;
        top: -16px;
        z-index: 2;
        font-size: 12px;
        line-height: 1;
        background: #fff;
        padding: 0 2px;
      }
      .title { text-align: center; font-size: 18px; font-weight: 700; margin-bottom: 4px; line-height: 1.2; }
      .sub-title { font-size: 14px; }
      table { width: 100%; border-collapse: collapse; table-layout: auto; }
      td, th { border: 0.8px solid #6f7f95; padding: 2px 4px; font-size: 12px; text-align: center; vertical-align: middle; word-break: break-all; }
      .left { text-align: left; }
      .no-wrap { white-space: nowrap; word-break: keep-all; }
      .route-col { min-width: 48px; white-space: nowrap; word-break: keep-all; }
      .section-title { font-weight: 700; text-align: left; padding-left: 6px; }
      .sign-label { text-align: left; padding-left: 8px; vertical-align: top; line-height: 1.2; }
      .sign-blank { vertical-align: top; min-height: 18px; }
      .order-req-content { min-height: 82px; vertical-align: top; padding-top: 6px; }
      .sign-row td { height: 18px; }
      .order-require-title { width: 34px; padding: 0; }
      .order-require-title-text {
        display: flex;
        height: 100%;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        line-height: 1.25;
        letter-spacing: 0;
        font-weight: 500;
      }
      .footer { margin-top: 10px; font-size: 12px; line-height: 1.7; padding: 0 2px; }
      .footer-row { display: flex; justify-content: space-between; }
      .footer-item { width: 33%; }
      .continued { text-align: right; font-size: 12px; padding-right: 8px; }
      @media print {
        @page { size: A4 portrait; margin: 8mm; }
        .page { width: 100%; margin: 0; padding: 0; }
      }
    </style>
  </head>
  <body>
    ${itemPages
      .map((pageItems, pageIndex) => {
        const isLastPage = pageIndex === totalPages - 1;
        const startIndex = pageIndex * pageSize;
        return `
    <div class="page">
      <div class="title">鹤壁天沐钢化玻璃厂<br /><span class="sub-title">生产流程卡(成品)</span></div>
      <div class="table-wrap">
      <div class="page-mark">第${pageIndex + 1}页,共${totalPages}页</div>
      <table>
        <thead>
          <tr>
            <td colspan="5" class="left">订单编号:${escapeHtml(data.salesContractNo)}</td>
            <td colspan="${totalCols - 5}" class="left">交货日期:${escapeHtml(formatDisplayDate(data.deliveryDate))}</td>
          </tr>
          <tr>
            <td colspan="5" class="left">客户名称:${escapeHtml(data.customerName)}</td>
            <td colspan="${totalCols - 5}" class="left">工艺流程:${escapeHtml(data.processPathDisplay)}</td>
          </tr>
          <tr>
            <th rowspan="2" style="width:6%;">订序</th>
            <th rowspan="2" style="width:22%;" class="no-wrap">楼层编号</th>
            <th rowspan="2" style="width:20%;" class="no-wrap">宽(弧长)*高</th>
            <th rowspan="2" style="width:8%;" class="no-wrap">数量</th>
            <th rowspan="2" style="width:8%;" class="no-wrap">面积</th>
            <th rowspan="2" style="width:20%;" class="no-wrap">明细加工要求</th>
            ${renderRouteHeader(routeNodes)}
          </tr>
          <tr>${renderRouteRow(routeNodes)}</tr>
        </thead>
        <tbody>
          <tr>
            <td colspan="${totalCols}" class="section-title">产品名称:${escapeHtml(productName)}</td>
          </tr>
          ${renderItems(pageItems, startIndex, routeNodes, totalCols)}
          ${
            isLastPage
              ? `<tr>
            <td colspan="3" class="left"><strong>合计:</strong></td>
            <td>${escapeHtml(data.totalQuantity)}</td>
            <td>${escapeHtml(data.totalArea)}</td>
            <td colspan="${signLabelCols}" class="sign-label">完工签名</td>
            <td colspan="${signBlankCols}" class="sign-blank"></td>
          </tr>
          <tr class="sign-row">
            <td rowspan="3" class="order-require-title">
              <div class="order-require-title-text">
                <span>订单</span>
                <span>加工</span>
                <span>要求</span>
              </div>
            </td>
            <td colspan="4" rowspan="3" class="left order-req-content">${escapeHtml(data.orderProcessRequirement)}</td>
            <td colspan="${signLabelCols}" class="sign-label">质检签名</td>
            <td colspan="${signBlankCols}" class="sign-blank"></td>
          </tr>
          <tr class="sign-row">
            <td colspan="${signLabelCols}" class="sign-label">接收签名</td>
            <td colspan="${signBlankCols}" class="sign-blank"></td>
          </tr>
          <tr class="sign-row">
            <td colspan="${signLabelCols}" class="sign-label">生产日期</td>
            <td colspan="${signBlankCols}" class="sign-blank"></td>
          </tr>`
              : `<tr><td colspan="${totalCols}" class="continued">下页续...</td></tr>`
          }
        </tbody>
      </table>
      </div>
      ${
        isLastPage
          ? `<div class="footer">
        <div class="footer-row">
          <div class="footer-item">制单员:${escapeHtml(data.register)}</div>
          <div class="footer-item">审核员:${escapeHtml(data.register)}</div>
          <div class="footer-item">工艺员:${escapeHtml(data.technician ?? "")}</div>
        </div>
        <div class="footer-row">
          <div class="footer-item">制单日期:${escapeHtml(formatDisplayDate(data.registerDate))}</div>
          <div class="footer-item">审核日期:${escapeHtml(formatDisplayDate(data.registerDate))}</div>
          <div class="footer-item">打印日期:${getCurrentDate()}</div>
        </div>
      </div>`
          : ""
      }
    </div>`;
      })
      .join("")}
  </body>
</html>
`;
  printWindow.document.write(html);
  printWindow.document.close();
  printWindow.onload = () => {
    setTimeout(() => {
      printWindow.focus();
      printWindow.print();
      printWindow.close();
    }, 300);
  };
};
src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,276 @@
const PRINT_TITLE = "销售发货单";
const escapeHtml = (value) =>
  String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
const toNumber = (value) => {
  const num = Number(value);
  return Number.isFinite(num) ? num : 0;
};
const formatDisplayDate = (value) => {
  if (!value) return "";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
};
const getItemArea = (item) => toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea);
const getOrderNo = (data, row, item) =>
  item?.salesContractNo || item?.orderNo || data?.salesContractNo || row?.salesContractNo || "";
const splitItemsByPage = (items, pageSize) => {
  const list = Array.isArray(items) ? items : [];
  if (list.length === 0) return [[]];
  const pages = [];
  for (let i = 0; i < list.length; i += pageSize) {
    pages.push(list.slice(i, i + pageSize));
  }
  return pages;
};
const normalizeInvoiceData = (raw, selectedRow) => {
  const data = raw ?? {};
  const groups = Array.isArray(data.groups) ? data.groups : [];
  if (!groups.length) return data;
  const items = groups.flatMap((group) =>
    (Array.isArray(group?.items) ? group.items : []).map((item) => ({
      ...item,
      productDescription: group?.productName || item?.productDescription || "",
      salesContractNo: group?.salesContractNo || item?.salesContractNo || "",
      widthHeight: item?.widthHeight || "",
    }))
  );
  return {
    ...data,
    items,
    customerName: data.customerName || selectedRow?.customerName || "",
    contactPerson: data.contactPerson || selectedRow?.contactPerson || "",
    contactPhone: data.contactPhone || selectedRow?.contactPhone || "",
    deliveryAddress:
      data.companyAddress || data.deliveryAddress || data.shippingAddress || selectedRow?.deliveryAddress || "",
    shipmentNo: data.externalOrderNo || data.shipmentNo || "",
    register: data.orderMaker || data.register || selectedRow?.entryPersonName || "",
    registerDate: data.executionDate || data.registerDate || data.entryDate || selectedRow?.entryDate || "",
  };
};
const groupByProduct = (items, data, row) => {
  const list = Array.isArray(items) ? items : [];
  const map = new Map();
  list.forEach((item) => {
    const key = `${item?.productDescription || ""}__${getOrderNo(data, row, item)}`;
    if (!map.has(key)) {
      map.set(key, {
        productName: item?.productDescription || "",
        orderNo: getOrderNo(data, row, item),
        items: [],
      });
    }
    map.get(key).items.push(item);
  });
  return Array.from(map.values());
};
const renderItemRows = (items, startIndex) =>
  items
    .map((item, idx) => {
      const sizeText = item?.widthHeight
        ? escapeHtml(item.widthHeight)
        : item?.width || item?.height
          ? `${escapeHtml(item?.width)} * ${escapeHtml(item?.height)}`
          : "";
      return `
      <tr>
        <td>${startIndex + idx + 1}</td>
        <td class="left">${escapeHtml(item?.floorCode)}</td>
        <td>${sizeText}</td>
        <td>${toNumber(item?.quantity) || ""}</td>
        <td>${getItemArea(item) ? getItemArea(item).toFixed(2) : ""}</td>
        <td class="left">${escapeHtml(item?.remark)}</td>
        <td class="left">${escapeHtml(item?.processRequirement)}</td>
      </tr>
    `;
    })
    .join("");
export const printSalesDeliveryNote = (rawData, selectedRow = {}) => {
  const data = normalizeInvoiceData(rawData, selectedRow);
  const allItems = Array.isArray(data.items) ? data.items : [];
  const pageSize = 18;
  const itemPages = splitItemsByPage(allItems, pageSize);
  const totalPages = itemPages.length;
  const printWindow = window.open("", "_blank", "width=1200,height=900");
  if (!printWindow) {
    throw new Error("浏览器拦截了弹窗,请允许弹窗后重试");
  }
  const html = `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>${PRINT_TITLE}</title>
    <style>
      body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #222; }
      .page { width: 198mm; margin: 0 auto; padding: 4mm 4mm 6mm; box-sizing: border-box; page-break-after: always; }
      .page:last-child { page-break-after: auto; }
      .head-top {
        display: grid;
        grid-template-columns: 1fr auto 1fr;
        align-items: end;
        margin-bottom: 1px;
      }
      .factory {
        grid-column: 2;
        text-align: center;
        font-size: 20px;
        font-weight: 700;
        line-height: 1.2;
      }
      .page-mark {
        grid-column: 3;
        justify-self: end;
        font-size: 12px;
        margin-right: 8mm;
        margin-bottom: 1px;
      }
      .head-mid {
        display: grid;
        grid-template-columns: 1fr auto 1fr;
        align-items: center;
        margin-bottom: 2px;
      }
      .head-mid-left { font-size: 13px; text-align: left; }
      .head-mid-title { font-size: 20px; font-weight: 700; text-align: center; }
      .head-mid-right { font-size: 13px; text-align: right; padding-right: 8mm; }
      table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; }
      td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; }
      .left { text-align: left; }
      .group-title td { font-weight: 700; }
      .subtotal td, .total-row td { font-weight: 700; }
      .empty td { height: 120px; color: #666; }
      .footer { margin-top: 6px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; font-size: 13px; }
      @media print {
        @page { size: A4 portrait; margin: 8mm; }
        .page { width: 100%; margin: 0; padding: 0; }
      }
    </style>
  </head>
  <body>
  ${itemPages
    .map((pageItems, pageIndex) => {
      const pageGroups = groupByProduct(pageItems, data, selectedRow);
      let serial = pageIndex * pageSize;
      const totalQty = toNumber(data.totalQuantity) || allItems.reduce((s, it) => s + toNumber(it?.quantity), 0);
      const totalArea = toNumber(data.totalArea) || allItems.reduce((s, it) => s + getItemArea(it), 0);
      return `
    <div class="page">
      <div class="head-top">
        <div></div>
        <div class="factory">鹤壁天沐钢化玻璃厂</div>
        <div class="page-mark">第${pageIndex + 1}页,共${totalPages}页</div>
      </div>
      <div class="head-mid">
        <div class="head-mid-left">对方单号: ${escapeHtml(data.deliveryNo || data.shippingNo || selectedRow.expressNumber || "")}</div>
        <div class="head-mid-title">销售发货单</div>
        <div class="head-mid-right">发货单号: ${escapeHtml(data.shipmentNo || data.deliveryNo || "")}</div>
      </div>
      <table>
        <tr>
          <td class="left" colspan="4">客户名称: ${escapeHtml(data.customerName || selectedRow.customerName || "")}</td>
          <td class="left" colspan="3">联系人: ${escapeHtml(data.contactPerson || selectedRow.contactPerson || "")}</td>
        </tr>
        <tr>
          <td class="left" colspan="4">发货地址: ${escapeHtml(data.deliveryAddress || data.shippingAddress || selectedRow.deliveryAddress || "")}</td>
          <td class="left" colspan="3">联系电话: ${escapeHtml(data.contactPhone || selectedRow.contactPhone || "")}</td>
        </tr>
        <tr>
          <th style="width:8%;">序号</th>
          <th style="width:22%;">楼层编号</th>
          <th style="width:20%;">宽(弧长)*高</th>
          <th style="width:10%;">数量</th>
          <th style="width:12%;">面积</th>
          <th style="width:10%;">备注</th>
          <th style="width:18%;">加工要求</th>
        </tr>
        ${
          pageGroups.length
            ? pageGroups
                .map((group) => {
                  const subQty = group.items.reduce((s, it) => s + toNumber(it?.quantity), 0);
                  const subArea = group.items.reduce((s, it) => s + getItemArea(it), 0);
                  const rows = renderItemRows(group.items, serial);
                  serial += group.items.length;
                  return `
          <tr class="group-title">
            <td colspan="5" class="left">产品名称: ${escapeHtml(group.productName)}</td>
            <td colspan="2" class="left">订单编号: ${escapeHtml(group.orderNo)}</td>
          </tr>
          ${rows}
          <tr class="subtotal">
            <td colspan="3">小计:</td>
            <td>${subQty || ""}</td>
            <td>${subArea ? subArea.toFixed(2) : ""}</td>
            <td colspan="2"></td>
          </tr>
                  `;
                })
                .join("")
            : `<tr class="empty"><td colspan="7">暂无明细</td></tr>`
        }
        ${
          pageIndex === totalPages - 1
            ? `
        <tr class="total-row">
          <td colspan="3">合计:</td>
          <td>${totalQty || ""}</td>
          <td>${totalArea ? totalArea.toFixed(2) : ""}</td>
          <td colspan="2"></td>
        </tr>
            `
            : ""
        }
      </table>
      ${
        pageIndex === totalPages - 1
          ? `
      <div class="footer">
        <div>制 å• å‘˜: ${escapeHtml(data.register || selectedRow.entryPersonName || "")}</div>
        <div>制单日期: ${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate || selectedRow.entryDate))}</div>
        <div>客户签字:</div>
        <div>签收日期:</div>
      </div>
          `
          : ""
      }
    </div>
      `;
    })
    .join("")}
  </body>
</html>
`;
  printWindow.document.write(html);
  printWindow.document.close();
  printWindow.onload = () => {
    setTimeout(() => {
      printWindow.focus();
      printWindow.print();
      printWindow.close();
    }, 300);
  };
};
src/views/salesManagement/salesLedger/components/salesLabelPrint.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,148 @@
const PRINT_TITLE = "销售标签";
const escapeHtml = (value) =>
  String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
const splitByPage = (items, pageSize) => {
  const list = Array.isArray(items) ? items : [];
  if (list.length === 0) return [[]];
  const pages = [];
  for (let i = 0; i < list.length; i += pageSize) {
    pages.push(list.slice(i, i + pageSize));
  }
  return pages;
};
const renderLabelCard = (item) => `
  <div class="label-card">
    <div class="line customer">${escapeHtml(item?.customerName)}</div>
    <div class="line order">${escapeHtml(item?.salesContractNo)}</div>
    <div class="line product">${escapeHtml(item?.productName)}</div>
    <div class="line spec">${escapeHtml(item?.specification)}</div>
    <div class="line address">${escapeHtml(item?.floorCode)}</div>
  </div>
`;
export const printSalesLabel = (rawList = []) => {
  const list = Array.isArray(rawList) ? rawList : [];
  if (!list.length) {
    throw new Error("标签数据为空,无法打印");
  }
  const pageSize = 18; // 3 åˆ— * 6 è¡Œï¼ˆ50x40mm)
  const pages = splitByPage(list, pageSize);
  const printWindow = window.open("", "_blank", "width=1200,height=900");
  if (!printWindow) {
    throw new Error("浏览器拦截了弹窗,请允许弹窗后重试");
  }
  const html = `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>${PRINT_TITLE}</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        font-family: "SimSun", serif;
        color: #222;
      }
      .page {
        width: 210mm;
        min-height: 297mm;
        margin: 0 auto;
        padding: 8mm 7mm;
        box-sizing: border-box;
        page-break-after: always;
      }
      .page:last-child {
        page-break-after: auto;
      }
      .grid {
        display: grid;
        grid-template-columns: repeat(3, 50mm);
        grid-auto-rows: 40mm;
        gap: 2mm;
        justify-content: start;
      }
      .label-card {
        border: 1px solid #ddd;
        border-radius: 3mm;
        padding: 1.8mm 2.2mm;
        width: 50mm;
        height: 40mm;
        box-sizing: border-box;
        display: grid;
        grid-template-rows: auto auto auto auto 1fr;
        row-gap: 0.9mm;
      }
      .line {
        font-weight: 700;
        line-height: 1.15;
        word-break: break-all;
        margin: 0;
      }
      .customer {
        font-size: 4.0mm;
      }
      .order {
        font-size: 5.0mm;
        letter-spacing: 0;
      }
      .product {
        font-size: 4.5mm;
      }
      .spec {
        font-size: 5.0mm;
        letter-spacing: 0;
      }
      .address {
        font-size: 3.8mm;
      }
      @media print {
        @page {
          size: A4 portrait;
          margin: 0;
        }
        .page {
          width: 100%;
          min-height: 0;
          margin: 0;
          padding: 8mm 7mm;
        }
      }
    </style>
  </head>
  <body>
    ${pages
      .map(
        (pageList) => `
      <div class="page">
        <div class="grid">
          ${pageList.map((item) => renderLabelCard(item)).join("")}
        </div>
      </div>
    `
      )
      .join("")}
  </body>
</html>`;
  printWindow.document.write(html);
  printWindow.document.close();
  printWindow.onload = () => {
    setTimeout(() => {
      printWindow.focus();
      printWindow.print();
      printWindow.close();
    }, 300);
  };
};
src/views/salesManagement/salesLedger/components/salesOrderPrint.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,443 @@
const PRINT_TITLE = "销售订单";
const formatDisplayDate = (value) => {
  if (!value) return "";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return String(value);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
};
const getCurrentDateTime = () => {
  const date = new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}/${month}/${day}`;
};
const escapeHtml = (value) =>
  String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
const toNumber = (value) => {
  const num = Number(value);
  return Number.isFinite(num) ? num : 0;
};
const formatMoney = (value) => {
  const num = toNumber(value);
  return num.toFixed(2);
};
const formatArea = (value) => {
  const num = toNumber(value);
  return num ? num.toFixed(2) : "";
};
const formatQty = (value) => {
  const num = toNumber(value);
  return num ? String(num) : "";
};
const getItemAmount = (item) => {
  const fromPrice = toNumber(item?.taxInclusiveUnitPrice) * toNumber(item?.quantity);
  const amount = toNumber(item?.taxInclusiveTotalPrice || item?.amount || fromPrice);
  return amount;
};
const getItemArea = (item) =>
  toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea);
const getDeliveryAddress = (data) =>
  data?.companyAddress || data?.deliveryAddress || data?.shippingAddress || data?.address || data?.shipAddress || "";
const normalizeRequirementText = (value) => {
  const text = String(value ?? "").trim();
  if (!text) return "";
  return text
    .replace(/^加工要求和备注[::]\s*/g, "")
    .replace(/^加工要求[::]\s*/g, "")
    .replace(/^备注[::]\s*/g, "");
};
const extractOtherFees = (data, items) => {
  const source = [];
  if (Array.isArray(data?.otherFees)) source.push(...data.otherFees);
  if (Array.isArray(data?.otherAmounts)) source.push(...data.otherAmounts);
  if (Array.isArray(data?.otherAmountList)) source.push(...data.otherAmountList);
  if (Array.isArray(data?.otherAmountProjects)) source.push(...data.otherAmountProjects);
  if (Array.isArray(data?.salesProductProcessList)) source.push(...data.salesProductProcessList);
  (Array.isArray(items) ? items : []).forEach((item) => {
    if (Array.isArray(item?.salesProductProcessList)) source.push(...item.salesProductProcessList);
    if (Array.isArray(item?.otherAmounts)) source.push(...item.otherAmounts);
  });
  const map = new Map();
  source.forEach((fee) => {
    const name = String(fee?.feeName || fee?.processName || fee?.name || fee?.itemName || "").trim();
    if (!name) return;
    const quantity = toNumber(fee?.quantity || fee?.num);
    const unitPrice = toNumber(fee?.unitPrice || fee?.price);
    const amount =
      toNumber(fee?.amount || fee?.totalPrice) || (quantity && unitPrice ? quantity * unitPrice : 0);
    const key = name;
    if (!map.has(key)) {
      map.set(key, { name, quantity: 0, unitPrice: 0, amount: 0 });
    }
    const row = map.get(key);
    row.quantity += quantity;
    row.unitPrice = unitPrice || row.unitPrice;
    row.amount += amount;
  });
  return Array.from(map.values());
};
const renderOtherFeeRows = (rows) => {
  const list = Array.isArray(rows) ? rows : [];
  if (list.length === 0) {
    return `<tr><td></td><td></td><td></td><td></td></tr>`;
  }
  return list
    .map((row) => {
      const name = escapeHtml(row.name);
      const unitPrice = row.unitPrice ? formatMoney(row.unitPrice) : "";
      const quantity = row.quantity ? String(row.quantity) : "";
      const amount = row.amount ? formatMoney(row.amount) : "";
      return `<tr><td class="other-fee-shift-left">${name}</td><td class="other-fee-shift-left">${unitPrice}</td><td class="other-fee-shift-left">${quantity}</td><td>${amount}</td></tr>`;
    })
    .join("");
};
const splitItemsByPage = (items, pageSize) => {
  const list = Array.isArray(items) ? items : [];
  if (list.length === 0) return [[]];
  const pages = [];
  for (let i = 0; i < list.length; i += pageSize) {
    pages.push(list.slice(i, i + pageSize));
  }
  return pages;
};
const renderRows = (items, startIndex) => {
  const list = Array.isArray(items) ? items : [];
  if (list.length === 0) {
    return `
      <tr>
        <td colspan="8" class="empty">暂无明细</td>
      </tr>
    `;
  }
  return list
    .map((item, idx) => {
      const width = escapeHtml(item?.width);
      const height = escapeHtml(item?.height);
      const sizeText = width || height ? `${width}*${height}` : "";
      const unitPrice = formatMoney(item?.taxInclusiveUnitPrice || item?.unitPrice);
      const amount = formatMoney(getItemAmount(item));
      return `
      <tr>
        <td>${startIndex + idx + 1}</td>
        <td>${escapeHtml(item?.floorCode)}</td>
        <td>${sizeText}</td>
        <td>${formatQty(item?.quantity)}</td>
        <td>${formatArea(item?.area || item?.settleTotalArea || item?.actualTotalArea)}</td>
        <td>${unitPrice}</td>
        <td>${amount}</td>
        <td>${escapeHtml(item?.processRequirement)}</td>
      </tr>
    `;
    })
    .join("");
};
export const printSalesOrder = (orderData) => {
  const data = orderData ?? {};
  const items = Array.isArray(data.items) ? data.items : [];
  const pageSize = 15;
  const pages = splitItemsByPage(items, pageSize);
  const totalPages = pages.length;
  const subtotalQuantity = toNumber(data.subtotalQuantity);
  const subtotalArea = toNumber(data.subtotalArea);
  const subtotalAmount = toNumber(data.subtotalAmount);
  const totalQuantity = toNumber(data.totalQuantity) || items.reduce((sum, item) => sum + toNumber(item?.quantity), 0);
  const totalArea = toNumber(data.totalArea) || items.reduce((sum, item) => sum + getItemArea(item), 0);
  const totalAmount = toNumber(data.totalAmount) || items.reduce((sum, item) => sum + getItemAmount(item), 0);
  const otherFees = extractOtherFees(data, items);
  const printWindow = window.open("", "_blank", "width=1200,height=900");
  if (!printWindow) {
    throw new Error("浏览器拦截了弹窗,请允许弹窗后重试");
  }
  const html = `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>${PRINT_TITLE}</title>
    <style>
      body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #111; }
      .page {
        width: 198mm;
        min-height: 186mm;
        margin: 0 auto;
        padding: 5mm 4mm 16mm;
        box-sizing: border-box;
        page-break-after: always;
        position: relative;
      }
      .page:last-child { page-break-after: auto; }
      .title-main { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 1px; line-height: 1.15; margin-top: 1mm; }
      .title-sub { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 6px; line-height: 1.15; margin: 1mm 0 4mm; }
      table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; }
      .detail-table {
        border-right: 1px solid #222 !important;
      }
      .detail-table-wrap {
        position: relative;
        margin-top: -1px;
      }
      .detail-table-wrap::after {
        content: none;
      }
      .sheet-wrap {
        position: relative;
        overflow: visible;
      }
      .sheet-wrap::after {
        content: none;
      }
      td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; }
      tr > td:first-child, tr > th:first-child { border-left: 1px solid #222 !important; }
      tr > td:last-child, tr > th:last-child { border-right: 1px solid #222 !important; }
      .left { text-align: left; }
      .bold { font-weight: 700; }
      .cell-title { font-weight: 700; white-space: nowrap; }
      .product-row td { font-size: 13px; font-weight: 700; }
      .empty { height: 140px; color: #777; }
      .large-row td { height: 46px; vertical-align: top; }
      .other-fee-content-row td { height: auto !important; }
      .total-row td { font-weight: 700; font-size: 13px; line-height: 1.2; }
      .footer { margin-top: 5px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; font-size: 13px; line-height: 1.7; }
      .footer b { display: inline-block; min-width: 74px; }
      .customer-sign-row {
        margin-top: 6px;
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
      }
      .customer-sign-cell { text-align: left; }
      .customer-sign { font-size: 13px; font-weight: 700; line-height: 1.2; }
      .right { text-align: right; }
      .footer-page-right { display: block; text-align: right; font-size: 12px; }
      .footer-pair {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
      }
      .other-fee-cell { white-space: normal; word-break: break-all; line-height: 1.35; vertical-align: top; }
      .other-fee-header { font-weight: 700; }
      .other-fee-header-grid {
        display: grid;
        grid-template-columns: repeat(4, 1fr);
        align-items: center;
        text-align: center;
      }
      .other-fee-header-row td { border-bottom: none !important; }
      .other-fee-content-row td { border-top: none !important; }
      .other-fee-inner {
        width: 100%;
        border-collapse: collapse;
        table-layout: fixed;
        border: none !important;
      }
      .other-fee-inner td {
        border: none !important;
        padding: 0 1px;
        text-align: center;
        vertical-align: middle;
        white-space: normal;
        word-break: break-all;
        line-height: 1.1;
      }
      .other-fee-inner tr > td:first-child,
      .other-fee-inner tr > td:last-child {
        border-left: none !important;
        border-right: none !important;
      }
      .other-fee-shift-left {
        position: relative;
        left: -30px;
      }
      @media print {
        @page { size: A4 landscape; margin: 6mm; }
        .page { width: 100%; margin: 0; padding: 0 0 14mm; min-height: 0; }
      }
    </style>
  </head>
  <body>
    ${pages
      .map((pageItems, pageIndex) => {
        const isLastPage = pageIndex === totalPages - 1;
        const startIndex = pageIndex * pageSize;
        return `
    <div class="page">
      <div class="title-main">${escapeHtml(data.companyName || "鹤壁天沐钢化玻璃厂")}</div>
      <div class="title-sub">销售订单</div>
      <div class="sheet-wrap">
      <table>
        <colgroup>
          <col style="width: 14%;" />
          <col style="width: 20%;" />
          <col style="width: 20%;" />
          <col style="width: 14%;" />
          <col style="width: 32%;" />
        </colgroup>
        <tr>
          <td class="cell-title">客户名称:</td>
          <td class="left">${escapeHtml(data.customerName)}</td>
          <td class="cell-title">项目名称:</td>
          <td class="left">${escapeHtml(data.projectName)}</td>
          <td class="left"><span class="cell-title">业 åŠ¡ å‘˜:</span> ${escapeHtml(data.salesman)}</td>
        </tr>
        <tr>
          <td class="cell-title">制单日期:</td>
          <td class="left">${escapeHtml(formatDisplayDate(data.executionDate || data.orderMakerDate || data.registerDate || data.entryDate))}</td>
          <td class="cell-title">交货日期:</td>
          <td class="left">${escapeHtml(formatDisplayDate(data.deliveryDate))}</td>
          <td class="left"></td>
        </tr>
        <tr>
          <td class="cell-title">送货地址:</td>
          <td colspan="4" class="left">${escapeHtml(getDeliveryAddress(data))}</td>
        </tr>
      </table>
      <div class="detail-table-wrap">
      <table class="detail-table">
        <colgroup>
          <col style="width: 9%;" />
          <col style="width: 12%;" />
          <col style="width: 12%;" />
          <col style="width: 7%;" />
          <col style="width: 10%;" />
          <col style="width: 8%;" />
          <col style="width: 10%;" />
          <col style="width: 32%;" />
        </colgroup>
        <tr>
          <th>序号</th>
          <th>楼层编号</th>
          <th>宽(弧长)*高</th>
          <th>数量</th>
          <th>结算面积</th>
          <th>单价</th>
          <th>金额</th>
          <th>加工要求</th>
        </tr>
        <tr class="product-row">
          <td colspan="6" class="left">产品名称: ${escapeHtml(data.productName || items[0]?.productDescription)}</td>
          <td colspan="2" class="left">订单编号: ${escapeHtml(data.salesContractNo)}</td>
        </tr>
        ${renderRows(pageItems, startIndex)}
        ${
          isLastPage
            ? `
        <tr class="total-row">
          <td colspan="3" class="left">小计:</td>
          <td>${subtotalQuantity || totalQuantity || ""}</td>
          <td>${subtotalArea ? subtotalArea.toFixed(2) : totalArea ? totalArea.toFixed(2) : ""}</td>
          <td></td>
          <td>${formatMoney(subtotalAmount || totalAmount)}</td>
          <td></td>
        </tr>
        <tr class="total-row">
          <td colspan="3" class="left">合计:</td>
          <td>${totalQuantity || ""}</td>
          <td>${totalArea ? totalArea.toFixed(2) : ""}</td>
          <td></td>
          <td>${formatMoney(totalAmount)}</td>
          <td></td>
        </tr>
        <tr class="other-fee-header-row">
          <td colspan="5" class="left other-fee-header">
            <div class="other-fee-header-grid">
              <span>其他费用</span>
              <span>单价</span>
              <span>数量</span>
              <span>金额</span>
            </div>
          </td>
          <td colspan="3" class="left other-fee-header">加工要求和备注:</td>
        </tr>
        <tr class="large-row other-fee-content-row">
          <td colspan="5" class="left other-fee-cell">
            <table class="other-fee-inner">
              <colgroup>
                <col style="width: 34%;" />
                <col style="width: 22%;" />
                <col style="width: 20%;" />
                <col style="width: 24%;" />
              </colgroup>
              ${renderOtherFeeRows(otherFees)}
            </table>
          </td>
          <td colspan="3" class="left other-fee-cell">${escapeHtml(normalizeRequirementText(data.remakes || data.remarks || data.orderProcessRequirement))}</td>
        </tr>
        <tr class="total-row">
          <td colspan="8" class="left">总金额: ${escapeHtml(data.totalAmountDisplay || `${formatMoney(totalAmount)}元`)}</td>
        </tr>
            `
            : `
        <tr><td colspan="8" class="right">下页续...</td></tr>
            `
        }
      </table>
      </div>
      </div>
      ${
        isLastPage
          ? `
      <div class="customer-sign-row">
        <span></span>
        <span></span>
        <span class="customer-sign customer-sign-cell">客户签名:</span>
      </div>
      <div class="footer">
        <div><b>制单员:</b>${escapeHtml(data.orderMaker || data.register)}</div>
        <div><b>审核员:</b>${escapeHtml(data.auditor)}</div>
        <div><b>打印人:</b>${escapeHtml(data.printPeople || data.register)}</div>
        <div><b>制单日期:</b>${escapeHtml(formatDisplayDate(data.orderMakerDate || data.executionDate || data.registerDate || data.entryDate))}</div>
        <div><b>审核日期:</b>${escapeHtml(formatDisplayDate(data.auditDate))}</div>
        <div class="footer-pair"><span><b>打印时间:</b>${escapeHtml(data.printTime || getCurrentDateTime())}</span><span class="footer-page-right">第${pageIndex + 1}页,共${totalPages}页</span></div>
      </div>
          `
          : ""
      }
    </div>`;
      })
      .join("")}
  </body>
</html>
`;
  printWindow.document.write(html);
  printWindow.document.close();
  printWindow.onload = () => {
    setTimeout(() => {
      printWindow.focus();
      printWindow.print();
      printWindow.close();
    }, 300);
  };
};
src/views/salesManagement/salesLedger/index.vue
@@ -25,18 +25,39 @@
    </div>
    <div class="table_list">
      <div class="actions">
        <div></div>
        <div>
          <OtherAmountMaintenanceButton />
          <ProcessFlowMaintenanceButton />
        </div>
        <ProcessFlowConfigSelectDialog
          v-model:visible="processFlowSelectDialogVisible"
          :default-route-id="processFlowSelectDefaultRouteId"
          :bound-route-name="processFlowSelectBoundRouteName"
          @confirm="handleProcessFlowSelectConfirm"
        />
        <div>
          <el-button type="primary" @click="openForm('add')">
            æ–°å¢žå°è´¦
          </el-button>
          <el-button type="primary" plain @click="openOtherAmountDialog">
            å…¶ä»–金额维护
          <el-button type="primary"  @click="handleBulkDelivery">
            å‘è´§
          </el-button>
          <el-button type="primary" plain @click="handleImport">导入</el-button>
          <el-button @click="handleOut">导出</el-button>
          <el-button type="danger" plain @click="handleDelete">删除</el-button>
          <el-button type="primary" plain @click="handlePrint">打印</el-button>
          <el-dropdown @command="handlePrintCommand">
            <el-button type="primary" plain>
              æ‰“印单据<el-icon class="el-icon--right"><ArrowDown /></el-icon>
            </el-button>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="finishedProcessCard">生产流程卡(成品)</el-dropdown-item>
                <el-dropdown-item command="salesOrder">销售订单</el-dropdown-item>
                <el-dropdown-item command="salesDeliveryNote">销售发货单</el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
          <el-button type="primary" plain @click="handlePrintLabel">打印标签</el-button>
        </div>
      </div>
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
@@ -49,7 +70,11 @@
              <el-table-column align="center" label="序号" type="index"/>
              <el-table-column label="产品大类" prop="productCategory" />
              <el-table-column label="规格型号" prop="specificationModel" />
              <el-table-column label="单位" prop="unit" />
              <el-table-column label="厚度" prop="thickness" min-width="90">
                <template #default="scope">
                  {{ scope.row.thickness ?? "" }}
                </template>
              </el-table-column>
                            <el-table-column label="产品状态"
                                                             width="100px"
                                                             align="center">
@@ -96,7 +121,7 @@
              <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
              <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
            <!--操作-->
              <el-table-column Width="60px" label="操作" align="center">
              <!-- <el-table-column Width="60px" label="操作" align="center">
                <template #default="scope">
                  <el-button 
                    link 
@@ -106,7 +131,7 @@
                    å‘è´§
                  </el-button>
                </template>
              </el-table-column>
              </el-table-column> -->
            </el-table>
          </template>
        </el-table-column>
@@ -123,9 +148,10 @@
        <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip />
        <el-table-column label="交付日期" prop="deliveryDate" width="120" show-overflow-tooltip />
        <el-table-column label="备注" prop="remarks" width="200" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" width="130" align="center">
        <el-table-column fixed="right" label="操作" width="200" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="openForm('edit', scope.row)" :disabled="!scope.row.isEdit">编辑</el-button>
            <el-button link type="primary" @click="openProcessFlowSelect(scope.row)" :disabled="!scope.row.isEdit">工艺路线</el-button>
            <el-button link type="primary" @click="downLoadFile(scope.row)">附件</el-button>
          </template>
        </el-table-column>
@@ -229,7 +255,11 @@
                    <el-table-column align="center" label="序号" type="index" width="60" />
                    <el-table-column label="产品大类" prop="productCategory" />
                    <el-table-column label="规格型号" prop="specificationModel" />
                    <el-table-column label="单位" prop="unit" />
                    <el-table-column label="厚度" prop="thickness" min-width="90">
                        <template #default="scope">
                            {{ scope.row.thickness ?? "" }}
                        </template>
                    </el-table-column>
                    <el-table-column label="数量" prop="quantity" />
                    <el-table-column label="税率(%)" prop="taxRate" />
                    <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
@@ -247,6 +277,20 @@
                    <el-col :span="24">
                        <el-form-item label="备注:" prop="remarks">
                            <el-input v-model="form.remarks" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="客户备注:" prop="customerRemarks">
                            <el-input
                                v-model="form.customerRemarks"
                                placeholder="请输入"
                                clearable
                                type="textarea"
                                :rows="2"
                                :disabled="operationType === 'view'"
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
@@ -374,8 +418,16 @@
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="单位:" prop="unit">
                            <el-input v-model="productForm.unit" placeholder="请输入" clearable />
                        <el-form-item label="厚度:" prop="thickness">
                            <el-input-number
                                v-model="productForm.thickness"
                                :min="0"
                                :step="0.000000000000001"
                                :precision="15"
                                style="width: 100%;"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
@@ -564,6 +616,13 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="楼层编号:" prop="floorCode">
                            <el-input v-model="productForm.floorCode" placeholder="请输入楼层编号" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <!-- å…¶ä»–金额(占满一行:等同于 3 åˆ—网格的整行) -->
                <el-row :gutter="30">
                    <el-col :span="12">
@@ -684,6 +743,7 @@
                </el-button>
            </template>
        </el-dialog>
        <!-- å¯¼å…¥å¼¹çª— -->
        <FormDialog
            v-model="importUpload.open"
@@ -725,122 +785,6 @@
            v-model="fileListDialogVisible"
            title="附件列表"
        />
        <!-- æ‰“印预览弹窗 -->
        <el-dialog
            v-model="printPreviewVisible"
            title="打印预览"
            width="90%"
            :close-on-click-modal="false"
            class="print-preview-dialog"
        >
            <div class="print-preview-container">
                <div class="print-preview-header">
                    <el-button type="primary" @click="executePrint">执行打印</el-button>
                    <el-button @click="printPreviewVisible = false">关闭预览</el-button>
                </div>
                <div class="print-preview-content">
                    <div v-if="printData.length === 0" style="text-align: center; padding: 50px; color: #999;">
                        æš‚无打印数据
                    </div>
                    <div v-else style="text-align: center; padding: 10px; color: #666; font-size: 14px; background: #e8f4fd; margin-bottom: 10px;">
                        å…± {{ printData.length }} æ¡æ•°æ®å¾…打印
                    </div>
                    <div v-for="(item, index) in printData" :key="index" class="print-page">
                        <div class="delivery-note">
                            <div class="header">
                                <div class="document-title">零售发货单</div>
                            </div>
                            <div class="info-section">
                                <div class="info-row">
                                    <div>
                                        <span class="label">发货日期:</span>
                                        <span class="value">{{ formatDate(item.createTime) }}</span>
                                    </div>
                                    <div>
                                        <span class="label">发货车牌号:</span>
                                        <span class="value">{{ item.shippingCarNumber }}</span>
                                    </div>
                                </div>
                                <div class="info-row">
                                    <div>
                                        <span class="label">客户名称:</span>
                                        <span class="value">{{ item.customerName }}</span>
                                    </div>
                                    <span class="label">单号:</span>
                                    <span class="value">{{ item.salesContractNo }}</span>
                                </div>
                            </div>
                            <div class="table-section">
                                <table class="product-table">
                                    <thead>
                                    <tr>
                                        <th>产品名称</th>
                                        <th>规格型号</th>
                                        <th>单位</th>
                                        <th>单价</th>
                                        <th>零售数量</th>
                                        <th>零售金额</th>
                                    </tr>
                                    </thead>
                                    <tbody>
                                    <tr v-for="product in item.products" :key="product.id">
                                        <td>{{ product.productCategory || '' }}</td>
                                        <td>{{ product.specificationModel || '' }}</td>
                                        <td>{{ product.unit || '' }}</td>
                                        <td>{{ product.taxInclusiveUnitPrice || '0' }}</td>
                                        <td>{{ product.quantity || '0' }}</td>
                                        <td>{{ product.taxInclusiveTotalPrice || '0' }}</td>
                                    </tr>
                                    <tr v-if="!item.products || item.products.length === 0">
                                        <td colspan="6" style="text-align: center; color: #999;">暂无产品数据</td>
                                    </tr>
                                    </tbody>
                                    <tfoot>
                                    <tr>
                                        <td class="label">合计</td>
                                        <td class="total-value"></td>
                                        <td class="total-value"></td>
                                        <td class="total-value"></td>
                                        <td class="total-value">{{ getTotalQuantity(item.products) }}</td>
                                        <td class="total-value">{{ getTotalAmount(item.products) }}</td>
                                    </tr>
                                    </tfoot>
                                </table>
                            </div>
                            <div class="footer-section">
                                <div class="footer-row">
                                    <div class="footer-item">
                                        <span class="label">收货电话:</span>
                                        <span class="value"></span>
                                    </div>
                                    <div class="footer-item">
                                        <span class="label">收货人:</span>
                                        <span class="value"></span>
                                    </div>
                                    <div class="footer-item address-item">
                                        <span class="label">收货地址:</span>
                                        <span class="value address-value"></span>
                                    </div>
                                </div>
                                <div class="footer-row">
                                    <div class="footer-item">
                                        <span class="label">操作员:</span>
                                        <span class="value">{{ userStore.nickName || '撕开前' }}</span>
                                    </div>
                                    <div class="footer-item">
                                        <span class="label">打印日期:</span>
                                        <span class="value">{{ formatDateTime(new Date()) }}</span>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </el-dialog>
        <!-- å‘货弹框 -->
        <el-dialog
            v-model="deliveryFormVisible"
@@ -916,114 +860,6 @@
            </template>
        </el-dialog>
        <!-- å…¶ä»–金额维护(新增/编辑/删除) -->
        <el-dialog
            v-model="otherAmountDialogVisible"
            title="其他金额维护"
            width="80%"
            :close-on-click-modal="false"
            @close="closeOtherAmountDialog"
        >
            <el-row :gutter="20">
                <el-col :span="14">
                    <el-table
                        :data="otherAmountRecords"
                        border
                        v-loading="otherAmountLoading"
                        height="55vh"
                    >
                        <el-table-column label="编码" prop="code" min-width="120" show-overflow-tooltip />
                        <el-table-column label="项目" prop="processName" min-width="180" show-overflow-tooltip />
                        <el-table-column label="数量" prop="quantity" min-width="110" :formatter="formattedNumber" />
                        <el-table-column label="单价(元)" prop="unitPrice" min-width="130" :formatter="formattedNumber" />
                        <el-table-column label="金额(元)" prop="amount" min-width="160" :formatter="formattedNumber" />
                        <el-table-column fixed="right" label="操作" width="160" align="center">
                            <template #default="scope">
                                <el-button link type="primary" size="small" @click="handleOtherEdit(scope.row)">编辑</el-button>
                                <el-button link type="danger" size="small" @click="handleOtherDelete(scope.row)">删除</el-button>
                            </template>
                        </el-table-column>
                    </el-table>
                    <pagination
                        v-show="otherAmountTotal > 0"
                        :total="otherAmountTotal"
                        layout="total, sizes, prev, pager, next, jumper"
                        :page="otherAmountPage.current"
                        :limit="otherAmountPage.size"
                        @pagination="otherAmountPaginationChange"
                    />
                </el-col>
                <el-col :span="10">
                    <div style="padding: 8px 0;">
                        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;">
                            <div style="font-weight:600;">
                                {{ otherAmountOperationType === 'add' ? '新增其他金额' : '编辑其他金额' }}
                            </div>
                            <el-button
                                type="primary"
                                plain
                                size="small"
                                @click="handleOtherAdd"
                                :disabled="otherAmountOperationType === 'add'"
                            >
                                æ–°å¢ž
                            </el-button>
                        </div>
                        <el-form
                            :model="otherAmountForm"
                            label-width="120px"
                            label-position="top"
                            :rules="otherAmountRules"
                            ref="otherAmountFormRef"
                        >
                            <el-form-item label="编码">
                                <el-input v-model="otherAmountForm.code" placeholder="请输入编码(可选)" clearable />
                            </el-form-item>
                            <el-form-item label="项目" prop="processName">
                                <el-input v-model="otherAmountForm.processName" placeholder="请输入项目名称" clearable />
                            </el-form-item>
                            <el-form-item label="数量" prop="quantity">
                                <el-input-number
                                    v-model="otherAmountForm.quantity"
                                    :min="0"
                                    :precision="2"
                                    style="width:100%"
                                    placeholder="请输入数量"
                                    clearable
                                    @change="recalcOtherAmount"
                                />
                            </el-form-item>
                            <el-form-item label="单价(元)" prop="unitPrice">
                                <el-input-number
                                    v-model="otherAmountForm.unitPrice"
                                    :min="0"
                                    :precision="2"
                                    style="width:100%"
                                    placeholder="请输入单价"
                                    clearable
                                    @change="recalcOtherAmount"
                                />
                            </el-form-item>
                            <el-form-item label="金额(元)">
                                <el-input v-model="otherAmountForm.amount" disabled />
                            </el-form-item>
                            <div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 8px;">
                                <el-button @click="closeOtherAmountDialog">取消</el-button>
                                <el-button type="primary" @click="submitOtherAmountForm">保存</el-button>
                            </div>
                        </el-form>
                    </div>
                </el-col>
            </el-row>
        </el-dialog>
    </div>
</template>
@@ -1033,11 +869,14 @@
import {onMounted, ref, getCurrentInstance} from "vue";
import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js";
import { ElMessageBox, ElMessage } from "element-plus";
import { UploadFilled, Download } from "@element-plus/icons-vue";
import { ArrowDown } from "@element-plus/icons-vue";
import useUserStore from "@/store/modules/user";
import { userListNoPage } from "@/api/system/user.js";
import FileListDialog from '@/components/Dialog/FileListDialog.vue';
import FormDialog from '@/components/Dialog/FormDialog.vue';
import OtherAmountMaintenanceButton from "./components/OtherAmountMaintenanceButton.vue";
import ProcessFlowMaintenanceButton from "./components/ProcessFlowMaintenanceButton.vue";
import ProcessFlowConfigSelectDialog from "./components/ProcessFlowConfigSelectDialog.vue";
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import {
    ledgerListPage,
@@ -1051,14 +890,22 @@
    delLedgerFile,
    getProductInventory,
    salesLedgerProductProcessList,
    salesLedgerProductProcessAdd,
    salesLedgerProductProcessUpdate,
    salesLedgerProductProcessDelete,
    saleProcessBind,
    getSaleProcessBindInfo,
    getProcessCard,
    getSalesOrder,
    getSalesInvoices,
    getSalesLabel,
} from "@/api/salesManagement/salesLedger.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
import useFormData from "@/hooks/useFormData.js";
import dayjs from "dayjs";
import { getCurrentDate } from "@/utils/index.js";
import { printFinishedProcessCard } from "./components/processCardPrint.js";
import { printSalesOrder } from "./components/salesOrderPrint.js";
import { printSalesDeliveryNote } from "./components/salesDeliveryPrint.js";
import { printSalesLabel } from "./components/salesLabelPrint.js";
// import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js";
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
@@ -1077,6 +924,13 @@
});
const total = ref(0);
const fileList = ref([]);
// å·¥è‰ºè·¯çº¿é…ç½®é€‰æ‹©å¼¹çª—(绑定到台账产品)
const processFlowSelectDialogVisible = ref(false);
const processFlowSelectLedgerRow = ref(null);
const processFlowSelectDefaultRouteId = ref(null);
const processFlowSelectBoundRouteId = ref(null);
const processFlowSelectBoundRouteName = ref("");
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
@@ -1119,7 +973,7 @@
    productForm: {
        productCategory: "",
        specificationModel: "",
        unit: "",
        thickness:null,
        quantity: "",
        taxInclusiveUnitPrice: "",
        taxRate: "",
@@ -1138,6 +992,8 @@
        processRequirement: "", // åŠ å·¥è¦æ±‚
        remark: "", // å¤‡æ³¨
        salesProductProcessList: [], // å…¶ä»–金额:[{id, processName, quantity}]
        processFlowConfigId: null, // å·¥è‰ºæµç¨‹é…ç½®ç»‘定
        floorCode: "", // æ¥¼å±‚编号
    },
    productRules: {
        productCategory: [{ required: true, message: "请选择", trigger: "change" }],
@@ -1145,7 +1001,7 @@
        specificationModel: [
            { required: true, message: "请选择", trigger: "change" },
        ],
        unit: [{ required: true, message: "请输入", trigger: "blur" }],
        thickness: [{ required: true, message: "请输入", trigger: "blur" }],
        quantity: [{ required: true, message: "请输入", trigger: "blur" }],
        taxInclusiveUnitPrice: [
            { required: true, message: "请输入", trigger: "blur" },
@@ -1169,10 +1025,6 @@
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
});
// æ‰“印相关
const printPreviewVisible = ref(false);
const printData = ref([]);
// æŠ¥ä»·å•导入相关
const quotationDialogVisible = ref(false);
const quotationLoading = ref(false);
@@ -1191,7 +1043,7 @@
// å‘货相关
const deliveryFormVisible = ref(false);
const currentDeliveryRow = ref(null);
const currentDeliveryRows = ref([]);
const deliveryFormData = reactive({
  deliveryForm: {
    type: "货车", // è´§è½¦, å¿«é€’
@@ -1203,169 +1055,6 @@
  },
});
const { deliveryForm, deliveryRules } = toRefs(deliveryFormData);
// å…¶ä»–金额维护(工序/流程金额维护)
const otherAmountDialogVisible = ref(false);
const otherAmountLoading = ref(false);
const otherAmountRecords = ref([]);
const otherAmountTotal = ref(0);
const otherAmountPage = reactive({
    current: 1,
    size: 10,
});
const otherAmountOperationType = ref("add"); // add/edit
const otherAmountFormRef = ref(null);
const otherAmountForm = reactive({
    id: null,
    code: "", // å‰ç«¯å­—段名:code;后端接口列表返回 remark,此处进行映射
    processName: "",
    quantity: 0,
    unitPrice: 0,
    amount: "0.00",
});
const otherAmountRules = reactive({
    processName: [{ required: true, message: "请输入项目名称", trigger: "change" }],
    quantity: [{ required: true, message: "请输入数量", trigger: "blur" }],
    unitPrice: [{ required: true, message: "请输入单价", trigger: "blur" }],
});
const recalcOtherAmount = () => {
    const quantity = Number(otherAmountForm.quantity ?? 0) || 0;
    const unitPrice = Number(otherAmountForm.unitPrice ?? 0) || 0;
    otherAmountForm.amount = (quantity * unitPrice).toFixed(2);
};
const resetOtherAmountForm = (type = "add") => {
    otherAmountOperationType.value = type;
    otherAmountForm.id = null;
    otherAmountForm.code = "";
    otherAmountForm.processName = "";
    otherAmountForm.quantity = 0;
    otherAmountForm.unitPrice = 0;
    otherAmountForm.amount = "0.00";
};
const openOtherAmountDialog = () => {
    otherAmountDialogVisible.value = true;
    resetOtherAmountForm("add");
    // æ‰“开弹框时刷新数据,避免长时间停留导致数据过期
    otherAmountPage.current = otherAmountPage.current || 1;
    fetchOtherAmountList();
};
const closeOtherAmountDialog = () => {
    otherAmountDialogVisible.value = false;
    resetOtherAmountForm("add");
};
const fetchOtherAmountList = async () => {
    otherAmountLoading.value = true;
    try {
        const params = {
            current: otherAmountPage.current,
            size: otherAmountPage.size,
        };
        const res = await salesLedgerProductProcessList(params);
        // å…¼å®¹ä¸åŒæŽ¥å£å“åº”结构:可能是 res.records / res.total æˆ– res.data.records / res.data.total
        const records = res?.records ?? res?.data?.records ?? [];
        const total = res?.total ?? res?.data?.total ?? 0;
        otherAmountRecords.value = records.map((item) => {
            const quantity = Number(item.quantity ?? 0) || 0;
            const unitPrice = Number(item.unitPrice ?? 0) || 0;
            const amount = Number(item.amount ?? quantity * unitPrice) || 0;
            return {
                id: item.id,
                code: item.code ?? item.remark ?? "",
                processName: item.processName ?? "",
                quantity,
                unitPrice,
                amount: amount.toFixed(2),
            };
        });
        otherAmountTotal.value = total;
    } finally {
        otherAmountLoading.value = false;
    }
};
const otherAmountPaginationChange = (obj) => {
    otherAmountPage.current = obj.page;
    otherAmountPage.size = obj.limit;
    fetchOtherAmountList();
};
const handleOtherAdd = () => {
    resetOtherAmountForm("add");
};
const handleOtherEdit = (row) => {
    if (!row) return;
    otherAmountOperationType.value = "edit";
    otherAmountForm.id = row.id ?? null;
    otherAmountForm.code = row.code ?? "";
    otherAmountForm.processName = row.processName ?? "";
    otherAmountForm.quantity = Number(row.quantity ?? 0) || 0;
    otherAmountForm.unitPrice = Number(row.unitPrice ?? 0) || 0;
    recalcOtherAmount();
};
const submitOtherAmountForm = () => {
    otherAmountFormRef.value?.validate((valid) => {
        if (!valid) return;
        const payload = {
            processName: otherAmountForm.processName,
            quantity: Number(otherAmountForm.quantity) || 0,
            unitPrice: Number(otherAmountForm.unitPrice) || 0,
            amount: Number(otherAmountForm.amount) || 0,
            // åˆ—表返回字段是 remark,这里按“code=remark”做映射
            remark: otherAmountForm.code,
            // å…¼å®¹åŽç«¯å¯èƒ½ç›´æŽ¥ä½¿ç”¨ code å­—段
            code: otherAmountForm.code,
        };
        if (otherAmountOperationType.value === "edit") {
            payload.id = otherAmountForm.id;
            salesLedgerProductProcessUpdate(payload).then(() => {
                proxy.$modal.msgSuccess("保存成功");
                fetchOtherAmountList();
                resetOtherAmountForm("add");
            });
        } else {
            salesLedgerProductProcessAdd(payload).then(() => {
                proxy.$modal.msgSuccess("保存成功");
                fetchOtherAmountList();
                resetOtherAmountForm("add");
            });
        }
    });
};
const handleOtherDelete = (row) => {
    if (!row?.id) return;
    ElMessageBox.confirm("确认删除该记录?", "删除", {
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning",
    })
        .then(() => {
            return salesLedgerProductProcessDelete(row.id).then(() => {
                proxy.$modal.msgSuccess("删除成功");
                fetchOtherAmountList();
                if (otherAmountOperationType.value === "edit" && otherAmountForm.id === row.id) {
                    resetOtherAmountForm("add");
                }
            });
        })
        .catch(() => {
            proxy.$modal.msg("已取消");
        });
};
// äº§å“å¼¹æ¡†ï¼šå…¶ä»–金额多选下拉(基于“其他金额维护”查询接口)
const otherAmountSelectOptions = ref([]); // [{id, processName}]
@@ -1636,6 +1325,86 @@
            tableLoading.value = false;
        });
};
// æ‰“开“工艺路线配置”选择弹窗(必须显式选择)
const openProcessFlowSelect = async (ledgerRow) => {
    if (!ledgerRow) return;
    if (!ledgerRow.isEdit) return;
    processFlowSelectLedgerRow.value = ledgerRow;
    processFlowSelectDefaultRouteId.value = null;
    processFlowSelectBoundRouteId.value = null;
    processFlowSelectBoundRouteName.value = "";
    try {
        const res = await getSaleProcessBindInfo(ledgerRow.id);
        const info = res?.data ?? res ?? {};
        const boundId =
            info?.processRouteId ??
            info?.routeId ??
            info?.id ??
            null;
        const boundName =
            info?.processRouteName ??
            info?.routeName ??
            info?.name ??
            "";
        processFlowSelectBoundRouteId.value = boundId;
        processFlowSelectBoundRouteName.value = boundName;
        processFlowSelectDefaultRouteId.value = boundId;
    } catch (e) {
        // æŸ¥è¯¢å¤±è´¥æ—¶æŒ‰æœªç»‘定处理,不阻塞弹窗
        processFlowSelectBoundRouteId.value = null;
        processFlowSelectBoundRouteName.value = "";
        processFlowSelectDefaultRouteId.value = null;
    }
    processFlowSelectDialogVisible.value = true;
};
// ç»‘定工艺路线到当前台账数据
const handleProcessFlowSelectConfirm = async (routeId) => {
    const ledgerRow = processFlowSelectLedgerRow.value;
    if (!ledgerRow?.id) return;
    const finalRouteId = routeId ?? null;
    if (!finalRouteId) return;
    const oldRouteId = processFlowSelectBoundRouteId.value;
    if (oldRouteId !== null && oldRouteId !== undefined && oldRouteId !== "" && String(oldRouteId) !== String(finalRouteId)) {
        try {
            await ElMessageBox.confirm(
                "该订单已绑定工艺路线,是否确定更换?",
                "提示",
                {
                    confirmButtonText: "确定",
                    cancelButtonText: "取消",
                    type: "warning",
                }
            );
        } catch {
            return;
        }
    }
    proxy?.$modal?.loading?.("正在绑定工艺路线,请稍候...");
    try {
        await saleProcessBind({
            salesLedgerId: ledgerRow.id,
            processRouteId: finalRouteId,
        });
        proxy?.$modal?.msgSuccess?.("工艺路线绑定成功");
        processFlowSelectDialogVisible.value = false;
        // ç»‘定后刷新列表,确保操作列再次点击能回显绑定
        await getList();
    } catch (e) {
        proxy?.$modal?.msgError?.("绑定失败,请稍后重试");
    } finally {
        proxy?.$modal?.closeLoading?.();
    }
};
// èŽ·å–äº§å“å¤§ç±»tree数据
const getProductOptions = () => {
    // è¿”回 Promise,便于在编辑产品时等待加载完成
@@ -1658,10 +1427,8 @@
    const index = modelOptions.value.findIndex((item) => item.id === value);
    if (index !== -1) {
        productForm.value.specificationModel = modelOptions.value[index].model;
        productForm.value.unit = modelOptions.value[index].unit;
    } else {
        productForm.value.specificationModel = null;
        productForm.value.unit = null;
    }
};
const findNodeById = (nodes, productId) => {
@@ -1784,11 +1551,14 @@
        form.value.entryDate = getCurrentDate();
        // ç­¾è®¢æ—¥æœŸé»˜è®¤ä¸ºå½“天
        form.value.executionDate = getCurrentDate();
        form.value.customerRemarks = "";
    } else {
        currentId.value = row.id;
        getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => {
            form.value = { ...res };
            form.value.entryPerson = Number(res.entryPerson);
            // å­—段名兼容:后端可能返回 customer_remarks
            form.value.customerRemarks = res?.customerRemarks ?? res?.customer_remarks ?? "";
            productData.value = form.value.productData;
            fileList.value = form.value.salesLedgerFiles;
        });
@@ -1886,7 +1656,7 @@
            // å°è´¦å­—段
            productCategory: p.product || p.productName || "",
            specificationModel: p.specification || "",
            unit: p.unit || "",
            thickness: p.thickness,
            quantity: quantity,
            taxRate: taxRate,
            taxInclusiveUnitPrice: unitPrice.toFixed(2),
@@ -1901,6 +1671,7 @@
            settlePieceArea: 0,
            settleTotalArea: 0,
            processRequirement: "",
            floorCode: "",
            remark: "",
            salesProductProcessList: [],
        };
@@ -2007,10 +1778,17 @@
        productForm.value.processRequirement =
            row?.processRequirement ?? row?.process_requirement ?? "";
        productForm.value.remark = row?.remark ?? row?.remarks ?? "";
        productForm.value.floorCode = row?.floorCode ?? row?.floor_code ?? "";
        // å·¥è‰ºæµç¨‹é…ç½®ç»‘定字段(后续由后端确认字段名)
        productForm.value.processFlowConfigId =
            row?.processFlowConfigId ?? row?.process_flow_config_id ?? null;
        // å‘¨é•¿å›žæ˜¾ï¼ˆå¦‚后端返回;最终仍以公式计算为准)
        productForm.value.perimeter =
            row?.perimeter ?? row?.heavyBoxPerimeter ?? row?.heavyboxPerimeter ?? 0;
        // åŽç«¯ç›´æŽ¥è¿”回 thickness
        productForm.value.thickness = row?.thickness;
        productForm.value.salesProductProcessList = normalizeOtherAmountsFromRow(row);
        productIndex.value = index;
@@ -2055,6 +1833,11 @@
const submitProduct = () => {
    proxy.$refs["productFormRef"].validate((valid) => {
        if (valid) {
            // åŽšåº¦ä¿ç•™ 15 ä½å°æ•°ï¼Œé¿å…ç”±äºŽæµ®ç‚¹è®¡ç®—/输入导致精度偏差
            if (productForm.value.thickness !== null && productForm.value.thickness !== undefined) {
                productForm.value.thickness = Number(Number(productForm.value.thickness).toFixed(15));
            }
            // é¢ç§¯/总计字段在提交前兜底计算一次
            recalcAreaTotals();
            // å…¶ä»–金额只提交 {id, processName, quantity}(后端字段:salesProductProcessList)
@@ -2248,364 +2031,118 @@
        });
};
// æ‰“印功能
const handlePrint = async () => {
    if (selectedRows.value.length === 0) {
        proxy.$modal.msgWarning("请选择要打印的数据");
const handlePrintCommand = async (command) => {
    if (command !== "finishedProcessCard" && command !== "salesOrder" && command !== "salesDeliveryNote") return;
    if (command === "salesDeliveryNote") {
        if (selectedRows.value.length === 0) {
            proxy.$modal.msgWarning("请至少选择一条销售台账数据进行打印");
            return;
        }
        const customerNames = Array.from(
            new Set(selectedRows.value.map((item) => String(item?.customerName ?? "").trim()))
        );
        if (customerNames.length > 1) {
            proxy.$modal.msgWarning("仅支持相同客户名称的销售台账合并发货打印");
            return;
        }
    } else if (selectedRows.value.length !== 1) {
        proxy.$modal.msgWarning("请选择一条销售台账数据进行打印");
        return;
    }
    // æ˜¾ç¤ºåŠ è½½çŠ¶æ€
    proxy.$modal.loading("正在获取产品数据,请稍候...");
    try {
        // ä¸ºæ¯ä¸ªé€‰ä¸­çš„销售台账记录查询对应的产品数据
        const printDataWithProducts = [];
        for (const row of selectedRows.value) {
            try {
                // è°ƒç”¨productList接口查询产品数据
                const productRes = await productList({ salesLedgerId: row.id, type: 1 });
                // å°†äº§å“æ•°æ®æ•´åˆåˆ°é”€å”®å°è´¦è®°å½•中
                const rowWithProducts = {
                    ...row,
                    products: productRes.data || []
                };
                printDataWithProducts.push(rowWithProducts);
            } catch (error) {
                console.error(`获取销售台账 ${row.id} çš„产品数据失败:`, error);
                // å³ä½¿æŸä¸ªè®°å½•的产品数据获取失败,也要包含该记录
                printDataWithProducts.push({
                    ...row,
                    products: []
                });
            }
    const selectedRow = selectedRows.value[0];
    const selectedId = selectedRow?.id;
    if (command === "salesDeliveryNote") {
        const selectedIds = selectedRows.value
            .map((item) => item?.id)
            .filter((id) => id !== null && id !== undefined && id !== "");
        if (selectedIds.length !== selectedRows.value.length) {
            proxy.$modal.msgWarning("当前选择数据存在缺少ID的记录,无法打印");
            return;
        }
        printData.value = printDataWithProducts;
        console.log('打印数据(包含产品):', printData.value);
        printPreviewVisible.value = true;
        const loadingText =
            command === "salesOrder"
                ? "正在获取销售订单数据,请稍候..."
                : command === "salesDeliveryNote"
                    ? "正在获取销售发货单数据,请稍候..."
                    : "正在获取生产流程卡数据,请稍候...";
        proxy.$modal.loading(loadingText);
        try {
            const res = await getSalesInvoices(selectedIds);
            const salesInvoiceData = res?.data ?? {};
            printSalesDeliveryNote(salesInvoiceData, selectedRow);
        } catch (error) {
            console.error("打印销售发货单失败:", error);
            proxy.$modal.msgError("打印失败,请稍后重试");
        } finally {
            proxy.$modal.closeLoading();
        }
        return;
    }
    if (!selectedId) {
        proxy.$modal.msgWarning("当前选择数据缺少ID,无法打印");
        return;
    }
    const loadingText =
        command === "salesOrder"
            ? "正在获取销售订单数据,请稍候..."
            : command === "salesDeliveryNote"
                ? "正在获取销售发货单数据,请稍候..."
                : "正在获取生产流程卡数据,请稍候...";
    proxy.$modal.loading(loadingText);
    try {
        if (command === "salesOrder") {
            const res = await getSalesOrder(selectedId);
            const salesOrderData = res?.data ?? {};
            printSalesOrder(salesOrderData);
        } else {
            const res = await getProcessCard(selectedId);
            const processCardData = res?.data ?? {};
            printFinishedProcessCard(processCardData);
        }
    } catch (error) {
        console.error('获取产品数据失败:', error);
        proxy.$modal.msgError("获取产品数据失败,请重试");
        console.error(
            command === "salesOrder"
                ? "打印销售订单失败:"
                : command === "salesDeliveryNote"
                    ? "打印销售发货单失败:"
                    : "打印生产流程卡失败:",
            error
        );
        proxy.$modal.msgError("打印失败,请稍后重试");
    } finally {
        proxy.$modal.closeLoading();
    }
};
// æ‰§è¡Œæ‰“印
const executePrint = () => {
    console.log('开始执行打印,数据条数:', printData.value.length);
    console.log('打印数据:', printData.value);
    // åˆ›å»ºä¸€ä¸ªæ–°çš„æ‰“印窗口
    const printWindow = window.open('', '_blank', 'width=800,height=600');
    // æž„建打印内容
    let printContent = `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>打印预览</title>
      <style>
        body {
          margin: 0;
          padding: 0;
          font-family: "SimSun", serif;
          background: white;
        }
                                                     .print-page {
            width: 200mm;
            height: 75mm;
            padding: 10mm;
            padding-left: 20mm;
            background: white;
            box-sizing: border-box;
            page-break-after: always;
            page-break-inside: avoid;
          }
         .print-page:last-child {
           page-break-after: avoid;
         }
        .delivery-note {
          width: 100%;
          height: 100%;
          font-size: 12px;
          line-height: 1.2;
          display: flex;
          flex-direction: column;
          color: #000;
        }
        .header {
          text-align: center;
          margin-bottom: 8px;
        }
        .company-name {
          font-size: 18px;
          font-weight: bold;
          margin-bottom: 4px;
        }
        .document-title {
          font-size: 16px;
          font-weight: bold;
        }
        .info-section {
          margin-bottom: 8px;
          display: flex;
          justify-content: space-between;
          align-items: center;
        }
        .info-row {
          line-height: 20px;
        }
        .label {
          font-weight: bold;
          width: 60px;
          font-size: 12px;
        }
        .value {
          margin-right: 20px;
          min-width: 80px;
          font-size: 12px;
        }
                 .table-section {
                 margin-bottom: 40px;
          //  flex: 0.6;
         }
        .product-table {
          width: 100%;
          border-collapse: collapse;
          border: 1px solid #000;
        }
                 .product-table th, .product-table td {
           border: 1px solid #000;
           padding: 6px;
           text-align: center;
           font-size: 12px;
           line-height: 1.4;
         }
        .product-table th {
          font-weight: bold;
        }
        .total-value {
          font-weight: bold;
        }
        .footer-section {
          margin-top: auto;
        }
        .footer-row {
          display: flex;
          margin-bottom: 3px;
          line-height: 22px;
          justify-content: space-between;
        }
        .footer-item {
          display: flex;
          margin-right: 20px;
        }
        .footer-item .label {
          font-weight: bold;
          width: 80px;
          font-size: 12px;
        }
        .footer-item .value {
          min-width: 80px;
          font-size: 12px;
        }
        .address-item .address-value {
          min-width: 200px;
        }
        @media print {
          body {
            margin: 0;
            padding: 0;
          }
                     .print-page {
             margin: 0;
             padding: 10mm;
             /* padding-left: 20mm; */
             page-break-inside: avoid;
             page-break-after: always;
           }
           .print-page:last-child {
             page-break-after: avoid;
           }
        }
      </style>
    </head>
    <body>
  `;
    // ä¸ºæ¯æ¡æ•°æ®ç”Ÿæˆæ‰“印页面
    printData.value.forEach((item, index) => {
        printContent += `
      <div class="print-page">
        <div class="delivery-note">
          <div class="header">
            <div class="document-title">零售发货单</div>
          </div>
          <div class="info-section">
            <div class="info-row">
              <div>
                <span class="label">发货日期:</span>
                <span class="value">${formatDate(item.createTime)}</span>
              </div>
              <div>
                <span class="label">客户名称:</span>
                <span class="value">${item.customerName}</span>
              </div>
            </div>
            <div class="info-row">
              <span class="label">单号:</span>
              <span class="value">${item.salesContractNo || ''}</span>
            </div>
          </div>
          <div class="table-section">
            <table class="product-table">
              <thead>
                <tr>
                  <th>产品名称</th>
                  <th>规格型号</th>
                  <th>单位</th>
                  <th>单价</th>
                  <th>零售数量</th>
                  <th>零售金额</th>
                </tr>
              </thead>
              <tbody>
                ${item.products && item.products.length > 0 ?
            item.products.map(product => `
                    <tr>
                      <td>${product.productCategory || ''}</td>
                      <td>${product.specificationModel || ''}</td>
                      <td>${product.unit || ''}</td>
                      <td>${product.taxInclusiveUnitPrice || '0'}</td>
                      <td>${product.quantity || '0'}</td>
                      <td>${product.taxInclusiveTotalPrice || '0'}</td>
                    </tr>
                  `).join('') :
            '<tr><td colspan="6" style="text-align: center; color: #999;">暂无产品数据</td></tr>'
const handlePrintLabel = async () => {
    if (selectedRows.value.length !== 1) {
        proxy.$modal.msgWarning("请选择一条销售台账数据进行标签打印");
        return;
    }
    const selectedId = selectedRows.value[0]?.id;
    if (!selectedId) {
        proxy.$modal.msgWarning("当前选择数据缺少ID,无法打印标签");
        return;
    }
    proxy.$modal.loading("正在获取标签数据,请稍候...");
    try {
        const res = await getSalesLabel(selectedId);
        const labelList = res?.data ?? [];
        if (!Array.isArray(labelList) || labelList.length === 0) {
            proxy.$modal.msgWarning("暂无可打印标签数据");
            return;
        }
              </tbody>
              <tfoot>
                <tr>
                  <td class="label">合计</td>
                  <td class="total-value"></td>
                  <td class="total-value"></td>
                  <td class="total-value"></td>
                  <td class="total-value">${getTotalQuantityForPrint(item.products)}</td>
                  <td class="total-value">${getTotalAmountForPrint(item.products)}</td>
                </tr>
              </tfoot>
            </table>
          </div>
          <div class="footer-section">
            <div class="footer-row">
              <div class="footer-item">
                <span class="label">收货电话:</span>
                <span class="value"></span>
              </div>
              <div class="footer-item">
                <span class="label">收货人:</span>
                <span class="value"></span>
              </div>
              <div class="footer-item address-item">
                <span class="label">收货地址:</span>
                <span class="value address-value"></span>
              </div>
            </div>
            <div class="footer-row">
              <div class="footer-item">
                <span class="label">操作员:</span>
                <span class="value">${userStore.nickName || '撕开前'}</span>
              </div>
              <div class="footer-item">
                <span class="label">打印日期:</span>
                <span class="value">${formatDateTime(new Date())}</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    `;
    });
    printContent += `
    </body>
    </html>
  `;
    // å†™å…¥å†…容到新窗口
    printWindow.document.write(printContent);
    printWindow.document.close();
    // ç­‰å¾…内容加载完成后打印
    printWindow.onload = () => {
        setTimeout(() => {
            printWindow.print();
            printWindow.close();
            printPreviewVisible.value = false;
        }, 500);
    };
};
// æ ¼å¼åŒ–日期
const formatDate = (dateString) => {
    if (!dateString) return getCurrentDate();
    const date = new Date(dateString);
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    return `${year}/${month}/${day}`;
};
// æ ¼å¼åŒ–日期时间
const formatDateTime = (date) => {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");
    const hours = String(date.getHours()).padStart(2, "0");
    const minutes = String(date.getMinutes()).padStart(2, "0");
    const seconds = String(date.getSeconds()).padStart(2, "0");
    return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
};
// è®¡ç®—产品总数量
const getTotalQuantity = (products) => {
    if (!products || products.length === 0) return '0';
    const total = products.reduce((sum, product) => {
        return sum + (parseFloat(product.quantity) || 0);
    }, 0);
    return total.toFixed(2);
};
// è®¡ç®—产品总金额
const getTotalAmount = (products) => {
    if (!products || products.length === 0) return '0';
    const total = products.reduce((sum, product) => {
        return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0);
    }, 0);
    return total.toFixed(2);
};
// ç”¨äºŽæ‰“印的计算函数
const getTotalQuantityForPrint = (products) => {
    if (!products || products.length === 0) return '0';
    const total = products.reduce((sum, product) => {
        return sum + (parseFloat(product.quantity) || 0);
    }, 0);
    return total.toFixed(2);
};
const getTotalAmountForPrint = (products) => {
    if (!products || products.length === 0) return '0';
    const total = products.reduce((sum, product) => {
        return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0);
    }, 0);
    return total.toFixed(2);
        printSalesLabel(labelList);
    } catch (error) {
        console.error("打印标签失败:", error);
        proxy.$modal.msgError("打印标签失败,请稍后重试");
    } finally {
        proxy.$modal.closeLoading();
    }
};
const mathNum = () => {
@@ -2926,6 +2463,72 @@
    return statusStr === '待发货' || statusStr === '审核拒绝';
};
const handleBulkDelivery = async () => {
    if (selectedRows.value.length === 0) {
        proxy.$modal.msgWarning("请选择数据");
        return;
    }
    const customerNames = selectedRows.value.map((r) => String(r.customerName || "").trim());
    const uniqueCustomers = Array.from(new Set(customerNames));
    // å®¢æˆ·åç§°ä¸ä¸€è‡´ä¸å…è®¸å‘è´§
    if (uniqueCustomers.length > 1) {
        proxy.$modal.msgWarning("客户名称不一致,不允许发货");
        return;
    }
    // å¤šæ¡ä¸”客户一致:二次确认
    if (selectedRows.value.length > 1) {
        try {
            await ElMessageBox.confirm("是否确认合并发货?", "合并发货", {
                confirmButtonText: "确认",
                cancelButtonText: "取消",
                type: "warning",
            });
        } catch (e) {
            proxy.$modal.msg("已取消");
            return;
        }
    }
    proxy.$modal.loading("正在获取产品数据,请稍候...");
    try {
        const targets = [];
        for (const ledger of selectedRows.value) {
            let products = [];
            try {
                const res = await productList({ salesLedgerId: ledger.id, type: 1 });
                products = res?.data || [];
            } catch {
                products = [];
            }
            for (const product of products) {
                if (!canShip(product)) continue;
                targets.push({
                    ...product,
                    salesLedgerId: product.salesLedgerId || ledger.id,
                });
            }
        }
        if (targets.length === 0) {
            proxy.$modal.msgWarning("没有可发货的数据");
            return;
        }
        currentDeliveryRows.value = targets;
        deliveryForm.value = { type: "货车" };
        // é‡ç½®å®¡æ‰¹äººèŠ‚ç‚¹ï¼ˆé»˜è®¤ä¸€ä¸ªç©ºèŠ‚ç‚¹ï¼‰
        approverNodes.value = [{ id: 1, userId: null }];
        nextApproverId = 2;
        deliveryFormVisible.value = true;
    } finally {
        proxy.$modal.closeLoading();
    }
};
/**
 * ä¸‹è½½æ–‡ä»¶
 *
@@ -2949,7 +2552,7 @@
        return;
    }
    
    currentDeliveryRow.value = row;
    currentDeliveryRows.value = [row];
  deliveryForm.value = {
    type: "货车",
  };
@@ -2972,13 +2575,28 @@
      const approveUserIds = approverNodes.value.map(node => node.userId).join(",");
      // ä¿å­˜å½“前展开的行ID,以便发货后重新加载子表格数据
      const currentExpandedKeys = [...expandedRowKeys.value];
      const salesLedgerId = currentDeliveryRow.value.salesLedgerId;
      addShippingInfo({
        salesLedgerId: salesLedgerId,
        salesLedgerProductId: currentDeliveryRow.value.id,
        type: deliveryForm.value.type,
                approveUserIds,
      })
      const targets = currentDeliveryRows.value || [];
      if (targets.length === 0) {
        proxy.$modal.msgWarning("未选择可发货的数据");
        return;
      }
      // ä¾æ¬¡å‘货(避免并发下库存扣减/状态更新互相影响)
      const run = async () => {
        for (const item of targets) {
          const salesLedgerId = item.salesLedgerId;
          if (!salesLedgerId) continue;
          await addShippingInfo({
            salesLedgerId,
            salesLedgerProductId: item.id,
            type: deliveryForm.value.type,
            approveUserIds,
          });
        }
      };
      run()
        .then(() => {
          proxy.$modal.msgSuccess("发货成功");
          closeDeliveryDia();
@@ -2986,8 +2604,7 @@
          getList().then(() => {
            // å¦‚果之前有展开的行,重新加载这些行的子表格数据
            if (currentExpandedKeys.length > 0) {
              // ä½¿ç”¨ Promise.all å¹¶è¡ŒåŠ è½½æ‰€æœ‰å±•å¼€è¡Œçš„å­è¡¨æ ¼æ•°æ®
              const loadPromises = currentExpandedKeys.map(ledgerId => {
              const loadPromises = currentExpandedKeys.map((ledgerId) => {
                return productList({ salesLedgerId: ledgerId, type: 1 }).then((res) => {
                  const index = tableData.value.findIndex((item) => item.id === ledgerId);
                  if (index > -1) {
@@ -2996,12 +2613,14 @@
                });
              });
              Promise.all(loadPromises).then(() => {
                // æ¢å¤å±•开状态
                expandedRowKeys.value = currentExpandedKeys;
              });
            }
          });
        })
        .catch(() => {
          proxy.$modal.msgError("发货失败,请稍后重试");
        });
    }
  });
};
@@ -3010,7 +2629,7 @@
const closeDeliveryDia = () => {
  proxy.resetForm("deliveryFormRef");
  deliveryFormVisible.value = false;
  currentDeliveryRow.value = null;
  currentDeliveryRows.value = [];
};
const currentFactoryName = ref("");
const getCurrentFactoryName = async () => {
@@ -3066,171 +2685,5 @@
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
}
.print-preview-dialog {
    .el-dialog__body {
        padding: 0;
        max-height: 80vh;
        overflow-y: auto;
    }
}
.print-preview-container {
    .print-preview-header {
        padding: 15px;
        border-bottom: 1px solid #e4e7ed;
        text-align: center;
        .el-button {
            margin: 0 10px;
        }
    }
    .print-preview-content {
        padding: 20px;
        background-color: #f5f5f5;
        min-height: 400px;
    }
}
.print-page {
    width: 220mm;
    height: 90mm;
    padding: 10mm;
    margin: 0 auto;
    background: white;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    margin-bottom: 10px;
    box-sizing: border-box;
}
.delivery-note {
    width: 100%;
    height: 100%;
    font-family: "SimSun", serif;
    font-size: 10px;
    line-height: 1.2;
    display: flex;
    flex-direction: column;
}
.header {
    text-align: center;
    margin-bottom: 8px;
    .company-name {
        font-size: 18px;
        font-weight: bold;
        margin-bottom: 4px;
    }
    .document-title {
        font-size: 16px;
        font-weight: bold;
    }
}
.info-section {
    margin-bottom: 8px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .info-row {
        line-height: 20px;
        .label {
            font-weight: bold;
            width: 60px;
            font-size: 14px;
        }
        .value {
            margin-right: 20px;
            min-width: 80px;
            font-size: 14px;
        }
    }
}
.table-section {
    margin-bottom: 4px;
    flex: 1;
    .product-table {
        width: 100%;
        border-collapse: collapse;
        border: 1px solid #000;
        th, td {
            border: 1px solid #000;
            padding: 6px;
            text-align: center;
            font-size: 14px;
            line-height: 1.4;
        }
        th {
            font-weight: bold;
        }
        .total-label {
            text-align: right;
            font-weight: bold;
        }
        .total-value {
            font-weight: bold;
        }
    }
}
.footer-section {
    .footer-row {
        display: flex;
        margin-bottom: 3px;
        line-height: 20px;
        justify-content: space-between;
        .footer-item {
            display: flex;
            margin-right: 20px;
            .label {
                font-weight: bold;
                width: 80px;
                font-size: 14px;
            }
            .value {
                min-width: 80px;
                font-size: 14px;
            }
            &.address-item {
                .address-value {
                    min-width: 200px;
                }
            }
        }
    }
}
@media print {
    .app-container {
        display: none;
    }
    .print-page {
        box-shadow: none;
        margin: 0;
        padding: 10mm;
        padding-left: 20mm;
        page-break-inside: avoid;
        page-break-after: always;
    }
    .print-page:last-child {
        page-break-after: avoid;
    }
}
</style>