Просмотр исходного кода

Merge remote-tracking branch 'origin/ecp' into ecp

纪新园 2 месяцев назад
Родитель
Сommit
d577a40782

+ 78 - 0
README.md

@@ -34,3 +34,81 @@ npm install --registry=https://registry.npmmirror.com
     1. [x] 发货状态查询
     2. [x] 发票及开票信息查询
 5. [x] 投诉管理
+
+## 会议纪要 TODO(自动生成)
+
+来源:doc/库比森交流会议纪要.xlsx(Sheet1)
+
+- 预测功能
+  - [x] 预测数量默认值设为 0(急)
+  - [ ] 预测物料明细分页展示(否)
+  - [x] 若预算月份已存在,禁用“新增预测”(急)
+  - [ ] 明细表头增加“品牌”下拉,支持多品牌(否)
+  - [ ] 明细列表支持复选框批量删除,未提交数据可删除(中)
+  - [ ] 导出预测:不再重复选择年月;列表首列增加复选框按选择导出;工厂导入包含两个 sheet:数据明细 与 所有经销商物料汇总(去掉经销商列)(急)
+
+- 订单管理
+  - [x] 编辑明细首次保存留在当前页,可继续编辑;修复保存后返回列表看不到的问题(急)
+  - [x] 详情页:已提交的物料明细不可编辑;未提交可编辑数量并删除行(急)
+  - [ ] 明确单价是否含税,需与客户确认(否)
+  - [ ] 库存不足可提交订单,仅提示不限制(中)
+  - [ ] 订单主表显示行数;查询页可见每单行数(中)
+  - [ ] 支持模糊搜索物料,弹窗多选添加(中)
+  - [x] 订单列表:订单编码可点击直达详情,移除“查看”按钮(急)
+  - [ ] 订单提交后状态应变更,排查为何仍为“草稿”(急) ——U9接口
+  - [x] 已提交订单禁用“提交”按钮,状态列展示“已提交”(急)
+  - [ ] 搜索新增“物料”条件,可查含某物料的订单(中)
+  - [ ] 工厂端新增“订单查询/审核”功能,用于批准发货(急) -- 待确认
+  - [ ] 明细编辑页:操作列冻结,首列增加复选框用于删除(未标注)
+
+- 整体的 UI/规范
+  - [ ] 前端规范:搜索/查询入口统一放右侧(中)
+  - [ ] 后端规范:多表操作需加事务(否)
+  - [ ] 单据号统一采用客户规则并提供维护机制,避免随机生成(否)
+
+## 会议纪要 TODO(重排视图)
+
+说明:以下仅对上方“会议纪要 TODO(自动生成)”选中片段进行重排与格式化;未更改原内容。每个分类内按优先级:急 > 中 > 否 排列,保留完成状态与备注。
+
+- 预测功能
+  - 急
+    - [x] 预测数量默认值设为 0(急)
+    - [x] 若预算月份已存在,禁用“新增预测”(急)
+    - [ ] 导出预测:不再重复选择年月;列表首列增加复选框按选择导出;工厂导入包含两个 sheet:数据明细 与 所有经销商物料汇总(去掉经销商列)(急) 
+        - 两个预测导出,哪个不需要重复选择?
+        - 需要导入接口
+  - 中
+    - [ ] 明细列表支持复选框批量删除,未提交数据可删除(中)
+      - 预测功能的提交、未提交状态是怎么判定的
+  - 否
+    - [ ] 预测物料明细分页展示(否)
+    - [ ] 明细表头增加“品牌”下拉,支持多品牌(否)
+        - 表头增加品牌下拉什么意思?
+
+- 订单管理
+  - 急
+    - [x] 编辑明细首次保存留在当前页,可继续编辑;修复保存后返回列表看不到的问题(急)
+    - [x] 详情页:已提交的物料明细不可编辑;未提交可编辑数量并删除行(急)
+    - [x] 订单列表:订单编码可点击直达详情,移除“查看”按钮(急)
+    - [ ] 订单提交后状态应变更,排查为何仍为“草稿”(急) ——U9接口
+        - 接口逻辑里需要变更状态
+    - [x] 已提交订单禁用“提交”按钮,状态列展示“已提交”(急)
+  - 中
+    - [x] 库存不足可提交订单,仅提示不限制(中)
+    - [x] 订单主表显示行数;查询页可见每单行数(中)
+    - [ ] 支持模糊搜索物料,弹窗多选添加(中) -- 什么ui结构?
+    - [ ] 搜索新增“物料”条件,可查含某物料的订单(中)
+      - 需要接口: 根据物料id搜索包含该物料的主订单
+    - [ ] 工厂端新增“订单查询/审核”功能,用于批准发货(急) -- 待确认 备注:原括号标注为“急”,为保持原文未调整优先级归类。
+  - 否
+    - [ ] 明确单价是否含税,需与客户确认(否)
+      - 确认是否需要
+    - [x] 明细编辑页:操作列冻结,首列增加复选框用于删除(未标注) 备注:此项未标注优先级,保留原状态。
+        - 完成,但是现在只有新增的时候才能编辑,已经提交的表单明细删除需要接口支持
+
+- 整体的 UI/规范
+  - 中
+    - [ ] 前端规范:搜索/查询入口统一放右侧(中)
+  - 否
+    - [ ] 后端规范:多表操作需加事务(否)
+    - [ ] 单据号统一采用客户规则并提供维护机制,避免随机生成(否)

+ 19 - 1
src/api/forecast/forecast-summary.js

@@ -21,6 +21,7 @@ import request from '@/router/axios'
  * @typedef {import('./types').SalesForecastMainDetailResponse} SalesForecastMainDetailResponse
  * @typedef {import('./types').SalesForecastMainUpdateRequest} SalesForecastMainUpdateRequest
  * @typedef {import('./types').SalesForecastMainUpdateResponse} SalesForecastMainUpdateResponse
+ * @typedef {import('./types').SalesForecastByMonthResponse} SalesForecastByMonthResponse
  */
 
 /**
@@ -203,7 +204,7 @@ export const updateSalesForecastMain = async (data) => {
     data
   })
 }
- 
+
 /**
  * 获取预测汇总详情
  * @param {string|number} forecastSummaryId - 预测汇总ID
@@ -482,3 +483,20 @@ export const approveSalesForecastSummaryParticulars = async (data) => {
     data: { id, forecastMainId, approvalStatus, remark: remark ?? approvalComment ?? '' }
   })
 }
+
+/**
+ * 按年月查询销售预测汇总明细
+ * 对应后端:GET /api/blade-factory/api/factory/salesForecastSummary/byMonth
+ * @param {number|string} year - 年份,例如 2025
+ * @param {number|string} month - 月份,1-12 或 '01'-'12'
+ * @returns {Promise<SalesForecastByMonthResponse>} 响应(data: ForecastSummaryRecord[])
+ * @example
+ * const res = await getSalesForecastSummaryByMonth(2025, 12)
+ */
+export const getSalesForecastSummaryByMonth = async (year, month) => {
+  return request({
+    url: '/api/blade-factory/api/factory/salesForecastSummary/byMonth',
+    method: 'get',
+    params: { year, month }
+  })
+}

+ 3 - 0
src/api/forecast/types.d.ts

@@ -155,6 +155,9 @@ export type ForecastSummaryPageResponse = Promise<AxiosResponse<ApiResponse<Page
 // 新增:销售预测汇总分页响应类型别名(复用 ForecastSummaryPageResponse)
 export type SalesForecastSummaryPageResponse = ForecastSummaryPageResponse
 
