|
|
@@ -59,7 +59,7 @@
|
|
|
|
|
|
// API接口导入
|
|
|
import { addForecast, updateForecast, getForecastDetail } from '@/api/forecast'
|
|
|
-import { addSalesForecastMain, updateSalesForecastMain, getSalesForecastSummaryByMonth, exportSalesForecastTemplate, exportSalesForecastSummaryByYearMonth, importSalesForecastSummaryById } from '@/api/forecast/forecast-summary'
|
|
|
+import { addSalesForecastMain, updateSalesForecastMain, getSalesForecastSummaryByMonth, exportSalesForecastTemplate, exportSalesForecastSummaryByYearMonth, importSalesForecastSummaryById, getSalesForecastMainList } from '@/api/forecast/forecast-summary'
|
|
|
import { getUserLinkGoods } from '@/api/order/sales-order'
|
|
|
|
|
|
// 常量和枚举导入
|
|
|
@@ -218,6 +218,8 @@ export default {
|
|
|
dataExportLoading: false,
|
|
|
/** 导入加载状态 */
|
|
|
importLoading: false,
|
|
|
+ /** 导入前预保存标记(用于新增态先落主表再导入) */
|
|
|
+ preSavedForImport: false,
|
|
|
|
|
|
/** 客户选项列表
|
|
|
* @type {Array<CustomerOption>}
|
|
|
@@ -974,6 +976,7 @@ export default {
|
|
|
*/
|
|
|
initFormData() {
|
|
|
if (this.isEdit && this.editData) {
|
|
|
+ this.preSavedForImport = false
|
|
|
// 编辑模式:使用传入的数据,确保year字段为字符串格式
|
|
|
this.formData = {
|
|
|
...this.editData,
|
|
|
@@ -989,6 +992,7 @@ export default {
|
|
|
// 非关键性异常,忽略
|
|
|
}
|
|
|
} else {
|
|
|
+ this.preSavedForImport = false
|
|
|
// 新增模式:使用默认数据,自动填入下个月
|
|
|
const now = new Date()
|
|
|
const currentYear = now.getFullYear()
|
|
|
@@ -1181,34 +1185,7 @@ export default {
|
|
|
}
|
|
|
|
|
|
// 组装子项明细,仅保留预测数量>0的行
|
|
|
- const items = this.stockTableData
|
|
|
- .filter(row => Number(row.forecastQuantity) > 0)
|
|
|
- .map(row => {
|
|
|
- const matchedBrand = this.brandDescList.find(b => b.cname === row.brandName)
|
|
|
- const rawBrandId = row.brandId != null && row.brandId !== '' ? row.brandId : (matchedBrand ? matchedBrand.id : '')
|
|
|
- const rawItemId = row.goodsId
|
|
|
-
|
|
|
- const brandId = toIdOutput(rawBrandId)
|
|
|
- const itemId = toIdOutput(rawItemId)
|
|
|
-
|
|
|
- const base = {
|
|
|
- brandId,
|
|
|
- brandCode: row.brandCode || '',
|
|
|
- brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
|
|
|
- itemId,
|
|
|
- itemCode: row.code || '',
|
|
|
- itemName: row.cname || '',
|
|
|
- specs: row.typeNo || '',
|
|
|
- pattern: row.productDescription || row.brandItem || '',
|
|
|
- forecastQuantity: toSafeNumberOrString(row.forecastQuantity),
|
|
|
- approvalStatus: Number(this.formData.approvalStatus ?? 0)
|
|
|
- }
|
|
|
- // 编辑模式下,如果明细有 id,带上给后端做区分
|
|
|
- if (this.isEdit && (row.id != null && row.id !== '')) {
|
|
|
- return { id: toIdOutput(row.id), ...base }
|
|
|
- }
|
|
|
- return base
|
|
|
- })
|
|
|
+ const items = this.buildForecastItems({ includeRowId: this.isEdit })
|
|
|
|
|
|
// 新增模式下需要至少一条有效明细;编辑模式下仅提交主表四个字段,不校验明细条数
|
|
|
if (!this.isEdit && !items.length) {
|
|
|
@@ -1227,7 +1204,7 @@ export default {
|
|
|
}
|
|
|
|
|
|
let res
|
|
|
- if (this.isEdit && this.formData.id) {
|
|
|
+ if ((this.isEdit || this.preSavedForImport) && this.formData.id) {
|
|
|
// 更新:需要主表 id
|
|
|
res = await updateSalesForecastMain({ id: toIdOutput(this.formData.id), ...payloadBase })
|
|
|
} else {
|
|
|
@@ -1367,10 +1344,6 @@ export default {
|
|
|
* @this {ForecastFormMixinComponent & Vue}
|
|
|
*/
|
|
|
handleImportClick() {
|
|
|
- if (!this.isEdit || !this.formData.id) {
|
|
|
- this.$message && this.$message.warning('当前记录未保存,无法导入')
|
|
|
- return
|
|
|
- }
|
|
|
const input = this.$refs && this.$refs.importInput
|
|
|
if (input && input.click) {
|
|
|
input.value = ''
|
|
|
@@ -1379,6 +1352,176 @@ export default {
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
+ * 从保存接口响应中提取预测ID
|
|
|
+ * @param {any} res
|
|
|
+ * @returns {string|number|null}
|
|
|
+ */
|
|
|
+ extractForecastIdFromResponse(res) {
|
|
|
+ const data = res && res.data ? res.data.data : null
|
|
|
+ if (data == null) return null
|
|
|
+ if (typeof data === 'number') return data
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ const trimmed = data.trim()
|
|
|
+ if (/^\d+$/.test(trimmed)) return trimmed
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ if (typeof data === 'object') {
|
|
|
+ const raw = data.id || data.forecastId || data.forecastMainId || null
|
|
|
+ if (raw == null) return null
|
|
|
+ if (typeof raw === 'number') return raw
|
|
|
+ if (typeof raw === 'string') {
|
|
|
+ const trimmed = raw.trim()
|
|
|
+ return /^\d+$/.test(trimmed) ? trimmed : null
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过年份/月和客户名称查询预测ID(用于新增保存后无返回ID的兜底)
|
|
|
+ * @param {number} year
|
|
|
+ * @param {number} month
|
|
|
+ * @returns {Promise<string|number|null>}
|
|
|
+ */
|
|
|
+ async fetchForecastIdByYearMonth(year, month) {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ year,
|
|
|
+ month
|
|
|
+ }
|
|
|
+ if (this.formData && this.formData.customerName) {
|
|
|
+ params.customerName = this.formData.customerName
|
|
|
+ }
|
|
|
+ const res = await getSalesForecastMainList(1, 1, params)
|
|
|
+ const pageData = res && res.data && res.data.code === 200 ? res.data.data : null
|
|
|
+ const records = pageData && Array.isArray(pageData.records) ? pageData.records : []
|
|
|
+ if (!records.length) return null
|
|
|
+ const candidate = records[0] && records[0].id
|
|
|
+ if (typeof candidate === 'number') return candidate
|
|
|
+ if (typeof candidate === 'string') {
|
|
|
+ const trimmed = candidate.trim()
|
|
|
+ return /^\d+$/.test(trimmed) ? trimmed : null
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('根据年月查询预测ID失败:', e)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建提交的预测明细列表
|
|
|
+ * @param {{ includeRowId?: boolean }} [options]
|
|
|
+ * @returns {Array<any>}
|
|
|
+ */
|
|
|
+ buildForecastItems(options = {}) {
|
|
|
+ const includeRowId = !!options.includeRowId
|
|
|
+
|
|
|
+ /** @param {unknown} val @returns {string|number|''} */
|
|
|
+ const toIdOutput = (val) => {
|
|
|
+ if (val == null || val === '') return ''
|
|
|
+ try {
|
|
|
+ const bi = BigInt(String(val))
|
|
|
+ const absBi = bi >= 0n ? bi : -bi
|
|
|
+ const maxSafe = BigInt(Number.MAX_SAFE_INTEGER)
|
|
|
+ if (absBi <= maxSafe) {
|
|
|
+ return Number(bi)
|
|
|
+ }
|
|
|
+ return String(bi)
|
|
|
+ } catch (e) {
|
|
|
+ return String(val)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @param {unknown} val @returns {number|string} */
|
|
|
+ const toSafeNumberOrString = (val) => {
|
|
|
+ if (val == null || val === '') return 0
|
|
|
+ if (typeof val === 'number') {
|
|
|
+ return Number.isFinite(val) ? val : String(val)
|
|
|
+ }
|
|
|
+ const parsed = Number(val)
|
|
|
+ return Number.isFinite(parsed) ? parsed : String(val)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (Array.isArray(this.stockTableData) ? this.stockTableData : [])
|
|
|
+ .filter(row => Number(row.forecastQuantity) > 0)
|
|
|
+ .map(row => {
|
|
|
+ const matchedBrand = (this.brandDescList || []).find(b => b.cname === row.brandName)
|
|
|
+ const rawBrandId = row.brandId != null && row.brandId !== '' ? row.brandId : (matchedBrand ? matchedBrand.id : '')
|
|
|
+ const rawItemId = row.goodsId
|
|
|
+
|
|
|
+ const brandId = toIdOutput(rawBrandId)
|
|
|
+ const itemId = toIdOutput(rawItemId)
|
|
|
+
|
|
|
+ const base = {
|
|
|
+ brandId,
|
|
|
+ brandCode: row.brandCode || '',
|
|
|
+ brandName: row.brandName || (matchedBrand ? matchedBrand.cname : ''),
|
|
|
+ itemId,
|
|
|
+ itemCode: row.code || '',
|
|
|
+ itemName: row.cname || '',
|
|
|
+ specs: row.typeNo || '',
|
|
|
+ pattern: row.productDescription || row.brandItem || '',
|
|
|
+ forecastQuantity: toSafeNumberOrString(row.forecastQuantity),
|
|
|
+ approvalStatus: Number(this.formData.approvalStatus ?? 0)
|
|
|
+ }
|
|
|
+ if (includeRowId && (row.id != null && row.id !== '')) {
|
|
|
+ return { id: toIdOutput(row.id), ...base }
|
|
|
+ }
|
|
|
+ return base
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导入前预保存主表,确保拿到预测ID
|
|
|
+ * @returns {Promise<string|number|null>}
|
|
|
+ * @this {ForecastFormMixinComponent & Vue}
|
|
|
+ */
|
|
|
+ async ensureForecastIdForImport() {
|
|
|
+ if (this.formData.id) return this.formData.id
|
|
|
+ if (!this.formData.customerId) {
|
|
|
+ this.$message && this.$message.warning('请选择客户')
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const year = typeof this.formData.year === 'string' ? parseInt(this.formData.year, 10) : this.formData.year
|
|
|
+ const month = Number(this.formData.month)
|
|
|
+ if (!year || !month) {
|
|
|
+ this.$message && this.$message.warning('请先选择年份和月份')
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ const items = this.buildForecastItems({ includeRowId: false })
|
|
|
+ const payloadBase = {
|
|
|
+ year,
|
|
|
+ month,
|
|
|
+ approvalStatus: Number(this.formData.approvalStatus ?? 0),
|
|
|
+ pcBladeSalesForecastSummaryList: items
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await addSalesForecastMain(payloadBase)
|
|
|
+ if (!(res && res.data && res.data.success)) {
|
|
|
+ const msg = (res && res.data && (res.data.msg || res.data.message)) || '保存失败,请稍后重试'
|
|
|
+ this.$message && this.$message.error(msg)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ let newId = this.extractForecastIdFromResponse(res)
|
|
|
+ if (!newId) {
|
|
|
+ newId = await this.fetchForecastIdByYearMonth(year, month)
|
|
|
+ }
|
|
|
+ if (!newId) {
|
|
|
+ this.$message && this.$message.error('保存成功但未查询到预测ID')
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ this.formData.id = newId
|
|
|
+ this.preSavedForImport = true
|
|
|
+ return newId
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
* 处理导入文件选择
|
|
|
* @param {Event} event
|
|
|
* @returns {Promise<void>}
|
|
|
@@ -1388,14 +1531,21 @@ export default {
|
|
|
const target = event && event.target
|
|
|
const file = target && target.files ? target.files[0] : null
|
|
|
if (!file) return
|
|
|
- if (!this.formData.id) {
|
|
|
- this.$message && this.$message.warning('未获取到当前记录ID')
|
|
|
- return
|
|
|
- }
|
|
|
|
|
|
try {
|
|
|
this.importLoading = true
|
|
|
- const res = await importSalesForecastSummaryById(this.formData.id, file)
|
|
|
+ let forecastId = this.formData.id
|
|
|
+ if (!forecastId) {
|
|
|
+ forecastId = await this.ensureForecastIdForImport()
|
|
|
+ if (!forecastId) return
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await importSalesForecastSummaryById(forecastId, file)
|
|
|
+ if (!(res && res.data && res.data.success)) {
|
|
|
+ const msg = (res && res.data && (res.data.msg || res.data.message)) || '导入失败,请稍后重试'
|
|
|
+ this.$message && this.$message.error(msg)
|
|
|
+ return
|
|
|
+ }
|
|
|
const list = res && res.data && res.data.data ? res.data.data : []
|
|
|
if (!Array.isArray(list)) {
|
|
|
this.$message && this.$message.warning('导入数据格式异常')
|