+// 新增:按年月查询销售预测汇总(byMonth)- 响应类型(data 为 ForecastSummaryRecord[])
+export type SalesForecastByMonthResponse = Promise<AxiosResponse<ApiResponse<ForecastSummaryRecord[]>>>
+
 // 品牌库存汇总查询参数
 export interface BrandStockQueryParams {
   storageName?: string

+ 62 - 11
src/components/forecast-form/forecast-form-mixin.js

@@ -59,7 +59,7 @@
 
 // API接口导入
 import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
-import { addSalesForecastMain, updateSalesForecastMain } from '@/api/forecast/forecast-summary'
+import { addSalesForecastMain, updateSalesForecastMain, getSalesForecastSummaryByMonth } from '@/api/forecast/forecast-summary'
 import { getUserLinkGoods } from '@/api/order/sales-order'
 
 // 常量和枚举导入
@@ -102,7 +102,9 @@ export const FORECAST_FORM_EVENTS = {
   /** 表单提交失败事件 */
   SUBMIT_ERROR: 'submit-error',
   /** 更新可见性事件 */
-  UPDATE_VISIBLE: 'update:visible'
+  UPDATE_VISIBLE: 'update:visible',
+  /** 保存禁用状态变更(用于父级按钮禁用控制) */
+  SAVE_DISABLED_CHANGE: 'save-disabled-change'
 }
 
 /**
@@ -327,7 +329,8 @@ export default {
             if (this.initialFormData) {
               this.formData = this.cleanAndFormatFormData(this.initialFormData)
             } else {
-              this.formData = this.createInitialFormData()
+              // 使用 initFormData,确保新增模式默认填入“下个月”而不是当前月
+              this.initFormData()
             }
 
             // 如果是编辑模式且有ID,则加载详情数据
@@ -344,6 +347,11 @@ export default {
             if (!this.isEdit) {
               this.loadCurrentCustomerInfo()
             }
+
+            // 新增模式下进行年月预测存在性检查(默认年月)
+            if (!this.isEdit) {
+              this.checkForecastByMonthAndEmit && this.checkForecastByMonthAndEmit()
+            }
           })
         }
       },
@@ -422,10 +430,30 @@ export default {
         if (!newVal) {
           this.initFormData()
         }
+        // 切换为编辑态时,通知父级恢复保存按钮可点击
+        if (newVal && this.$emit) {
+          this.$emit(FORECAST_FORM_EVENTS.SAVE_DISABLED_CHANGE, false)
+        }
       },
       immediate: true
     },
 
+    // 新增:监听年份与月份变更以触发按月校验(仅新增模式)
+    'formData.year': {
+      handler() {
+        if (this.visible && !this.isEdit) {
+          this.checkForecastByMonthAndEmit && this.checkForecastByMonthAndEmit()
+        }
+      }
+    },
+    'formData.month': {
+      handler() {
+        if (this.visible && !this.isEdit) {
+          this.checkForecastByMonthAndEmit && this.checkForecastByMonthAndEmit()
+        }
+      }
+    },
+
     /**
      * 监听预测ID变化
      * @param {string|number} val - 新的预测ID
@@ -1056,7 +1084,7 @@ export default {
 
         // 新增模式下需要至少一条有效明细;编辑模式下仅提交主表四个字段,不校验明细条数
         if (!this.isEdit && !items.length) {
-          this.$message && this.$message.warning('请至少填写一条有效的预测数量')
+        //   this.$message && this.$message.warning('请至少填写一条有效的预测数量')
           // 通知父组件失败,便于父侧重置保存按钮loading
           this.$emit && this.$emit(FORECAST_FORM_EVENTS.SUBMIT_ERROR, { message: '未填写有效的预测明细' })
           return
@@ -1154,7 +1182,7 @@ export default {
           value: item.id
         }))
         // 默认显示全部物料至下方表格,预测数量默认 0,用户可手动删除不需要的物料
-        this.stockTableData = stockList.map(item => ({ ...item, forecastQuantity: 1 }))
+        this.stockTableData = stockList.map(item => ({ ...item, forecastQuantity: 0 }))
       } catch (e) {
         console.error('加载用户关联商品失败:', e)
         this.$message.error(e.message || '加载用户关联商品失败')
@@ -1165,7 +1193,7 @@ export default {
 
     /**
      * 导入所选物料到下方表格
-     * @description 仅在点击"导入物料"按钮后,将选择的物料行添加到表格,默认预测数量为 1
+     * @description 仅在点击"导入物料"按钮后,将选择的物料行添加到表格,默认预测数量为 0
      * @returns {void}
      * @this {ForecastFormMixinComponent & Vue}
      */
@@ -1181,7 +1209,7 @@ export default {
         this.$message.error('未找到所选物料数据,请重新选择')
         return
       }
-      
+
       // 防止重复导入 - 使用多个字段进行更全面的重复检查
       const exists = this.stockTableData.some(row => {
         // 优先使用 id 进行匹配
@@ -1198,15 +1226,15 @@ export default {
         }
         return false
       })
-      
+
       if (exists) {
         this.$message.warning('该物料已在列表中')
         this.selectedStockId = null
         return
       }
-      
-      // 添加到表格,默认预测数量为 1
-      this.stockTableData.push({ ...stock, forecastQuantity: 1 })
+
+      // 添加到表格,默认预测数量为 0
+      this.stockTableData.push({ ...stock, forecastQuantity: 0 })
       // 清空已选
       this.selectedStockId = null
     },
@@ -1345,6 +1373,29 @@ export default {
       } catch (e) {
         console.warn('回显库存合并失败:', e)
       }
+    },
+
+    /**
+     * 新增模式:检查指定年月是否已有预测,并通过事件通知父组件控制保存按钮禁用
+     * @returns {Promise<void>}
+     */
+    async checkForecastByMonthAndEmit() {
+      try {
+        const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : Number(this.formData.year)
+        const month = Number(this.formData.month)
+        if (!year || !month) return
+        // 仅新增模式需要校验
+        if (this.isEdit) {
+          this.$emit && this.$emit(FORECAST_FORM_EVENTS.SAVE_DISABLED_CHANGE, false)
+          return
+        }
+        const res = await getSalesForecastSummaryByMonth(year, month)
+        const hasData = !!(res && res.data && res.data.success && Array.isArray(res.data.data) && res.data.data.length > 0)
+        this.$emit && this.$emit(FORECAST_FORM_EVENTS.SAVE_DISABLED_CHANGE, hasData)
+      } catch (e) {
+        // 异常时不阻塞新增,默认允许保存
+        this.$emit && this.$emit(FORECAST_FORM_EVENTS.SAVE_DISABLED_CHANGE, false)
+      }
     }
   }
 }

+ 8 - 2
src/components/forecast-form/index.js

@@ -319,10 +319,16 @@ export default {
      * @private
      */
     createInitialFormData() {
+      // 默认使用“下个月”的年月,与 mixin 中的逻辑保持一致
+      const now = new Date()
+      const currentYear = now.getFullYear()
+      const currentMonth = now.getMonth() + 1
+      const nextYear = currentMonth === 12 ? currentYear + 1 : currentYear
+      const nextMonth = currentMonth === 12 ? 1 : currentMonth + 1
       return {
         forecastCode: '',
-        year: new Date().getFullYear(),
-        month: new Date().getMonth() + 1,
+        year: nextYear,
+        month: nextMonth,
         customerId: 0,
         customerCode: '',
         customerName: '',

+ 4 - 2
src/components/order-form/events.js

@@ -68,7 +68,9 @@ export const MATERIAL_DETAIL_EVENTS = {
   /** 物料删除事件 */
   MATERIAL_DELETE: 'material-delete',
   /** 物料更新事件 */
-  MATERIAL_UPDATE: 'material-update'
+  MATERIAL_UPDATE: 'material-update',
+  /** 物料批量删除事件 */
+  MATERIAL_BATCH_DELETE: 'material-batch-delete'
 }
 
 /**
@@ -126,7 +128,7 @@ export const ALL_EVENTS = {
 
 /**
  * 物料明细事件类型
- * @typedef {'refresh'|'material-import'|'material-delete'|'material-update'} MaterialDetailEventType
+ * @typedef {'refresh'|'material-import'|'material-delete'|'material-update'|'material-batch-delete'} MaterialDetailEventType
  */
 
 /**

+ 80 - 1
src/components/order-form/material-detail-mixin.js

@@ -100,6 +100,15 @@ export default {
         typeof item.id === 'string' &&
         typeof item.itemCode === 'string'
       )
+    },
+
+    /**
+     * 是否为草稿状态 - 草稿状态下允许编辑所有物料明细
+     * @type {import('vue').PropOptions<boolean>}
+     */
+    isDraft: {
+      type: Boolean,
+      default: false
     }
   },
 
@@ -165,7 +174,13 @@ export default {
        * 正在编辑的属性名 - 用于记录当前编辑的字段
        * @type {string|null}
        */
-      editingProp: null
+      editingProp: null,
+
+      /**
+       * 选中的行数据 - 用于批量操作
+       * @type {MaterialDetailRecord[]}
+       */
+      selectedRows: []
     }
   },
 
@@ -335,6 +350,8 @@ export default {
      * @returns {boolean} 是否可编辑,true表示可编辑,false表示不可编辑
      */
     isRowEditable(row) {
+      // 草稿状态下允许编辑所有行
+      if (this.isDraft === true) return true
       // 如果没有数据来源信息,默认可编辑
       if (!row || !row.dataSource) {
         return true
@@ -987,6 +1004,68 @@ export default {
        })
 
        return index >= 0 ? index : -1
+     },
+
+     /**
+      * 处理表格选择变化事件
+      * @description 当用户选择或取消选择表格行时触发
+      * @param {MaterialDetailRecord[]} selection - 当前选中的行数据数组
+      * @this {MaterialDetailTableComponent}
+      */
+     handleSelectionChange(selection) {
+       this.selectedRows = selection || []
+     },
+
+     /**
+      * 处理批量删除操作
+      * @description 删除当前选中的所有行,并触发批量删除事件
+      * @this {MaterialDetailTableComponent}
+      */
+     handleBatchDelete() {
+       if (!this.selectedRows || this.selectedRows.length === 0) {
+         this.$message.warning('请先选择要删除的物料')
+         return
+       }
+
+       // 检查选中的行是否都可以删除
+       const undeletableRows = this.selectedRows.filter(row => !this.isRowEditable(row))
+       if (undeletableRows.length > 0) {
+         this.$message.warning('选中的物料中包含不可删除的项目')
+         return
+       }
+
+       const count = this.selectedRows.length
+       this.$confirm(`确定要删除选中的 ${count} 个物料吗?`, '批量删除确认', {
+         confirmButtonText: '确定',
+         cancelButtonText: '取消',
+         type: 'warning'
+       }).then(() => {
+         // 触发批量删除事件
+         this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_BATCH_DELETE, {
+           rows: [...this.selectedRows],
+           count: count
+         })
+         
+         // 清空选择
+         this.resetSelection()
+         
+         this.$message.success(`成功删除 ${count} 个物料`)
+       }).catch(() => {
+         // 用户取消删除
+       })
+     },
+
+     /**
+      * 重置选择状态
+      * @description 清空当前选中的行
+      * @this {MaterialDetailTableComponent}
+      */
+     resetSelection() {
+       this.selectedRows = []
+       // 如果表格组件有 clearSelection 方法,也调用它
+       if (this.$refs.crud && typeof this.$refs.crud.clearSelection === 'function') {
+         this.$refs.crud.clearSelection()
+       }
      }
   },
 

+ 6 - 0
src/components/order-form/material-detail-option.js

@@ -55,6 +55,12 @@ export function getMaterialDetailOption(isEditMode = false) {
     page: DEFAULT_PAGINATION_CONFIG,
     menuWidth: 120,
     rowKey: 'id',
+    // 启用多选功能
+    selection: isEditMode,
+    selectable: isEditMode ? (row, index) => {
+      // 只有可删除的物料才能被选中
+      return row.isDeletable === true
+    } : undefined,
     column: [
       {
         label: '物料编码',

+ 10 - 0
src/components/order-form/material-detail-table.vue

@@ -48,6 +48,15 @@
           >
             增加
           </el-button>
+          <el-button
+            v-if="editMode && selectedRows.length > 0"
+            type="danger"
+            icon="el-icon-delete"
+            size="small"
+            @click="handleBatchDelete"
+          >
+            批量删除 ({{ selectedRows.length }})
+          </el-button>
         </div>
       </div>
     </div>
@@ -62,6 +71,7 @@
         @refresh-change="handleRefresh"
         @current-change="handleCurrentChange"
         @size-change="handleSizeChange"
+        @selection-change="handleSelectionChange"
         @row-del="handleRowDelete"
         @row-update="handleRowUpdate"
       >

+ 204 - 22
src/components/order-form/order-form-mixin.js

@@ -6,7 +6,7 @@
 // API接口导入
 import { add, update as updateOrderHeader, getDetail } from '@/api/order/order'
 import { getList as getOrderItemList } from '@/api/order/order-item'
-import { createSalesOrder, updateOrder, addOrderItem } from '@/api/order/sales-order'
+import { createSalesOrder, updateOrder, addOrderItem, updateOrderItem } from '@/api/order/sales-order'
 import { getCustomerInfo } from '@/api/common/index'
 import { getList as getAddressList } from '@/api/order/address'
 import { submitOrderToU9 } from '@/api/order/sales-order'
@@ -158,6 +158,13 @@ export default {
       materialDetails: [],
 
       /**
+       * 远程明细原始快照映射
+       * @description 记录从服务器加载的远程物料明细的原始值,用于草稿状态下的变更对比
+       * @type {Record<string, {orderQuantity:number, confirmQuantity:number, unitPrice:number, taxRate:number}>}
+       */
+      originalRemoteDetailsById: {},
+
+      /**
        * 订单类型选项列表
        * @description 订单类型下拉选择器的选项数据
        * @type {typeof ORDER_TYPE_OPTIONS}
@@ -332,10 +339,10 @@ export default {
                 callback()
               }
             },
-            trigger: 'blur'
-          }
-        ],
-        receiverRegion: [
+             trigger: 'blur'
+           }
+         ],
+         receiverRegion: [
           {
             required: true,
             message: '请输入收货地区',
@@ -440,6 +447,16 @@ export default {
           }
         ]
       }
+    },
+
+    /**
+     * 是否为草稿状态
+     * @description 根据订单状态判断是否为草稿
+     * @returns {boolean}
+     */
+    isDraft() {
+      const status = Number(this.formData && this.formData.status)
+      return status === ORDER_STATUS.DRAFT
     }
   },
 
@@ -659,8 +676,8 @@ export default {
       try {
         // 并行加载订单详情和物料明细数据以提高性能
         const [orderResponse, materialResponse] = await Promise.all([
-          getDetail(orderId),
-          this.loadMaterialDetails(orderId)
+          getDetail(String(orderId)),
+          this.loadMaterialDetails(String(orderId))
         ])
 
         // 验证订单详情响应数据
@@ -681,6 +698,9 @@ export default {
         // 设置物料明细数据(确保是数组类型)
         this.materialDetails = Array.isArray(materialResponse) ? materialResponse : []
 
+        // 新增:建立远程明细原始快照,用于后续对比并持久化更新
+        this.originalRemoteDetailsById = this.buildOriginalRemoteSnapshot(this.materialDetails)
+
         console.log(`成功加载订单详情,订单编码: ${orderData.orderCode || orderId}`)
 
       } catch (error) {
@@ -803,6 +823,52 @@ export default {
       }
     },
 
+    // 构建远程明细原始快照
+    buildOriginalRemoteSnapshot(materials) {
+      const map = {}
+      try {
+        (materials || [])
+          .filter(m => m && m.dataSource === MaterialDetailDataSource.REMOTE)
+          .forEach(m => {
+            const key = String(m.id || m.itemId || m.itemCode || '')
+            if (!key) return
+            map[key] = {
+              orderQuantity: Math.round(Number(m.orderQuantity) || 0),
+              confirmQuantity: Math.round(Number(m.confirmQuantity) || 0),
+              unitPrice: preciseRound(Number(m.unitPrice) || 0, 2),
+              taxRate: preciseRound(Number(m.taxRate) || 0, 4)
+            }
+          })
+      } catch (e) {
+        // eslint-disable-next-line no-console
+        console.warn('构建原始快照失败:', e)
+      }
+      return map
+    },
+
+    // 判断远程明细是否发生变化(与原始快照对比)
+    hasRemoteMaterialChanged(currentRow) {
+      if (!currentRow) return false
+      const key = String(currentRow.id || currentRow.itemId || currentRow.itemCode || '')
+      if (!key) return false
+      const original = this.originalRemoteDetailsById && this.originalRemoteDetailsById[key]
+      if (!original) return false
+
+      const curr = {
+        orderQuantity: Math.round(Number(currentRow.orderQuantity) || 0),
+        confirmQuantity: Math.round(Number(currentRow.confirmQuantity) || 0),
+        unitPrice: preciseRound(Number(currentRow.unitPrice) || 0, 2),
+        taxRate: preciseRound(Number(currentRow.taxRate) || 0, 4)
+      }
+
+      return (
+        curr.orderQuantity !== original.orderQuantity ||
+        curr.confirmQuantity !== original.confirmQuantity ||
+        curr.unitPrice !== original.unitPrice ||
+        curr.taxRate !== original.taxRate
+      )
+    },
+
     /**
      * 映射订单数据到表单格式
      * @description 将API返回的订单数据安全地映射为表单数据格式,并格式化数字字段
@@ -820,10 +886,10 @@ export default {
       return {
         id: orderData.id ? String(orderData.id) : undefined,
         orderCode: String(orderData.orderCode || ''),
-        orgId: orderData.orgId ? Number(orderData.orgId) : undefined,
+        orgId: orderData.orgId ? String(orderData.orgId) : undefined,
         orgCode: String(orderData.orgCode || ''),
         orgName: String(orderData.orgName || ''),
-        customerId: Number(orderData.customerId) || null,
+        customerId: orderData.customerId ? String(orderData.customerId) : null,
         customerCode: String(orderData.customerCode || ''),
         customerName: String(orderData.customerName || ''),
         orderType: Number(orderData.orderType) || ORDER_TYPES.NORMAL,
@@ -886,12 +952,23 @@ export default {
             .slice(0, 5)
             .map(it => `${it.itemName || it.itemCode || '物料'}:订单数量 ${Number(it.orderQuantity || 0)} > 可用数量 ${Number(it.availableQuantity || 0)}`)
             .join('\n')
-          await this.$alert(
-            `库存不足,以下物料订单数量超过可用数量:\n${detailText}${exceededItems.length > 5 ? '\n...' : ''}`,
-            '库存不足',
-            { customClass: 'order-stock-alert' }
-          )
-          return
+          try {
+            await this.$confirm(
+              `库存不足,以下物料订单数量超过可用数量:\n${detailText}${exceededItems.length > 5 ? '\n...' : ''}\n\n是否继续提交?`,
+              '库存不足',
+              {
+                confirmButtonText: '继续提交',
+                cancelButtonText: '取消',
+                type: 'warning',
+                customClass: 'order-stock-alert',
+                distinguishCancelAndClose: true
+              }
+            )
+            // 用户确认,继续后续提交流程
+          } catch (e) {
+            // 用户取消或关闭,终止提交
+            return
+          }
         }
 
         this.saveLoading = true
@@ -914,8 +991,13 @@ export default {
          */
         this.$emit(ORDER_FORM_EVENTS.SAVE_SUCCESS, response.data.data)
 
-        // 返回列表
-        this.handleBack()
+        // 保持在当前页:编辑模式不返回列表,仅在新增模式返回列表
+        if (!this.isEdit) {
+          // 新增模式:保存成功后返回列表
+          this.handleBack()
+        } else {
+          // 编辑模式:留在当前页,方便继续编辑
+        }
 
       } catch (error) {
         const errorMessage = this.isEdit ? '订单更新失败,请重试' : '订单创建失败,请重试'
@@ -996,7 +1078,7 @@ export default {
      */
     async submitOrderData(submitData) {
       if (this.isEdit) {
-        // 编辑状态下:先仅更新订单基础信息(不包含物料明细),再单独添加“导入”的物料
+        // 编辑状态下:先仅更新订单基础信息(不包含物料明细),再单独处理导入新增与远程变更
         // 第一步:更新订单头
         const headerData = this.prepareSubmitData()
         const headerResponse = await updateOrderHeader(headerData)
@@ -1013,7 +1095,7 @@ export default {
           const orderCode = (this.formData && this.formData.orderCode) || ''
 
           const payloads = importedMaterials.map(material => ({
-            orderId: String(orderId), // 以字符串传输,避免大整数精度问题
+            orderId: String(orderId),
             orderCode,
             itemId: String(material.itemId || ''),
             itemCode: material.itemCode || '',
@@ -1040,7 +1122,48 @@ export default {
           }
         }
 
-        // 两步均成功,返回头部更新的响应
+        // 第三步:草稿状态下,找出远程明细的变更并调用 updateOrderItem 持久化
+        if (this.isDraft) {
+          const orderId = (this.formData && this.formData.id) || this.orderId
+          const remoteChanged = (this.materialDetails || [])
+            .filter(m => m.dataSource === MaterialDetailDataSource.REMOTE)
+            .filter(m => this.hasRemoteMaterialChanged(m))
+
+          if (remoteChanged.length > 0) {
+            const updatePayloads = remoteChanged.map(material => ({
+              id: String(material.id || ''),
+              orderId: String(orderId || ''),
+              orderCode: (this.formData && this.formData.orderCode) || '',
+              itemId: String(material.itemId || ''),
+              itemCode: material.itemCode || '',
+              itemName: material.itemName || '',
+              specs: material.specs || '',
+              mainItemCategoryId: String(material.mainItemCategoryId || ''),
+              mainItemCategoryName: material.mainItemCategoryName || '',
+              warehouseId: String(material.warehouseId || ''),
+              warehouseName: material.warehouseName || '',
+              availableQuantity: Number(material.availableQuantity) || 0,
+              orderQuantity: Number(material.orderQuantity) || 0,
+              confirmQuantity: Number(material.confirmQuantity) || Number(material.orderQuantity) || 0,
+              unitPrice: Number(material.unitPrice) || 0,
+              taxRate: Number(material.taxRate) || 0,
+              taxAmount: Number(material.taxAmount) || 0,
+              totalAmount: Number(material.totalAmount) || 0,
+              itemStatus: material.itemStatus || ORDER_ITEM_STATUS.UNCONFIRMED
+            }))
+
+            const updateResults = await Promise.all(updatePayloads.map(p => updateOrderItem(p)))
+            const updatesOk = updateResults.every(r => r && r.data && r.data.success)
+            if (!updatesOk) {
+              throw new Error('部分物料更新失败')
+            }
+
+            // 更新成功后,刷新原始快照,避免后续重复提交
+            this.originalRemoteDetailsById = this.buildOriginalRemoteSnapshot(this.materialDetails)
+          }
+        }
+
+        // 返回头部更新的响应
         return headerResponse
       } else {
         // 新建状态下使用createSalesOrder接口,包含物料明细数据
@@ -1305,6 +1428,65 @@ export default {
      },
 
     /**
+     * 处理物料批量删除事件
+     * @description 批量删除选中的物料记录,仅允许删除可删除的物料
+     * @param {import('./types').MaterialBatchDeleteEventData} data - 批量删除事件数据
+     * @returns {void}
+     * @public
+     * @this {import('./types').OrderFormMixinComponent}
+     */
+    handleMaterialBatchDelete({ rows, count }) {
+      if (!Array.isArray(rows) || rows.length === 0) {
+        this.$message.warning('请选择要删除的物料')
+        return
+      }
+
+      // 检查是否有不可删除的物料
+      const undeletableItems = rows.filter(row => !row.isDeletable)
+      if (undeletableItems.length > 0) {
+        const itemNames = undeletableItems.map(item => item.itemName).join('、')
+        this.$message.warning(`以下物料不允许删除:${itemNames}`)
+        return
+      }
+
+      // 确认删除操作
+      this.$confirm(`确定要删除选中的 ${count} 条物料记录吗?`, '批量删除确认', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        try {
+          // 批量删除物料
+          const deletedItems = []
+          rows.forEach(row => {
+            const materialIndex = this.materialDetails.findIndex(item =>
+              item.itemCode === row.itemCode &&
+              item.dataSource === row.dataSource
+            )
+            
+            if (materialIndex !== -1) {
+              const deletedItem = this.materialDetails.splice(materialIndex, 1)[0]
+              deletedItems.push(deletedItem.itemName)
+            }
+          })
+
+          if (deletedItems.length > 0) {
+            this.$message.success(`成功删除 ${deletedItems.length} 条物料记录`)
+            // 触发选择重置,清空已删除的选中项
+            this.$refs.materialDetailTable?.resetSelection()
+          } else {
+            this.$message.warning('未找到要删除的物料记录')
+          }
+        } catch (error) {
+          this.$message.error('批量删除物料失败,请重试')
+          console.error('批量删除物料失败:', error)
+        }
+      }).catch(() => {
+        // 用户取消删除操作
+      })
+    },
+
+    /**
      * 处理物料导入事件
      * @description 将导入的物料数据添加到物料明细列表中,格式化数字字段并标记为可删除
      * @param {MaterialDetailRecord[]} importedMaterials - 导入的物料数据数组
@@ -1478,7 +1660,7 @@ export default {
     handleCustomerSelected(/** @type {import('./types').CustomerSelectData} */ customerData) {
       if (this.formData) {
         // 更新客户相关字段
-        this.$set(this.formData, 'customerId', customerData.customerId)
+        this.$set(this.formData, 'customerId', customerData.customerId != null ? String(customerData.customerId) : null)
         this.$set(this.formData, 'customerCode', customerData.customerCode)
         this.$set(this.formData, 'customerName', customerData.customerName)
 
@@ -1501,7 +1683,7 @@ export default {
     handleAddressSelected(/** @type {import('./types').AddressSelectData} */ addressData) {
       if (this.formData) {
         // 更新地址相关字段
-        this.$set(this.formData, 'addressId', addressData.addressId)
+        this.$set(this.formData, 'addressId', addressData.addressId != null ? String(addressData.addressId) : '')
         this.$set(this.formData, 'receiverName', addressData.receiverName || '')
         this.$set(this.formData, 'receiverPhone', addressData.receiverPhone || '')
         this.$set(this.formData, 'receiverRegion', addressData.regionName || '')

+ 3 - 1
src/components/order-form/order-form.vue

@@ -58,9 +58,11 @@
           :order-id="orderId"
           :edit-mode="true"
           :material-details="materialDetails"
+          :is-draft="isDraft"
           @refresh="handleMaterialChange"
           @material-import="handleMaterialImport"
           @material-delete="handleMaterialDelete"
+          @material-batch-delete="handleMaterialBatchDelete"
           @material-update="handleMaterialUpdate"
         />
       </div>
@@ -71,7 +73,7 @@
           type="primary"
           icon="el-icon-upload2"
           size="small"
-          v-if="isEdit && canSubmitToU9(formData) && (orderId || formData.id)"
+          v-if="isEdit && isDraft && (orderId || formData.id)"
           @click="handleSubmitToU9"
         >
           提交

+ 17 - 0
src/components/order-form/types.d.ts

@@ -243,6 +243,8 @@ export interface OrderFormMixinMethods {
   prepareMaterialItemData(material: MaterialDetailRecord): any;
   /** 处理物料删除 */
   handleMaterialDelete(data: MaterialDeleteEventData): void;
+  /** 处理物料批量删除 */
+  handleMaterialBatchDelete(data: MaterialBatchDeleteEventData): void;
   /** 处理物料导入 */
   handleMaterialImport(importedMaterials: MaterialDetailRecord[]): void;
   /** 处理表单提交事件 */
@@ -648,6 +650,7 @@ export interface MaterialDetailTableComponent extends Vue {
   DIALOG_EVENTS: typeof import('./events').DIALOG_EVENTS;
   editingRow: MaterialDetailRecord | null;
   editingProp: string | null;
+  selectedRows: MaterialDetailRecord[];
 
   // computed
   tableOption: AvueCrudOption;
@@ -672,6 +675,9 @@ export interface MaterialDetailTableComponent extends Vue {
   handleSizeChange(size: number): void;
   handleRowDelete(row: MaterialDetailRecord, index: number): void;
   handleDeleteMaterial(row: MaterialDetailRecord, index: number): void;
+  handleSelectionChange(selection: MaterialDetailRecord[]): void;
+  handleBatchDelete(): void;
+  resetSelection(): void;
   handleRowUpdate(row: MaterialDetailRecord, index: number, done: (result: any) => void): void;
   handleQuantityBlur(row: MaterialDetailRecord, index: number): void;
   handleQuantityChange(row: MaterialDetailRecord, index: number): void;
@@ -719,6 +725,17 @@ export interface MaterialDeleteEventData {
   index: number;
 }
 
+/**
+ * 物料明细批量删除事件数据
+ * @description 当物料明细被批量删除时传递的事件数据
+ */
+export interface MaterialBatchDeleteEventData {
+  /** 被删除的行数据数组 */
+  rows: MaterialDetailRecord[];
+  /** 选中的行数量 */
+  count: number;
+}
+
 
 
 /**

+ 42 - 0
src/constants/lead.js

@@ -108,6 +108,48 @@ export const LEAD_STATUS_OPTIONS = [
 ]
 
 /**
+ * 线索来源枚举
+ * @readonly
+ * @enum {string}
+ */
+export const LEAD_SOURCE = {
+  /** 线上广告 */
+  ONLINE_ADS: '线上广告',
+  /** 线下活动 */
+  OFFLINE_EVENTS: '线下活动',
+  /** 客户推荐 */
+  CUSTOMER_REFERRAL: '客户推荐'
+}
+
+/**
+ * 线索来源选项数据
+ * @readonly
+ * @type {Array<{label: string, value: string}>}
+ */
+export const LEAD_SOURCE_OPTIONS = [
+  { label: LEAD_SOURCE.ONLINE_ADS, value: LEAD_SOURCE.ONLINE_ADS },
+  { label: LEAD_SOURCE.OFFLINE_EVENTS, value: LEAD_SOURCE.OFFLINE_EVENTS },
+  { label: LEAD_SOURCE.CUSTOMER_REFERRAL, value: LEAD_SOURCE.CUSTOMER_REFERRAL }
+]
+
+/**
+ * 验证线索来源值是否有效
+ * @param {string} source - 来源值
+ * @returns {boolean} 是否有效
+ */
+export function isValidLeadSource(source) {
+  return Object.values(LEAD_SOURCE).includes(source)
+}
+
+/**
+ * 获取所有线索来源值
+ * @returns {Array<string>} 所有来源值
+ */
+export function getAllLeadSourceValues() {
+  return Object.values(LEAD_SOURCE)
+}
+
+/**
  * 获取线索优先级标签
  * @param {number} priority - 优先级值
  * @returns {string} 优先级标签

+ 67 - 11
src/views/announcement/index.vue

@@ -1,7 +1,29 @@
 <template>
     <basic-container>
+        <!-- 公告预览(全屏容器) -->
+        <div v-if="announcementPreviewVisible" class="announcement-preview-container basic-container">
+            <div class="preview-header">
+                <div class="header-top">
+                    <el-button
+                        type="text"
+                        icon="el-icon-arrow-left"
+                        size="small"
+                        class="back-btn"
+                        @click="handlePreviewBack"
+                    >
+                        返回列表
+                    </el-button>
+                </div>
+                <div class="preview-title">{{ currentDetail.title || '公告预览' }}</div>
+            </div>
+            <div class="preview-content">
+                <div class="detail-body" v-html="currentDetail.content"></div>
+            </div>
+        </div>
+
         <!-- 公告表单组件 -->
         <announcement-form
+            v-else-if="announcementFormVisible"
             :visible.sync="announcementFormVisible"
             :is-edit="isEditMode"
             :announcement-id="editAnnouncementId"
@@ -10,7 +32,7 @@
 
         <!-- 公告列表 -->
         <avue-crud
-            v-if="!announcementFormVisible"
+            v-else
             :option="option"
             :data="data"
             ref="crud"
@@ -93,16 +115,6 @@
             </template>
         </avue-crud>
 
-        <!-- 详情对话框 -->
-        <el-dialog title="公告详情" :visible.sync="detailVisible" append-to-body width="60%" :close-on-click-modal="false">
-            <div class="detail-content" v-if="currentDetail.id">
-                <!-- 仅展示富文本内容 -->
-                <div class="detail-body" v-html="currentDetail.content"></div>
-            </div>
-            <span slot="footer" class="dialog-footer">
-                <el-button @click="detailVisible = false">关 闭</el-button>
-            </span>
-        </el-dialog>
     </basic-container>
 </template>
 
@@ -135,4 +147,48 @@ export default {
 
 <style lang="scss" scoped>
 @import './index.scss';
+
+.announcement-preview-container {
+  padding: 0;
+}
+
+.preview-header {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start; /* 显式左对齐头部子项,避免外部样式影响 */
+  padding: 12px 16px;
+  background-color: #ffffff;
+  border-radius: 8px 8px 0 0;
+  margin-bottom: 8px;
+}
+
+.header-top {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.back-btn {
+  padding-left: 0;
+}
+
+.preview-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  text-align: center;
+  margin-top: 4px;
+  align-self: center; /* 让标题在交叉轴居中 */
+  width: 100%;        /* 占满一行,文本居中更明显 */
+}
+
+.preview-content {
+  background-color: #ffffff;
+  padding: 16px;
+  border-radius: 0 0 8px 8px;
+}
+
+.detail-body {
+  min-height: 300px;
+}
 </style>

+ 22 - 2
src/views/announcement/mixins/announcementIndex.js

@@ -139,6 +139,8 @@ export default {
             loading: true,
             /** @type {boolean} 详情对话框显示状态 */
             detailVisible: false,
+            /** 用于全屏预览的可见性控制 */
+            announcementPreviewVisible: false,
             /** @type {Partial<NoticeRecord>} 当前查看的详情数据 */
             currentDetail: {},
             /** @type {PageInfo} 分页信息 */
@@ -394,6 +396,14 @@ export default {
                 // 关闭表单后,avue-crud 会通过 v-if 重新渲染,需等 DOM 恢复后再刷新字典
                 this.$nextTick(() => this.loadCategoryOptions());
             }
+        },
+        announcementPreviewVisible(val) {
+            if (val === true) {
+                // 打开预览时不需要刷新列表
+            } else {
+                // 关闭预览后,avue-crud 会通过 v-if 重新渲染,需等 DOM 恢复后再刷新字典
+                this.$nextTick(() => this.loadCategoryOptions());
+            }
         }
     },
 
@@ -475,7 +485,7 @@ export default {
         },
 
         /**
-         * 查看详情(支持行内操作)
+         * 查看详情(支持行内操作)- 改为进入全屏预览模式
          * @async
          * @this {import('@/views/announcement/types').AnnouncementComponent & import('vue').default}
          * @param {Partial<NoticeRecord>} [row] - 可选,当前行数据
@@ -492,13 +502,23 @@ export default {
             try {
                 const response = await getAnnouncement(id);
                 this.currentDetail = response.data?.data || {};
-                this.detailVisible = true;
+                // 打开全屏预览
+                this.announcementPreviewVisible = true;
             } catch (error) {
                 console.error('获取详情失败:', error);
                 this.$message.error('获取详情失败,请稍后重试');
             }
         },
 
+        /**
+         * 关闭全屏预览并返回列表
+         */
+        handlePreviewBack() {
+            this.announcementPreviewVisible = false;
+            // 可按需清理当前详情
+            // this.currentDetail = {};
+        },
+
         // 新增:单行删除(操作列按钮)
         /**
          * 行删除(操作列按钮)

+ 35 - 1
src/views/forecast/forecastIndex.js

@@ -54,6 +54,8 @@ export default {
       submitting: false,
       /** @type {boolean} 保存按钮加载状态 */
       saveLoading: false,
+      /** @type {boolean} 保存按钮禁用状态(按年月重复校验) */
+      saveDisabled: false,
       /** @type {boolean} 添加/编辑弹窗显示状态(保留兼容性) */
       dialogVisible: false,
       /** @type {boolean} 是否为编辑模式(保留兼容性) */
@@ -660,6 +662,9 @@ export default {
       // 生成预测编码
       this.generateForecastCode()
 
+      // 进入新增:先禁用保存,待子组件按默认年月校验完成后再由事件恢复
+      this.saveDisabled = true
+
       // 保持兼容性
       this.isEdit = false
       this.dialogVisible = true
@@ -678,8 +683,19 @@ export default {
         return
       }
 
-      this.isEdit = true
+      this.editMode = 'edit'
+      this.editTitle = '编辑预测申报'
       this.currentForecastId = row.id
+      this.editVisible = true
+
+      // 设置表单数据
+      this.form = { ...row }
+
+      // 编辑模式不需要按月校验禁用
+      this.saveDisabled = false
+
+      // 保持兼容性
+      this.isEdit = true
       this.dialogVisible = true
     },
 
@@ -705,6 +721,9 @@ export default {
       // 设置表单数据
       this.form = { ...row }
 
+      // 编辑模式不需要按月校验禁用
+      this.saveDisabled = false
+
       // 保持兼容性
       this.isEdit = true
       this.dialogVisible = true
@@ -723,6 +742,9 @@ export default {
       this.currentForecastId = null
       this.form = { ...DEFAULT_FORECAST_FORM }
 
+      // 重置保存禁用状态
+      this.saveDisabled = false
+
       // 保持兼容性
       this.dialogVisible = false
       this.isEdit = false
@@ -734,6 +756,10 @@ export default {
      * @returns {void}
      */
     handleSave() {
+      if (this.saveDisabled) {
+        this.$message && this.$message.warning('所选年月已存在预测,无法重复保存')
+        return
+      }
       if (this.$refs.forecastForm) {
         this.saveLoading = true
         this.$refs.forecastForm.handleSubmit()
@@ -741,6 +767,14 @@ export default {
     },
 
     /**
+     * 子组件发出的保存禁用状态变更
+     * @param {boolean} disabled
+     */
+    onSaveDisabledChange(disabled) {
+      this.saveDisabled = !!disabled
+    },
+
+    /**
      * 处理表单提交成功
      * @this {Vue & {saveLoading: boolean, editMode: string, $message: any, handleBackToList: () => void, refreshChange: () => void}}
      * @param {{msg?: string}} data - 提交成功的数据

+ 2 - 0
src/views/forecast/index.vue

@@ -156,6 +156,7 @@
           icon="el-icon-check"
           size="small"
           :loading="saveLoading"
+          :disabled="saveDisabled"
           @click="handleSave"
         >
           保存
@@ -171,6 +172,7 @@
         @close="handleFormClose"
         @submit="handleFormSubmit"
         @submit-error="handleFormError"
+        @save-disabled-change="onSaveDisabledChange"
       />
     </div>
   </basic-container>

+ 71 - 27
src/views/lead/index.vue

@@ -1,7 +1,7 @@
 <template>
     <basic-container>
         <avue-crud :option="option" :data="data" ref="crud" v-model="form" :page.sync="page"
-            :permission="permissionList" :before-open="beforeOpen" :table-loading="loading" 
+            :permission="permissionList" :before-open="beforeOpen" :table-loading="loading"
             @row-update="rowUpdate" @row-save="rowSave" @search-change="searchChange" @search-reset="searchReset"
             @selection-change="selectionChange" @current-change="currentChange" @size-change="sizeChange"
             @refresh-change="refreshChange" @on-load="onLoad">
@@ -56,7 +56,8 @@
             width="80%"
             :close-on-click-modal="false"
             :destroy-on-close="true"
-            append-to-body>
+            append-to-body
+            custom-class="lead-detail-dialog">
             <div v-if="currentLead">
                 <div class="lead-info-flex" style="margin-bottom: 20px;">
                     <div class="info-row">
@@ -81,26 +82,26 @@
                     </div>
                 </div>
             </div>
-            
-            <avue-crud 
-                :option="detailOption" 
-                :data="detailData" 
-                ref="detailCrud" 
-                v-model="detailForm" 
+
+            <avue-crud
+                :option="detailOption"
+                :data="detailData"
+                ref="detailCrud"
+                v-model="detailForm"
                 :page.sync="detailPage"
-                :permission="detailPermissionList" 
-                :before-open="detailBeforeOpen" 
-                :table-loading="detailLoading" 
+                :permission="detailPermissionList"
+                :before-open="detailBeforeOpen"
+                :table-loading="detailLoading"
                 @row-del="detailRowDel"
-                @row-update="detailRowUpdate" 
-                @row-save="detailRowSave" 
-                @selection-change="detailSelectionChange" 
-                @current-change="detailCurrentChange" 
+                @row-update="detailRowUpdate"
+                @row-save="detailRowSave"
+                @selection-change="detailSelectionChange"
+                @current-change="detailCurrentChange"
                 @size-change="detailSizeChange"
-                @refresh-change="detailRefreshChange" 
+                @refresh-change="detailRefreshChange"
                 @on-load="detailOnLoad">
                 <template slot="menuLeft">
-                    <el-button type="danger" size="small" plain icon="el-icon-delete" 
+                    <el-button type="danger" size="small" plain icon="el-icon-delete"
                         @click="handleDetailDelete" :disabled="detailSelectionList.length === 0">
                         删除
                     </el-button>
@@ -115,7 +116,42 @@ import leadIndexMixin from './mixins/leadIndex'
 
 export default {
     name: 'Lead',
-    mixins: [leadIndexMixin]
+    mixins: [leadIndexMixin],
+    watch: {
+      detailDialogVisible(visible) {
+        if (visible) {
+          this.injectErrorStyle();
+        } else {
+          this.removeErrorStyle();
+        }
+      }
+    },
+    methods: {
+      injectErrorStyle() {
+        const styleId = 'lead-error-style';
+        if (document.getElementById(styleId)) return;
+
+        const style = document.createElement('style');
+        style.id = styleId;
+        style.textContent = `
+          .el-form-item__error {
+            display: block !important;
+            position: static !important;
+            margin-top: 4px !important;
+            white-space: normal !important;
+            color: #f56c6c !important;
+          }
+        `;
+        document.head.appendChild(style);
+      },
+      removeErrorStyle() {
+        const styleId = 'lead-error-style';
+        const style = document.getElementById(styleId);
+        if (style) {
+          style.remove();
+        }
+      }
+    }
 }
 </script>
 
@@ -134,38 +170,38 @@ export default {
     border-radius: 4px;
     padding: 15px;
     background-color: #fff;
-    
+
     .info-row {
         display: flex;
         margin-bottom: 10px;
-        
+
         &:last-child {
             margin-bottom: 0;
         }
-        
+
         &.title-row {
             margin-top: 5px;
         }
     }
-    
+
     .info-item {
         flex: 1;
         padding: 0 10px;
-        
+
         &.full-width {
             flex: 3;
             width: 100%;
         }
-        
+
         .label {
             font-weight: bold;
             color: #606266;
             margin-right: 5px;
         }
-        
+
         .value {
             color: #303133;
-            
+
             &.title-value {
                 font-size: 16px;
             }
@@ -179,4 +215,12 @@ export default {
         margin: 0;
     }
 }
-</style>
+
+// 仅在本组件的弹层内,修正 Element UI 错误提示的定位与显示,确保可见
+.el-form-item__error {
+    display: block !important;
+    position: static !important;
+    white-space: normal !important;
+    color: #f56c6c !important;
+}
+</style>

+ 16 - 17
src/views/lead/mixins/leadIndex.js

@@ -21,7 +21,8 @@ import {
   isLeadEditable,
   isLeadConvertible,
   isLeadCompleted,
-  isLeadActive
+  isLeadActive,
+  LEAD_SOURCE_OPTIONS
 } from '@/constants/lead'
 
 
@@ -127,11 +128,12 @@ export default {
                         rules: [{
                             required: true,
                             message: '请输入详细信息内容',
-                            trigger: 'blur'
+                            trigger: ['blur', 'change']
                         }, {
+                            type: 'string',
                             min: 10,
                             message: '详细信息内容至少10个字符',
-                            trigger: 'blur'
+                            trigger: ['blur', 'change']
                         }],
                         overHidden: true,
                         width: 400
@@ -418,11 +420,7 @@ export default {
                         label: '优先级',
                         prop: 'priority',
                         type: 'select',
-                        dicData: [
-                            { label: '高', value: 1 },
-                            { label: '中', value: 2 },
-                            { label: '低', value: 3 }
-                        ],
+                        dicData: LEAD_PRIORITY_OPTIONS,
                         rules: [{
                             required: true,
                             message: '请选择优先级',
@@ -434,9 +432,15 @@ export default {
                     {
                         label: '线索来源',
                         prop: 'source',
+                        type: 'select',
+                        filterable: true,
+                        allowCreate: true,
+                        clearable: true,
+                        placeholder: '请选择或输入线索来源',
+                        dicData: LEAD_SOURCE_OPTIONS,
                         rules: [{
                             required: true,
-                            message: '请输入线索来源',
+                            message: '请选择或输入线索来源',
                             trigger: 'blur'
                         }],
                         search: true
@@ -455,11 +459,7 @@ export default {
                         label: '状态',
                         prop: 'status',
                         type: 'select',
-                        dicData: [
-                            { label: '新建', value: 1 },
-                            { label: '跟进中', value: 2 },
-                            { label: '已关闭', value: 3 }
-                        ],
+                        dicData: LEAD_STATUS_OPTIONS,
                         rules: [{
                             required: true,
                             message: '请选择状态',
@@ -1151,12 +1151,11 @@ export default {
         
         /**
          * 详细信息刷新变更
-         * @param {Object} page - 分页参数
          * @returns {void}
          * @this {LeadComponent & Vue}
          */
-        detailRefreshChange(page) {
-            this.detailOnLoad(page)
+        detailRefreshChange() {
+            this.detailOnLoad(this.detailPage)
         },
         
         /**

+ 4 - 4
src/views/lead/types.d.ts

@@ -9,7 +9,7 @@ export interface LeadRecord {
   createTime: string
   updateUser: string
   updateTime: string
-  status: number // 1-新建 2-跟进中 3-已关闭
+  status: number // 0-待处理 1-跟进中 2-已转化 3-已关闭
   isDeleted: number // 0-未删除 1-已删除
   leadCode: string
   customerId: number
@@ -19,7 +19,7 @@ export interface LeadRecord {
   contactPhone: string
   title: string
   endTime: string
-  priority: number // 1-高 2-中 3-低
+  priority: number // 1-低 2-中 3-高
   source: string
   groupName: string
   closeReason?: string | null
@@ -32,7 +32,7 @@ export interface LeadDetailRecord {
   createTime: string
   updateUser: string
   updateTime: string
-  status: number
+  status: number // 0-待处理 1-跟进中 2-已转化 3-已关闭
   isDeleted: number
   leadId: string
   followTime: string
@@ -48,7 +48,7 @@ export interface LeadQueryParams {
   contactName?: string
   contactPhone?: string
   title?: string
-  status?: number
+  status?: number // 0-待处理 1-跟进中 2-已转化 3-已关闭
   priority?: number
   source?: string
   groupName?: string

+ 46 - 8
src/views/order/factory/index.vue

@@ -33,6 +33,11 @@
         {{ parseInt(row.totalQuantity || 0) }}
       </template>
 
+      <!-- 明细条数显示 -->
+      <template slot="detailCount" slot-scope="{row}">
+        {{ (Array.isArray(row.pcBladeOrderItemList) ? row.pcBladeOrderItemList.length : (typeof row.detailCount === 'number' ? row.detailCount : 0)) }}
+      </template>
+
       <!-- 订单状态显示 -->
       <template slot="status" slot-scope="{row}">
         <el-tag :type="getOrderStatusTagType(row.status)">
@@ -68,6 +73,7 @@ import { getList } from '@/api/order/order'
 import { getOrderTypeLabel, getOrderTypeTagType, getOrderStatusLabel, getOrderStatusTagType } from '@/constants'
 import OrderItemTable from '@/components/order-item-table/index.vue'
 import { safeBigInt } from '@/util/util'
+import { getList as getOrderItemList } from '@/api/order/order-item'
 
 export default {
   name: 'FactoryOrderList',
@@ -86,9 +92,11 @@ export default {
 
     return {
       option: opt,
+      /** @type {FactoryOrderRecord[]} */
       data: [],
       form: {},
       loading: true,
+      /** @type {FactoryOrderRecord[]} */
       selectionList: [],
       page: { pageSize: 10, currentPage: 1, total: 0 },
       // 记录最后一次有效的搜索参数,保证分页时参数不丢失
@@ -120,26 +128,53 @@ export default {
       // 合并来源:上次有效参数 + 当前表单 + 本次传入
       const source = Object.assign({}, this.lastQuery || {}, this.form || {}, params || {})
       // 仅映射后端支持的查询参数,并兼容 Avue 日期范围
+      /** @type {any} */
+      const anySource = source
+      const formRange = Array.isArray(anySource.createTime) ? anySource.createTime : undefined
       const query = {
         orderCode: source.orderCode,
         orgName: source.orgName,
         customerName: source.customerName,
         receiverName: source.receiverName,
         receiverPhone: source.receiverPhone,
-        createTimeStart: source.createTimeStart || (Array.isArray(source.createTime) ? source.createTime[0] : undefined),
-        createTimeEnd: source.createTimeEnd || (Array.isArray(source.createTime) ? source.createTime[1] : undefined),
+        createTimeStart: source.createTimeStart || (formRange ? formRange[0] : undefined),
+        createTimeEnd: source.createTimeEnd || (formRange ? formRange[1] : undefined),
         orderType: source.orderType,
         status: source.status
       }
-      Object.keys(query).forEach(k => query[k] === undefined && delete query[k])
+      // 清理 undefined 字段,避免多余查询参数
+      const cleanedQuery = {}
+      if (query.orderCode !== undefined) cleanedQuery.orderCode = query.orderCode
+      if (query.orgName !== undefined) cleanedQuery.orgName = query.orgName
+      if (query.customerName !== undefined) cleanedQuery.customerName = query.customerName
+      if (query.receiverName !== undefined) cleanedQuery.receiverName = query.receiverName
+      if (query.receiverPhone !== undefined) cleanedQuery.receiverPhone = query.receiverPhone
+      if (query.createTimeStart !== undefined) cleanedQuery.createTimeStart = query.createTimeStart
+      if (query.createTimeEnd !== undefined) cleanedQuery.createTimeEnd = query.createTimeEnd
+      if (query.orderType !== undefined) cleanedQuery.orderType = query.orderType
+      if (query.status !== undefined) cleanedQuery.status = query.status
+
       // 持久化搜索条件,确保翻页时不丢失
-      this.lastQuery = Object.assign({}, query)
+      this.lastQuery = Object.assign({}, cleanedQuery)
 
-      getList(page.currentPage, page.pageSize, query)
-        .then(res => {
+      getList(page.currentPage, page.pageSize, cleanedQuery)
+        .then(async res => {
           const data = res.data && res.data.data ? res.data.data : { records: [], total: 0 }
-          this.data = Array.isArray(data.records) ? data.records : []
-          this.page.total = Number(data.total || 0)
+          const records = Array.isArray(data.records) ? data.records : []
+          const total = Number(data.total || 0)
+          // 并行查询每条订单的明细总数(使用分页响应 total 字段作为计数)
+          const counts = await Promise.all(records.map(async (row) => {
+            try {
+              const resp = await getOrderItemList(1, 1, { orderId: row.id })
+              const payload = resp && resp.data && resp.data.data ? resp.data.data : { total: 0 }
+              return Number(payload.total || 0)
+            } catch (e) {
+              return 0
+            }
+          }))
+          // 合并计数到记录中
+          this.data = records.map((row, idx) => ({ ...row, detailCount: counts[idx] }))
+          this.page.total = total
           this.loading = false
         })
         .catch(() => { this.loading = false })
@@ -162,15 +197,18 @@ export default {
       this.onLoad(this.page)
     },
     /** 选择变化 */
+    /** @param {FactoryOrderRecord[]} list */
     selectionChange(list) {
       this.selectionList = list
     },
     /** 页码变化:保持搜索条件不丢失并重新加载 */
+    /** @param {number} currentPage */
     currentChange(currentPage) {
       this.page.currentPage = currentPage
       this.onLoad(this.page, this.lastQuery)
     },
     /** 每页大小变化:保持搜索条件不丢失并重新加载 */
+    /** @param {number} pageSize */
     sizeChange(pageSize) {
       this.page.pageSize = pageSize
       this.onLoad(this.page, this.lastQuery)

+ 71 - 22
src/views/order/order/index-avue.vue

@@ -57,6 +57,12 @@
       </template>
 
       <!-- 订单类型显示 -->
+      <template slot="orderCode" slot-scope="{row}">
+        <el-link type="primary" :underline="true" @click="handleView(row)">
+          {{ row.orderCode }}
+        </el-link>
+      </template>
+
       <template slot="orderType" slot-scope="{row}">
         <el-tag :type="getOrderTypeTagType(row.orderType)">
           {{ getOrderTypeLabel(row.orderType) }}
@@ -73,6 +79,11 @@
         {{ parseInt(row.totalQuantity || 0) }}
       </template>
 
+      <!-- 明细条数显示 -->
+      <template slot="detailCount" slot-scope="{row}">
+        {{ row.detailCount && typeof row.detailCount === 'number' ? row.detailCount : 0 }}
+      </template>
+
       <!-- 订单状态显示 -->
       <template slot="status" slot-scope="{row}">
         <el-tag :type="getOrderStatusTagType(row.status)">
@@ -80,6 +91,13 @@
         </el-tag>
       </template>
 
+      <!-- 提交状态显示:草稿显示未提交,非草稿显示已提交 -->
+      <template slot="submitStatus" slot-scope="{row}">
+        <el-tag :type="row.status === ORDER_STATUS.DRAFT ? 'info' : 'success'">
+          {{ row.status === ORDER_STATUS.DRAFT ? '未提交' : '已提交' }}
+        </el-tag>
+      </template>
+
       <!-- 自定义操作菜单 -->
       <template slot-scope="{row}" slot="menu">
         <el-button
@@ -117,6 +135,7 @@ import { option } from './option'
 // 原接口: import { getList, add, update, getDetail } from '@/api/order/order'
 import { add, update, getDetail } from '@/api/order/order'
 import { getOrderList } from '@/api/order/sales-order'
+import { getList as getOrderItemList } from '@/api/order/order-item'
 import { submitOrderToU9 } from '@/api/order/sales-order'
 import { getUnification } from '@/api/common'
 import {
@@ -141,15 +160,13 @@ export default {
   data() {
     return {
       option,
+      /** @type {import('@/api/types/order').SalesOrderRecord[]} */
       data: [],
       form: {},
       loading: true,
+      /** @type {import('@/api/types/order').SalesOrderRecord[]} */
       selectionList: [],
-      page: {
-        pageSize: 10,
-        currentPage: 1,
-        total: 0
-      },
+      page: { pageSize: 10, currentPage: 1, total: 0 },
 
       // 订单表单相关状态
       orderFormVisible: false,
@@ -159,7 +176,9 @@ export default {
       syncLoading: false,
       
       // 事件常量
-      ORDER_FORM_EVENTS
+      ORDER_FORM_EVENTS,
+      // 暴露到模板的订单状态常量,供插槽中使用 ORDER_STATUS.DRAFT 判断
+      ORDER_STATUS
     }
   },
   computed: {
@@ -185,22 +204,35 @@ export default {
      */
     onLoad(page, params = {}) {
       this.loading = true
-      // 仅映射销售订单接口支持的查询参数,并兼容 Avue 日期范围
+      /** @type {any} */
+      const anySource = params
+      const formRange = Array.isArray(anySource.createTime) ? anySource.createTime : undefined
       const query = {
-        orderCode: params.orderCode,
-        orgName: params.orgName,
-        customerName: params.customerName,
-        receiverName: params.receiverName,
-        receiverPhone: params.receiverPhone,
-        createTimeStart: params.createTimeStart || (Array.isArray(params.createTime) ? params.createTime[0] : undefined),
-        createTimeEnd: params.createTimeEnd || (Array.isArray(params.createTime) ? params.createTime[1] : undefined)
+        orderCode: anySource.orderCode,
+        orgName: anySource.orgName,
+        customerName: anySource.customerName,
+        receiverName: anySource.receiverName,
+        receiverPhone: anySource.receiverPhone,
+        createTimeStart: anySource.createTimeStart || (formRange ? formRange[0] : undefined),
+        createTimeEnd: anySource.createTimeEnd || (formRange ? formRange[1] : undefined)
       }
-      // 移除 undefined 字段,避免向后端传递空参数
       Object.keys(query).forEach(k => query[k] === undefined && delete query[k])
 
-      getOrderList(page.currentPage, page.pageSize, query).then(res => {
+      getOrderList(page.currentPage, page.pageSize, query).then(async res => {
         const data = res.data.data
-        this.data = data.records
+        const records = Array.isArray(data.records) ? data.records : []
+        // 与工厂列表一致:并行查询每条订单的明细总数,使用分页响应的 total
+        const counts = await Promise.all(records.map(async (row) => {
+          try {
+            const resp = await getOrderItemList(1, 1, { orderId: row.id })
+            const payload = resp && resp.data && resp.data.data ? resp.data.data : { total: 0 }
+            return Number(payload.total || 0)
+          } catch (e) {
+            return 0
+          }
+        }))
+        // 将计算得到的明细条数合并到每条记录的 detailCount 字段
+        this.data = records.map((row, idx) => ({ ...row, detailCount: counts[idx] }))
         this.page.total = data.total
         this.loading = false
       }).catch(() => {
@@ -338,6 +370,14 @@ export default {
       this.orderFormVisible = true
     },
 
+    // 新增:点击订单编码查看详情
+    handleView(row) {
+      if (!row || !row.id) return
+      this.isEditMode = true
+      this.editOrderId = row.id
+      this.orderFormVisible = true
+    },
+
     /**
      * 同步数据(调用通用unification接口)
      * @returns {Promise<void>}
@@ -382,11 +422,20 @@ export default {
      * @returns {void}
      */
     handleFormSaveSuccess(orderData) {
-      this.orderFormVisible = false
-      this.isEditMode = false
-      this.editOrderId = null
-      // 刷新列表数据
-      this.onLoad(this.page)
+      if (this.isEditMode) {
+        // 编辑模式:保持在表单页,不返回列表
+        this.orderFormVisible = true
+        // 确保编辑ID保持最新(后端返回可能是字符串/数字)
+        this.editOrderId = orderData && (orderData.id || this.editOrderId)
+        // 后台刷新列表数据,不影响停留在表单
+        this.onLoad(this.page)
+      } else {
+        // 新增模式:保存后返回列表
+        this.orderFormVisible = false
+        this.isEditMode = false
+        this.editOrderId = null
+        this.onLoad(this.page)
+      }
     },
 
     /**

+ 21 - 1
src/views/order/order/option.js

@@ -53,7 +53,7 @@ export const option = {
   searchMenuSpan: 6,
   border: true,
   index: true,
-  viewBtn: true,
+  viewBtn: false,
   editBtn: true,
   delBtn: false, // 禁用删除按钮
   selection: true,
@@ -69,6 +69,7 @@ export const option = {
       label: '订单编码',
       prop: 'orderCode',
       minWidth: 150,
+      slot: true,
       search: true,
       editDisabled: true,
       addDisplay: false,
@@ -149,6 +150,16 @@ export const option = {
       editDisabled: true,
       addDisplay: false
     },
+    // 新增:提交状态(虚拟列,使用插槽渲染,不依赖后端字段)
+    {
+      label: '提交状态',
+      prop: 'submitStatus',
+      minWidth: 100,
+      align: 'center',
+      slot: true,
+      editDisabled: true,
+      addDisplay: false
+    },
     {
       label: '订单总金额',
       prop: 'totalAmount',
@@ -178,6 +189,15 @@ export const option = {
       }]
     },
     {
+      label: '明细条数',
+      prop: 'detailCount',
+      minWidth: 100,
+      align: 'center',
+      slot: true,
+      addDisplay: false,
+      editDisabled: true
+    },
+    {
       label: '收货人姓名',
       prop: 'receiverName',
       minWidth: 120,