1218 lines
43 KiB
Vue
1218 lines
43 KiB
Vue
<template>
|
||
<el-card shadow="never" class="step-card" v-loading="pulling" :element-loading-text="loadingText">
|
||
<template #header>
|
||
<div class="card-head">
|
||
<span>
|
||
{{ headerTitle }}
|
||
<span class="sub">{{ typeHint }}</span>
|
||
</span>
|
||
<div v-if="hasBuildMode" class="card-actions">
|
||
<el-button
|
||
v-if="showPullAction"
|
||
size="small"
|
||
:disabled="(livePersist && !priceSetId) || pulling"
|
||
@click="handlePullLibrary"
|
||
>拉取产品列表</el-button>
|
||
<el-dropdown
|
||
v-if="showImportAction && !isModifyImport && showCountryImport"
|
||
trigger="click"
|
||
@command="handleImportCommand"
|
||
>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
:disabled="(livePersist && !priceSetId) || pulling || (livePersist && buildModeDirty)"
|
||
>
|
||
{{ importActionLabel }}
|
||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="all">全部导入(多国家价表)</el-dropdown-item>
|
||
<el-dropdown-item command="country">按国家导入(单国专线成本价)</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
<el-button
|
||
v-else-if="showImportAction"
|
||
type="primary"
|
||
size="small"
|
||
:disabled="(livePersist && !priceSetId) || pulling || (livePersist && buildModeDirty)"
|
||
@click="openImport"
|
||
>{{ importActionLabel }}</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Exclusive quote: only Excel fresh build; hide the mode picker and default IMPORT_FRESH -->
|
||
<div v-if="!onlyImportFresh" class="build-choice">
|
||
<p class="build-choice__tip">创建价格方式</p>
|
||
<div class="build-choice__cards">
|
||
<button
|
||
type="button"
|
||
class="build-card build-card--pull"
|
||
:class="{ selected: isPullBuildMode(priceBuildMode) }"
|
||
:disabled="!canChooseBuildMode"
|
||
@click="chooseBuildMode('PULL')"
|
||
>
|
||
<div class="build-card__title">按产品价建价</div>
|
||
<div class="build-card__desc">按产品库基础价生成套餐产品列表,可再导入 Excel 修改价格</div>
|
||
</button>
|
||
<button
|
||
v-if="showImportInherit"
|
||
type="button"
|
||
class="build-card build-card--import"
|
||
:class="{ selected: isImportBuildMode(priceBuildMode) }"
|
||
:disabled="!canChooseBuildMode || !isFixedType(setType)"
|
||
@click="chooseBuildMode('IMPORT')"
|
||
>
|
||
<div class="build-card__title">按上一版 Excel 建价</div>
|
||
<div class="build-card__desc">延用以前的产品价格,带出上一版后再导入 Excel 按产品+国家覆盖或新增</div>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="build-card build-card--import-fresh"
|
||
:class="{ selected: isImportFreshBuildMode(priceBuildMode) }"
|
||
:disabled="!canChooseBuildMode || !isFixedType(setType)"
|
||
@click="chooseBuildMode('IMPORT_FRESH')"
|
||
>
|
||
<div class="build-card__title">Excel 新建价</div>
|
||
<div class="build-card__desc">{{ importFreshCardDesc }}</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filters">
|
||
<el-input v-model="query.keyword" clearable :placeholder="phKeyword" style="width: 180px" />
|
||
<!-- <el-select v-model="query.taxMode" clearable :placeholder="phTax" style="width: 140px">
|
||
<el-option v-for="d in bms_tax_mode" :key="d.value" :label="d.label" :value="d.value" />
|
||
</el-select> -->
|
||
<el-button size="small" @click="noop">查询</el-button>
|
||
</div>
|
||
|
||
<el-empty
|
||
v-if="!(filteredProducts && filteredProducts.length)"
|
||
:description="emptyTip"
|
||
:image-size="80"
|
||
/>
|
||
<el-table v-else :data="filteredProducts" border>
|
||
<el-table-column :label="colCode" min-width="80">
|
||
<template #default="{ row }"><b>{{ row.productCode }}</b></template>
|
||
</el-table-column>
|
||
<el-table-column :label="colName" prop="productName" min-width="80" show-overflow-tooltip />
|
||
<el-table-column :label="colLevel" prop="serviceLevel" width="90" align="center" />
|
||
<el-table-column :label="colCargo" width="200" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.cargoAttr" type="primary" size="small" effect="plain">{{ row.cargoAttr }}</el-tag>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="colRegion" prop="destRegion" width="90" align="center" />
|
||
<el-table-column :label="colAction" width="220" align="center" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" @click="openPrice(row)">编辑价格</el-button>
|
||
<el-button link type="primary" @click="openSurchargeForProduct(row)">附加费</el-button>
|
||
<el-button link type="danger" @click="removeProduct(row.productCode)">移除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
|
||
<surcharge-picker-dialog
|
||
v-model="scVisible"
|
||
:title="scTitle"
|
||
:list="scList"
|
||
:selected-ids="scPickedIds"
|
||
@confirm="onSurchargeConfirm"
|
||
/>
|
||
|
||
<el-dialog v-model="discVisible" :title="discTitle" width="420px" append-to-body>
|
||
<el-form label-width="100px">
|
||
<el-form-item :label="colDiscount">
|
||
<el-input-number v-model="discValue" :min="1" :max="99" :precision="2" controls-position="right" />
|
||
<span class="unit">%</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="discVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="saveDiscount">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<price-edit-drawer
|
||
v-model="priceVisible"
|
||
:product-code="editCode"
|
||
:product-name="editName"
|
||
:rows="editRows"
|
||
@row-change="onRowPriceChange"
|
||
@surcharge-change="onRowSurchargeChange"
|
||
@pick-surcharge="openSurchargeFromDrawer"
|
||
/>
|
||
|
||
<excel-import-dialog
|
||
ref="importRef"
|
||
:title="importDialogTitle"
|
||
width="560px"
|
||
:show-update-support="false"
|
||
:import-desc="importDesc"
|
||
:file-limit="5"
|
||
template-file-code="SALES_ALL"
|
||
template-file-name="销售价多国导入模板"
|
||
:submit-handler="runImportAsync"
|
||
@success="onImportSuccess"
|
||
/>
|
||
|
||
<excel-import-dialog
|
||
ref="countryImportRef"
|
||
title="按国家导入"
|
||
width="560px"
|
||
:show-update-support="false"
|
||
:import-desc="countryImportDesc"
|
||
:file-limit="5"
|
||
template-file-code="SALES_COUNTRY"
|
||
template-file-name="销售价单国导入模板"
|
||
:before-submit="validateCountryImport"
|
||
:submit-handler="runCountryImportAsync"
|
||
@success="onImportSuccess"
|
||
>
|
||
<template #extra>
|
||
<el-form label-width="90px">
|
||
<el-form-item label="目的国家" required>
|
||
<el-select v-model="importCountryCode" filterable clearable placeholder="请选择国家" style="width: 100%">
|
||
<el-option
|
||
v-for="item in regionOptions"
|
||
:key="item.countryCode2"
|
||
:label="regionLabel(item)"
|
||
:value="item.countryCode2"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
</template>
|
||
</excel-import-dialog>
|
||
|
||
<el-dialog
|
||
v-model="importProgressVisible"
|
||
:title="progressDialogTitle"
|
||
width="420px"
|
||
:close-on-click-modal="false"
|
||
:close-on-press-escape="false"
|
||
:show-close="false"
|
||
append-to-body
|
||
>
|
||
<div class="import-progress">
|
||
<el-progress :percentage="importProgressPercent" :status="importProgressStatus" />
|
||
<div class="import-progress-msg">{{ importProgressMessage }}</div>
|
||
</div>
|
||
</el-dialog>
|
||
</el-card>
|
||
</template>
|
||
|
||
<script setup name="StepDetails">
|
||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||
import PriceEditDrawer from './PriceEditDrawer.vue'
|
||
import SurchargePickerDialog from './SurchargePickerDialog.vue'
|
||
import { useDetailCascade } from '../../composables/usePriceSetRefData'
|
||
import {
|
||
createEmptyDetailRow,
|
||
formatWeightLabel,
|
||
isBuildModeLockedVersion,
|
||
isDiscountType,
|
||
isFixedType,
|
||
isExcelBuildMode,
|
||
isImportBuildMode,
|
||
isImportFreshBuildMode,
|
||
isPullBuildMode,
|
||
pickLatestPriorVersion,
|
||
setTypeLabel,
|
||
surchargeAppliesToRow,
|
||
toDetailPayload
|
||
} from '../../utils/priceSet'
|
||
import { usePriceSetModule } from '../../composables/usePriceSetModule'
|
||
import { listRegionOptions } from '@/api/data/region'
|
||
|
||
const mod = usePriceSetModule()
|
||
const { api } = mod
|
||
const onlyImportFresh = computed(() => !!mod.onlyImportFresh)
|
||
const showCountryImport = computed(() => !mod.exclusiveQuote)
|
||
|
||
const props = defineProps({
|
||
details: { type: Array, required: true },
|
||
products: { type: Array, default: () => [] },
|
||
priceSetId: { type: [Number, String], default: undefined },
|
||
psvId: { type: [Number, String], default: undefined },
|
||
setType: { type: String, default: 'FIXED' },
|
||
priceBuildMode: { type: String, default: undefined },
|
||
effectiveTime: { type: String, default: undefined },
|
||
versionNo: { type: String, default: '' },
|
||
refData: { type: Object, required: true },
|
||
headerTitle: { type: String, default: '编辑产品价格' },
|
||
/** true=edit live DB; false=create local draft until finish */
|
||
livePersist: { type: Boolean, default: true },
|
||
/** Version status: draft can change build mode; started versions lock it */
|
||
versionStatus: { type: [String, Number], default: undefined },
|
||
/** True when build mode changed locally and not saved yet */
|
||
buildModeDirty: { type: Boolean, default: false },
|
||
/** False while parent still loading package data — avoid first click being wiped by applySetData */
|
||
buildModeReady: { type: Boolean, default: true },
|
||
/**
|
||
* Show「按上一版 Excel 建价」(IMPORT). Hidden for brand-new package create
|
||
* (no prior version); keep for newVersion / copy / edit.
|
||
*/
|
||
showImportInherit: { type: Boolean, default: true }
|
||
})
|
||
|
||
const emit = defineEmits(['update:psvId', 'update:priceBuildMode', 'update:pefId'])
|
||
const { proxy } = getCurrentInstance()
|
||
const { bms_tax_mode } = useDict('bms_tax_mode')
|
||
const { hydrateRow } = useDetailCascade(props.refData)
|
||
|
||
const loadingText = '正在全量拉取产品价格…'
|
||
const phKeyword = '产品代码/名称'
|
||
const phTax = '全部税模式'
|
||
const colCode = '产品代码'
|
||
const colName = '产品名称'
|
||
const colLevel = '时效等级'
|
||
const colCargo = '走货属性'
|
||
const colRegion = '目的区域'
|
||
// const colTax = '税模式'
|
||
const colAction = '操作'
|
||
const colDiscount = '产品折扣'
|
||
|
||
const query = reactive({ keyword: '', taxMode: undefined })
|
||
const priceVisible = ref(false)
|
||
const editCode = ref('')
|
||
const editName = ref('')
|
||
const editRows = ref([])
|
||
/** 抽屉当前对应的产品分组(抽屉内点附加费时用它打开勾选弹窗) */
|
||
const editGroup = ref(null)
|
||
const scVisible = ref(false)
|
||
const scTitle = ref('附加费')
|
||
const scList = ref([])
|
||
const scRows = ref([])
|
||
const scPickedIds = ref([])
|
||
/** 作用范围:null=产品级(覆盖全部明细行),否则=行级键(只写回国家+重量段那一行) */
|
||
const scScopeKey = ref(null)
|
||
const scScopeName = ref('')
|
||
const discVisible = ref(false)
|
||
const discTitle = ref('编辑价格')
|
||
const discValue = ref(undefined)
|
||
const discCode = ref('')
|
||
const importRef = ref()
|
||
const countryImportRef = ref()
|
||
const importCountryCode = ref('')
|
||
const regionOptions = ref([])
|
||
const countryImportDesc = [
|
||
'选择目的国家后上传专线成本价表(含「产品目录」),目录中的产品都会记到该国家。',
|
||
'按产品+国家覆盖:仅替换已选国家的明细,其它产品/国家保留。',
|
||
'邮编、赔偿标准等 sheet 不导入。仅套餐 Excel 建价方式可用。'
|
||
]
|
||
const importProgressVisible = ref(false)
|
||
const importProgressPercent = ref(0)
|
||
const importProgressMessage = ref('')
|
||
const importProgressStatus = ref(undefined)
|
||
const progressDialogTitle = ref('正在导入价格')
|
||
const hasBuildMode = computed(() => {
|
||
if (isPullBuildMode(props.priceBuildMode) || isExcelBuildMode(props.priceBuildMode)) return true
|
||
// Legacy / already has products: treat as build mode ready
|
||
return (props.products || []).some(p => p && p.productCode)
|
||
})
|
||
/** Started (non-draft) versions: show cards but disable switching */
|
||
const buildModeLocked = computed(() => isBuildModeLockedVersion(props.versionStatus))
|
||
const choosingMode = ref(false)
|
||
const canChooseBuildMode = computed(() =>
|
||
!!props.buildModeReady && !buildModeLocked.value && !choosingMode.value
|
||
)
|
||
const isModifyImport = computed(() => {
|
||
if (isPullBuildMode(props.priceBuildMode)) return true
|
||
if (isExcelBuildMode(props.priceBuildMode)) return false
|
||
return (props.products || []).some(p => p && p.productCode)
|
||
})
|
||
const importActionLabel = computed(() =>
|
||
isModifyImport.value ? '导入修改价格' : '导入 Excel 建价'
|
||
)
|
||
const importDialogTitle = computed(() => importActionLabel.value)
|
||
const importFreshCardDesc = computed(() => onlyImportFresh.value
|
||
? '仅支持大客折扣报价表(目的地/运费/操作费),导入后直接生成专属报价明细'
|
||
: '不延用以前的产品价格,不带出上一版,上传 Excel 直接生成套餐销售价'
|
||
)
|
||
const importDesc = computed(() => {
|
||
if (onlyImportFresh.value) {
|
||
return [
|
||
'\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868\uff08\u542b\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u4e0e\u5404\u56fd\u4ef7\u8868\uff09\u3002',
|
||
'\u8868\u5934\u4e3a\u76ee\u7684\u5730 / \u91cd\u91cf\u6bb5 / \u8fd0\u8d39(CNY/KG) / \u64cd\u4f5c\u8d39(CNY/PCS)\uff0c\u4e0e\u666e\u901a\u5957\u9910 Excel \u6a21\u677f\u4e0d\u540c\u3002',
|
||
'\u4ec5\u300cExcel \u65b0\u5efa\u4ef7\u300d\uff1a\u4e0d\u62c9\u53d6\u4ea7\u54c1\u5e93\u3001\u4e0d\u5ef6\u7528\u4e0a\u4e00\u7248\uff0c\u5bfc\u5165\u540e\u76f4\u63a5\u751f\u6210\u9500\u552e\u4ef7\u660e\u7ec6\u3002'
|
||
]
|
||
}
|
||
if (isModifyImport.value) {
|
||
return [
|
||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||
'按产品+国家+分区(有则匹配)+重量段匹配已有套餐明细,仅更新运费/挂号费。',
|
||
'仅固定价套餐可导入;未匹配行不会新增明细。'
|
||
]
|
||
}
|
||
if (isImportFreshBuildMode(props.priceBuildMode)) {
|
||
return [
|
||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||
'不延用以前的产品价格;导入按「产品+国家」写入套餐销售价,直接生成产品明细。',
|
||
'同一产品下重复国家+重量段将整单失败。仅固定价套餐「Excel 新建价」可用。'
|
||
]
|
||
}
|
||
return [
|
||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||
'延用以前的产品价格:先带出上一版全部产品;导入按「产品+国家」匹配:命中则覆盖该国家明细,未命中则新增;其它产品/国家保留。',
|
||
'同一产品下重复国家+重量段将整单失败。仅固定价套餐「按上一版 Excel 建价」可用。'
|
||
]
|
||
})
|
||
|
||
const typeHint = computed(() => {
|
||
if (isPullBuildMode(props.priceBuildMode)) return '· 按产品价建价'
|
||
if (isImportBuildMode(props.priceBuildMode)) return '· 按上一版 Excel 建价'
|
||
if (isImportFreshBuildMode(props.priceBuildMode)) return '· Excel 新建价'
|
||
if (hasBuildMode.value) return '· 当前:' + setTypeLabel(props.setType)
|
||
return '· 请选择创建价格方式'
|
||
})
|
||
// PULL: hide pull btn (already pulled), show modify-import.
|
||
// IMPORT / IMPORT_FRESH: show create-import.
|
||
const showPullAction = computed(() => false)
|
||
const showImportAction = computed(() => hasBuildMode.value && isFixedType(props.setType))
|
||
const emptyTip = computed(() => {
|
||
if (props.livePersist) return '暂无产品'
|
||
if (isPullBuildMode(props.priceBuildMode)) return '暂无产品,请先返回上一步重新拉取或等待拉取完成'
|
||
if (isImportBuildMode(props.priceBuildMode)) return '暂无上一版产品,请点击右上角导入 Excel 建价(按产品+国家覆盖或新增)'
|
||
if (isImportFreshBuildMode(props.priceBuildMode)) return '暂无产品,请点击右上角导入 Excel 建价'
|
||
return '暂无产品,请先选择创建价格方式'
|
||
})
|
||
|
||
|
||
/** Resolve version id: editing prop/local first; never replace with package EFFECT version. */
|
||
const resolvedPsvId = ref(props.psvId)
|
||
watch(() => props.psvId, v => { if (v != null && v !== '') resolvedPsvId.value = v })
|
||
|
||
function knownPsvId() {
|
||
if (props.psvId != null && props.psvId !== '') return props.psvId
|
||
if (resolvedPsvId.value != null && resolvedPsvId.value !== '') return resolvedPsvId.value
|
||
return null
|
||
}
|
||
|
||
async function resolvePsvId(force) {
|
||
const known = knownPsvId()
|
||
// Keep the version being edited. force must NOT switch to data.version (EFFECT).
|
||
if (known != null && !force) {
|
||
resolvedPsvId.value = known
|
||
return known
|
||
}
|
||
if (!props.priceSetId) return known
|
||
const res = await api.getPriceSet(props.priceSetId)
|
||
const data = res.data || {}
|
||
const versions = data.versions || []
|
||
if (known != null) {
|
||
const still = versions.find(v => v && String(v.psvId) === String(known))
|
||
if (still && still.psvId != null) {
|
||
resolvedPsvId.value = still.psvId
|
||
return still.psvId
|
||
}
|
||
}
|
||
// Only when no editing psvId (e.g. brand-new draft): fall back to package current.
|
||
const fromData = (data.version && data.version.psvId) ? data.version : null
|
||
const current = fromData || versions[0] || {}
|
||
if (current.psvId != null) {
|
||
resolvedPsvId.value = current.psvId
|
||
emit('update:psvId', current.psvId)
|
||
}
|
||
return resolvedPsvId.value || null
|
||
}
|
||
|
||
/** productCode -> detail rows (loaded on demand) */
|
||
const detailByProduct = reactive({})
|
||
|
||
function clearDetailCache() {
|
||
Object.keys(detailByProduct).forEach(k => delete detailByProduct[k])
|
||
}
|
||
|
||
/** Clear local product/detail draft when create wizard switches build mode. */
|
||
function clearLocalDraft() {
|
||
clearDetailCache()
|
||
if (Array.isArray(props.details)) {
|
||
props.details.splice(0, props.details.length)
|
||
}
|
||
if (Array.isArray(props.products)) {
|
||
props.products.splice(0, props.products.length)
|
||
}
|
||
priceVisible.value = false
|
||
editCode.value = ''
|
||
editName.value = ''
|
||
editRows.value = []
|
||
editGroup.value = null
|
||
}
|
||
|
||
/**
|
||
* Excel build-price: preload products + details from latest prior version
|
||
* so import can upsert by product+country.
|
||
*/
|
||
async function loadLatestPriorVersionDraft() {
|
||
if (!props.priceSetId) return false
|
||
pulling.value = true
|
||
try {
|
||
const res = await api.getPriceSet(props.priceSetId)
|
||
const data = res.data || {}
|
||
const versions = data.versions || []
|
||
// Only exclude the draft being edited. Do NOT use resolvedPsvId — it may
|
||
// point at EFFECT after resolvePsvId() and wrongly skip the real prior.
|
||
const prior = pickLatestPriorVersion(versions, props.psvId)
|
||
if (!prior || prior.psvId == null) return false
|
||
const products = (prior.products || [])
|
||
.filter(p => p && p.productCode)
|
||
.map(p => {
|
||
const row = { ...p }
|
||
delete row.pspId
|
||
delete row.psvId
|
||
delete row.priceSetId
|
||
return row
|
||
})
|
||
if (!products.length) return false
|
||
const detailRes = await api.listSetDetailByVersion(prior.psvId)
|
||
applyLocalDraft(products, detailRes.data || [])
|
||
return true
|
||
} finally {
|
||
pulling.value = false
|
||
}
|
||
}
|
||
|
||
async function loadProductDetails(productCode, force) {
|
||
if (!productCode) return []
|
||
if (!force && detailByProduct[productCode]) return detailByProduct[productCode]
|
||
if (!props.livePersist) {
|
||
const rows = (props.details || []).filter(d => d && d.productCode === productCode)
|
||
detailByProduct[productCode] = rows
|
||
return rows
|
||
}
|
||
const psvId = await resolvePsvId(false)
|
||
if (!psvId) return []
|
||
const res = await api.listSetDetailByProduct(psvId, productCode)
|
||
const rows = (res.data || []).map(d => hydrateRow(d, props.setType))
|
||
detailByProduct[productCode] = rows
|
||
// keep parent details array in sync for any legacy callers
|
||
if (Array.isArray(props.details)) {
|
||
for (let i = props.details.length - 1; i >= 0; i--) {
|
||
if (props.details[i] && props.details[i].productCode === productCode) props.details.splice(i, 1)
|
||
}
|
||
props.details.push(...rows)
|
||
}
|
||
return rows
|
||
}
|
||
|
||
const productGroups = computed(() => {
|
||
return (props.products || []).filter(p => p && p.productCode).map(p => ({
|
||
productCode: p.productCode,
|
||
productName: p.productName || p.productCode,
|
||
serviceLevel: p.serviceLevel,
|
||
cargoAttr: p.cargoAttr,
|
||
destRegion: p.destRegion,
|
||
taxMode: p.taxMode,
|
||
rows: detailByProduct[p.productCode] || []
|
||
}))
|
||
})
|
||
|
||
const filteredProducts = computed(() => {
|
||
const kw = (query.keyword || '').trim().toLowerCase()
|
||
return productGroups.value.filter(p => {
|
||
if (query.taxMode && p.taxMode !== query.taxMode) return false
|
||
if (!kw) return true
|
||
const blob = [p.productCode, p.productName].filter(Boolean).join(' ').toLowerCase()
|
||
return blob.includes(kw)
|
||
})
|
||
})
|
||
|
||
const pulling = ref(false)
|
||
|
||
function noop() {}
|
||
|
||
async function chooseBuildMode(mode) {
|
||
if (!mode || !canChooseBuildMode.value) return
|
||
if (mode === props.priceBuildMode) return
|
||
choosingMode.value = true
|
||
try {
|
||
emit('update:priceBuildMode', mode)
|
||
} finally {
|
||
choosingMode.value = false
|
||
}
|
||
}
|
||
|
||
let localKeySeq = 0
|
||
|
||
/** Apply preview products/details into parent reactive arrays (create defer mode). */
|
||
function mergeLocalProducts(existing, incoming) {
|
||
const map = new Map()
|
||
for (const p of existing || []) {
|
||
if (p && p.productCode) map.set(String(p.productCode).toUpperCase(), { ...p })
|
||
}
|
||
for (const p of incoming || []) {
|
||
if (!p || !p.productCode) continue
|
||
const key = String(p.productCode).toUpperCase()
|
||
const prev = map.get(key) || {}
|
||
const next = { ...prev, ...p }
|
||
map.set(key, next)
|
||
}
|
||
return Array.from(map.values())
|
||
}
|
||
|
||
function applyLocalDraft(products, details) {
|
||
clearDetailCache()
|
||
const hydrated = (details || []).map(d => {
|
||
const row = hydrateRow({ ...d }, props.setType)
|
||
row.psdId = undefined
|
||
row.pspId = undefined
|
||
row.priceSetId = undefined
|
||
row.psvId = undefined
|
||
row._localKey = 'L' + (++localKeySeq)
|
||
return row
|
||
})
|
||
if (Array.isArray(props.details)) {
|
||
props.details.splice(0, props.details.length)
|
||
hydrated.forEach(row => props.details.push(row))
|
||
}
|
||
if (Array.isArray(props.products)) {
|
||
props.products.splice(0, props.products.length)
|
||
;(products || []).forEach(p => props.products.push({ ...p }))
|
||
}
|
||
hydrated.forEach(d => {
|
||
if (!d || !d.productCode) return
|
||
if (!detailByProduct[d.productCode]) detailByProduct[d.productCode] = []
|
||
detailByProduct[d.productCode].push(d)
|
||
})
|
||
}
|
||
|
||
function regionLabel(item) {
|
||
const name = (item && (item.countryNameCn || item.countryNameEn)) || ''
|
||
const code = (item && item.countryCode2) || ''
|
||
return name && code ? name + ' (' + code + ')' : (name || code)
|
||
}
|
||
|
||
function loadRegionOptions() {
|
||
if (regionOptions.value.length) return
|
||
listRegionOptions().then(res => {
|
||
regionOptions.value = res.data || []
|
||
})
|
||
}
|
||
|
||
async function canOpenImport() {
|
||
// Edit live: mode change must be saved before import. Create draft: import is local preview.
|
||
if (props.livePersist && props.buildModeDirty) {
|
||
proxy.$modal.msgWarning("请先点「完成」保存建价方式")
|
||
return false
|
||
}
|
||
if (props.livePersist && !props.priceSetId) {
|
||
proxy.$modal.msgWarning('缺少套餐ID')
|
||
return false
|
||
}
|
||
if (!isFixedType(props.setType)) {
|
||
proxy.$modal.msgWarning('仅固定价套餐支持导入')
|
||
return false
|
||
}
|
||
if (props.livePersist) {
|
||
// Use the version being edited (props.psvId). Do not force-refresh to EFFECT.
|
||
const psvId = await resolvePsvId(false)
|
||
if (!psvId) {
|
||
proxy.$modal.msgWarning('缺少套餐版本,请先保存基本信息')
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
|
||
watch(
|
||
() => [onlyImportFresh.value, props.priceBuildMode, props.buildModeReady],
|
||
() => {
|
||
if (!onlyImportFresh.value || !props.buildModeReady) return
|
||
if (isImportFreshBuildMode(props.priceBuildMode)) return
|
||
if (buildModeLocked.value) return
|
||
emit('update:priceBuildMode', 'IMPORT_FRESH')
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
async function openImport() {
|
||
if (!(await canOpenImport())) return
|
||
importRef.value && importRef.value.open()
|
||
}
|
||
|
||
function handleImportCommand(cmd) {
|
||
if (cmd === 'country') openCountryImport()
|
||
else openImport()
|
||
}
|
||
|
||
async function openCountryImport() {
|
||
if (!(await canOpenImport())) return
|
||
importCountryCode.value = ''
|
||
loadRegionOptions()
|
||
countryImportRef.value && countryImportRef.value.open()
|
||
}
|
||
|
||
function validateCountryImport() {
|
||
if (!importCountryCode.value) return '请先选择目的国家'
|
||
return ''
|
||
}
|
||
|
||
function unwrapUploadFiles(uploadFile) {
|
||
const list = Array.isArray(uploadFile) ? uploadFile : [uploadFile]
|
||
const out = []
|
||
for (const item of list) {
|
||
const raw = item && (item.raw || item)
|
||
if (raw) out.push(raw)
|
||
}
|
||
return out
|
||
}
|
||
|
||
async function runImportAsync(uploadFile, countryCode) {
|
||
const files = unwrapUploadFiles(uploadFile)
|
||
if (!files.length) {
|
||
throw new Error('请先选择文件')
|
||
}
|
||
const replaceByProductCountry = !isModifyImport.value || !!countryCode
|
||
if (!props.livePersist) {
|
||
const res = await api.previewImportDetailPrices(files, {
|
||
updateOnly: isModifyImport.value && !countryCode,
|
||
replaceByProductCountry,
|
||
countryCode: countryCode || undefined,
|
||
details: props.details || []
|
||
})
|
||
const data = res.data || {}
|
||
const ids = Array.isArray(data.pefIds) ? data.pefIds : (data.pefId != null ? [data.pefId] : [])
|
||
ids.forEach(id => emit('update:pefId', id))
|
||
const products = replaceByProductCountry
|
||
? mergeLocalProducts(props.products, data.products || [])
|
||
: (isModifyImport.value ? (props.products || []) : (data.products || []))
|
||
applyLocalDraft(products, data.details || [])
|
||
return data.message || '导入完成'
|
||
}
|
||
const { task } = await api.importSetDetailPricesAsync(
|
||
files,
|
||
{
|
||
priceSetId: props.priceSetId,
|
||
psvId: await resolvePsvId(false),
|
||
updateOnly: isModifyImport.value && !countryCode,
|
||
replaceByProductCountry,
|
||
countryCode: countryCode || undefined
|
||
}
|
||
)
|
||
return (task && task.message) ? task.message : '导入完成'
|
||
}
|
||
|
||
async function runCountryImportAsync(uploadFile) {
|
||
if (!importCountryCode.value) {
|
||
throw new Error('请先选择目的国家')
|
||
}
|
||
return runImportAsync(uploadFile, importCountryCode.value)
|
||
}
|
||
|
||
function applyBuildProgress(t) {
|
||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||
else if (t.status === 'RUNNING') {
|
||
const m = t.message || ''
|
||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||
else if (m === 'matching details') importProgressMessage.value = '正在匹配并更新价格…'
|
||
else if (m === 'pulling library') importProgressMessage.value = '正在拉取产品库…'
|
||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||
else if (m.indexOf('importing ') === 0) importProgressMessage.value = '正在导入产品 ' + m.slice('importing '.length)
|
||
else if (m.indexOf('saving details ') === 0) importProgressMessage.value = '正在保存明细 ' + m.slice('saving details '.length)
|
||
else if (m === 'details exist, skip pull') importProgressMessage.value = '明细已存在,已跳过拉取'
|
||
else if (m === 'saving surcharges') importProgressMessage.value = '正在保存附加费…'
|
||
else importProgressMessage.value = m || '处理中…'
|
||
} else if (t.status === 'SUCCESS') {
|
||
importProgressMessage.value = t.message || '完成'
|
||
importProgressStatus.value = 'success'
|
||
} else if (t.status === 'FAIL') {
|
||
importProgressMessage.value = t.error || t.message || '失败'
|
||
importProgressStatus.value = 'exception'
|
||
}
|
||
}
|
||
|
||
async function onImportSuccess() {
|
||
if (!props.livePersist) return
|
||
await reloadDetailsFromServer()
|
||
}
|
||
|
||
async function reloadDetailsFromServer() {
|
||
if (!props.priceSetId) return
|
||
try {
|
||
clearDetailCache()
|
||
const res = await api.getPriceSet(props.priceSetId)
|
||
const data = res.data || {}
|
||
const versions = data.versions || []
|
||
const qPsv = resolvedPsvId.value || props.psvId
|
||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||
const fromData = (data.version && data.version.psvId) ? data.version : null
|
||
const current = byQuery || fromData || versions[0] || {}
|
||
if (current.psvId != null) {
|
||
resolvedPsvId.value = current.psvId
|
||
emit('update:psvId', current.psvId)
|
||
}
|
||
if (Array.isArray(props.details)) {
|
||
props.details.splice(0, props.details.length)
|
||
}
|
||
if (Array.isArray(props.products)) {
|
||
props.products.splice(0, props.products.length, ...(current.products || []).map(p => ({ ...p })))
|
||
}
|
||
if (priceVisible.value && editCode.value) {
|
||
const rows = await loadProductDetails(editCode.value, true)
|
||
editRows.value = rows
|
||
editGroup.value = productGroups.value.find(p => p.productCode === editCode.value) || null
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function handlePullLibrary() {
|
||
if (props.livePersist && !props.priceSetId) {
|
||
proxy.$modal.msgWarning('缺少套餐ID')
|
||
return
|
||
}
|
||
const hasProducts = (props.products || []).some(p => p && p.productCode)
|
||
let force = false
|
||
if (hasProducts) {
|
||
try {
|
||
await proxy.$modal.confirm('当前版本已有明细,拉取将清空并按产品库重建,是否继续?')
|
||
force = true
|
||
} catch (e) {
|
||
return
|
||
}
|
||
}
|
||
await pullAllFromLibrary({ force, silent: false })
|
||
}
|
||
|
||
/** Pull product library: live -> async DB; create defer -> preview into local state. */
|
||
async function pullAllFromLibrary(opts = {}) {
|
||
const silent = !!opts.silent
|
||
const force = !!opts.force || !!opts.replace
|
||
if (props.livePersist && !props.priceSetId) {
|
||
if (!silent) proxy.$modal.msgWarning('缺少套餐ID')
|
||
return { added: 0 }
|
||
}
|
||
progressDialogTitle.value = '正在拉取产品列表'
|
||
importProgressVisible.value = true
|
||
importProgressPercent.value = 0
|
||
importProgressMessage.value = '排队中…'
|
||
importProgressStatus.value = undefined
|
||
pulling.value = true
|
||
try {
|
||
if (!props.livePersist) {
|
||
importProgressMessage.value = '正在拉取产品库…'
|
||
importProgressPercent.value = 40
|
||
const res = await api.previewPullFromLibrary()
|
||
const data = res.data || {}
|
||
applyLocalDraft(data.products || [], data.details || [])
|
||
importProgressPercent.value = 100
|
||
importProgressStatus.value = 'success'
|
||
if (!silent) {
|
||
proxy.$modal.msgSuccess(data.message || '拉取完成')
|
||
}
|
||
return { added: (props.products || []).length }
|
||
}
|
||
const { task } = await api.pullSetDetailsFromLibraryAsync(
|
||
{ priceSetId: props.priceSetId, psvId: await resolvePsvId(false), force },
|
||
applyBuildProgress
|
||
)
|
||
importProgressPercent.value = 100
|
||
importProgressStatus.value = 'success'
|
||
await reloadDetailsFromServer()
|
||
if (!silent) {
|
||
proxy.$modal.msgSuccess((task && task.message) ? task.message : '拉取完成')
|
||
}
|
||
return { added: (props.products || []).length }
|
||
} catch (e) {
|
||
importProgressStatus.value = 'exception'
|
||
const tip = (e && e.task && e.task.error) || (e && e.message) || '拉取失败'
|
||
importProgressMessage.value = tip
|
||
if (!silent) proxy.$modal.msgError(tip)
|
||
await new Promise(resolve => setTimeout(resolve, 600))
|
||
return { added: 0 }
|
||
} finally {
|
||
pulling.value = false
|
||
importProgressVisible.value = false
|
||
}
|
||
}
|
||
|
||
function countryToDetail(item, productCode, productName) {
|
||
const row = createEmptyDetailRow()
|
||
row.productCode = productCode
|
||
row.productName = productName
|
||
row.countryCode = item.countryCode
|
||
row.countryName = item.countryName
|
||
row.zoneId = item.zoneId
|
||
row.zoneCode = item.zoneCode
|
||
row.zoneName = item.zoneName
|
||
row.weightMin = item.weightMin
|
||
row.weightMax = item.weightMax
|
||
row.weightLabel = formatWeightLabel(item.weightMin, item.weightMax)
|
||
row.productCountryId = item.detailId
|
||
const baseFv = item.freightPrice != null ? Number(item.freightPrice) : undefined
|
||
const baseReg = item.registrationFee != null ? Number(item.registrationFee) : undefined
|
||
row.fvValue = baseFv
|
||
row.fvRegistrationFee = baseReg
|
||
row.effectiveDate = item.effectiveDate
|
||
row.expireDate = item.expireDate
|
||
row._countryKey = item.countryCode || (item.countryName ? 'N:' + item.countryName : undefined)
|
||
return hydrateRow(row, props.setType)
|
||
}
|
||
|
||
async function openPrice(row) {
|
||
if (isDiscountType(props.setType)) {
|
||
discCode.value = row.productCode
|
||
discTitle.value = row.productCode + ' · ' + (row.productName || '') + ' · 产品折扣'
|
||
try {
|
||
const rows = await loadProductDetails(row.productCode)
|
||
const hit = rows[0]
|
||
discValue.value = hit && hit.discountValue != null ? Number(hit.discountValue) : undefined
|
||
} catch (e) {
|
||
discValue.value = undefined
|
||
}
|
||
discVisible.value = true
|
||
return
|
||
}
|
||
editCode.value = row.productCode
|
||
editName.value = row.productName || ''
|
||
editGroup.value = row
|
||
try {
|
||
editRows.value = await loadProductDetails(row.productCode)
|
||
priceVisible.value = true
|
||
} catch (e) {
|
||
proxy.$modal.msgError((e && e.message) ? e.message : '加载产品价格失败')
|
||
}
|
||
}
|
||
|
||
/** list-row surcharge: load that product details first */
|
||
async function openSurchargeForProduct(row) {
|
||
try {
|
||
const rows = await loadProductDetails(row.productCode)
|
||
await openSurcharge({ ...row, rows })
|
||
} catch (e) {
|
||
proxy.$modal.msgError((e && e.message) ? e.message : '加载产品明细失败')
|
||
}
|
||
}
|
||
|
||
/** 抽屉里点附加费:带行 = 行级(只改该行,国家+重量段),不带 = 产品级(覆盖全部明细行) */
|
||
function openSurchargeFromDrawer(row) {
|
||
const group = editGroup.value || { productCode: editCode.value, rows: editRows.value }
|
||
if (row && (row.countryCode || row.countryName)) {
|
||
openSurcharge(group, rowScopeKey(row), rowScopeLabel(row))
|
||
} else {
|
||
openSurcharge(group)
|
||
}
|
||
}
|
||
|
||
|
||
/** persist one detail row to DB immediately (edit mode only) */
|
||
async function persistDetail(row, withSurcharge) {
|
||
if (!props.livePersist) return
|
||
if (!row || !row.psdId || !props.priceSetId) return
|
||
const payload = toDetailPayload(row)
|
||
payload.priceSetId = props.priceSetId
|
||
if (!withSurcharge) delete payload.surcharges
|
||
await api.updateSetDetail(payload)
|
||
}
|
||
|
||
/** drawer hover remove surcharge -> write snapshot for this row */
|
||
async function onRowSurchargeChange(row) {
|
||
if (!row) return
|
||
if (!props.livePersist) {
|
||
proxy.$modal.msgSuccess('附加费已移除(完成时统一落库)')
|
||
return
|
||
}
|
||
if (!row.psdId) return
|
||
try {
|
||
await persistDetail(row, true)
|
||
proxy.$modal.msgSuccess('附加费已移除')
|
||
} catch (e) {
|
||
proxy.$modal.msgError((e && e.message) ? e.message : '附加费移除失败')
|
||
}
|
||
}
|
||
|
||
/** drawer field change -> immediate DB update (edit) or local only (create) */
|
||
async function onRowPriceChange(row) {
|
||
if (!row) return
|
||
if (!props.livePersist) {
|
||
proxy.$modal.msgSuccess('价格已更新(完成时统一落库)')
|
||
return
|
||
}
|
||
if (!row.psdId) {
|
||
proxy.$modal.msgWarning('明细尚未落库,请先点「完成」保存后再改价')
|
||
return
|
||
}
|
||
const prevFv = row._editPrevFv
|
||
const prevReg = row._editPrevReg
|
||
try {
|
||
await persistDetail(row, false)
|
||
proxy.$modal.msgSuccess('价格已更新')
|
||
} catch (e) {
|
||
if (prevFv !== undefined) row.fvValue = prevFv
|
||
if (prevReg !== undefined) row.fvRegistrationFee = prevReg
|
||
proxy.$modal.msgError((e && e.message) ? e.message : '价格更新失败')
|
||
}
|
||
}
|
||
|
||
async function saveDiscount() {
|
||
const hit = (props.details || []).find(d => d.productCode === discCode.value)
|
||
if (hit) {
|
||
hit.discountValue = discValue.value
|
||
if (!props.livePersist) {
|
||
proxy.$modal.msgSuccess('折扣已更新(完成时统一落库)')
|
||
} else {
|
||
try {
|
||
await persistDetail(hit, false)
|
||
proxy.$modal.msgSuccess('折扣已写入数据库')
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
discVisible.value = false
|
||
}
|
||
|
||
/** 明细行的国家 key(与 countryToDetail 的 _countryKey 同口径,大写便于比较) */
|
||
function rowCountryKey(r) {
|
||
if (!r) return ''
|
||
return String(r.countryCode || (r.countryName ? 'N:' + r.countryName : '')).toUpperCase()
|
||
}
|
||
|
||
/** 明细行的行级唯一键(国家 + 分区 + 重量段),用于精确定位「只改这一行」 */
|
||
function rowScopeKey(r) {
|
||
if (!r) return ''
|
||
const c = rowCountryKey(r)
|
||
const z = String(r.zoneCode || r.zoneName || '')
|
||
const w = String(r.weightLabel || r.weightMin || r.weightMax || '')
|
||
return `${c}|${z}|${w}`
|
||
}
|
||
|
||
/** 行级作用域展示文案,如「荷兰 · 0.3-2kg」 */
|
||
function rowScopeLabel(r) {
|
||
if (!r) return ''
|
||
const c = r.countryName || r.countryCode || ''
|
||
const z = r.zoneName || r.zoneCode || ''
|
||
const w = r.weightLabel || ''
|
||
return [c, z, w].filter(Boolean).join(' · ')
|
||
}
|
||
|
||
/**
|
||
* 打开附加费勾选弹窗
|
||
* 列表 = 价格基础 → 附加费配置 主表全量(不要求产品已做关联);
|
||
* 勾选回显 = 作用范围内明细行当前附加费的并集;
|
||
* scopeKey 传行级键时只作用于该行(国家+重量段),不传则产品级全覆盖。
|
||
*/
|
||
async function openSurcharge(row, scopeKey, scopeName) {
|
||
await props.refData.ensureAllSurcharges()
|
||
scRows.value = row.rows || []
|
||
scScopeKey.value = scopeKey || null
|
||
const targets = scScopeKey.value
|
||
? scRows.value.filter(r => rowScopeKey(r) === scScopeKey.value)
|
||
: scRows.value
|
||
scScopeName.value = scopeName || (scScopeKey.value
|
||
? ((targets[0] && rowScopeLabel(targets[0])) || '')
|
||
: '')
|
||
scList.value = props.refData.allSurchargeConfigs()
|
||
const picked = new Set()
|
||
targets.forEach(r => {
|
||
(r.surcharges || []).forEach(s => {
|
||
if (s && s.scId != null) picked.add(String(s.scId))
|
||
})
|
||
})
|
||
scPickedIds.value = [...picked]
|
||
scTitle.value = row.productCode
|
||
+ (scScopeName.value ? ' · ' + scScopeName.value : '')
|
||
+ ' · 附加费'
|
||
scVisible.value = true
|
||
}
|
||
|
||
/**
|
||
* 勾选结果写回:行级只改该行(国家+重量段),产品级覆盖该产品全部明细行;同一附加费只留一条。
|
||
*
|
||
* 校验口径:附加费的 country_code 是它自身的计费适用国家(如「燃油附加费 RO」仅对罗马
|
||
* 尼亚计费),勾选挂载时【按目标明细行的国家做适用性校验】——国家码不匹配的附加费
|
||
* 不落到该行(该行计费时也不会命中它,见 TmsPriceQuoteServiceImpl.surchargeApplies),
|
||
* 并统计不匹配数提示用户。country_code 为空 = 全部国家,直接适用。
|
||
*/
|
||
function onSurchargeConfirm(picked) {
|
||
const ids = new Set((picked || []).map(s => String(s.scId)))
|
||
const targets = scScopeKey.value
|
||
? scRows.value.filter(r => rowScopeKey(r) === scScopeKey.value)
|
||
: scRows.value
|
||
let applied = 0
|
||
let skipped = 0
|
||
targets.forEach(r => {
|
||
const seen = new Set()
|
||
const list = []
|
||
scList.value.forEach(rel => {
|
||
const key = String(rel.scId)
|
||
if (!ids.has(key) || seen.has(key)) return
|
||
if (!surchargeAppliesToRow(rel, r)) {
|
||
skipped++
|
||
return
|
||
}
|
||
seen.add(key)
|
||
list.push(rel)
|
||
})
|
||
r.surcharges = list
|
||
applied += list.length
|
||
})
|
||
const scopeText = scScopeName.value ? `(仅 ${scScopeName.value})` : ''
|
||
if (!scList.value.length) {
|
||
proxy.$modal.msgWarning('附加费配置列表为空,请先到「价格基础 → 附加费配置」维护')
|
||
return
|
||
}
|
||
if (ids.size && !applied) {
|
||
proxy.$modal.msgWarning('已勾选但未生效:这些附加费配置了适用国家/重量段,与当前明细行不匹配')
|
||
return
|
||
}
|
||
const okText = ids.size
|
||
? (skipped > 0
|
||
? `已纳入 ${applied} 项附加费${scopeText};${skipped} 项因适用国家不匹配被跳过`
|
||
: `已纳入 ${ids.size} 项附加费${scopeText}`)
|
||
: `已清空附加费${scopeText}`
|
||
scVisible.value = false
|
||
if (!props.livePersist) {
|
||
proxy.$modal.msgSuccess(okText + '(完成时统一落库)')
|
||
return
|
||
}
|
||
const payload = targets.filter(r => r.psdId).map(r => toDetailPayload(r)).map(p => {
|
||
p.priceSetId = props.priceSetId
|
||
return p
|
||
})
|
||
if (!payload.length) {
|
||
proxy.$modal.msgSuccess(okText)
|
||
return
|
||
}
|
||
proxy.$modal.msgSuccess(okText + ',正在后台写入…')
|
||
api.updateSetDetailSurchargeAsync(payload).then(() => {
|
||
proxy.$modal.msgSuccess(okText + ',已写入数据库')
|
||
}).catch(e => {
|
||
proxy.$modal.msgError((e && e.message) ? e.message : '附加费后台写入失败')
|
||
})
|
||
}
|
||
|
||
async function removeProduct(code) {
|
||
if (props.livePersist) {
|
||
try {
|
||
const rows = await loadProductDetails(code)
|
||
const ids = rows.filter(d => d && d.psdId).map(d => d.psdId)
|
||
if (ids.length) {
|
||
await api.delSetDetail(ids.join(','))
|
||
}
|
||
} catch (e) {
|
||
return
|
||
}
|
||
}
|
||
delete detailByProduct[code]
|
||
if (Array.isArray(props.details)) {
|
||
for (let i = props.details.length - 1; i >= 0; i--) {
|
||
if (props.details[i] && props.details[i].productCode === code) props.details.splice(i, 1)
|
||
}
|
||
}
|
||
if (Array.isArray(props.products)) {
|
||
for (let i = props.products.length - 1; i >= 0; i--) {
|
||
if (props.products[i] && props.products[i].productCode === code) props.products.splice(i, 1)
|
||
}
|
||
}
|
||
proxy.$modal.msgSuccess('已移除产品明细')
|
||
}
|
||
|
||
defineExpose({ pullAllFromLibrary, clearLocalDraft, loadLatestPriorVersionDraft })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
.sub {
|
||
margin-left: 8px;
|
||
color: #909399;
|
||
font-weight: normal;
|
||
font-size: 13px;
|
||
}
|
||
.mb12 { margin-bottom: 12px; }
|
||
.import-progress {
|
||
padding: 8px 4px 0;
|
||
}
|
||
.import-progress-msg {
|
||
margin-top: 12px;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
word-break: break-all;
|
||
}
|
||
.build-choice {
|
||
padding: 8px 0 16px;
|
||
text-align: left;
|
||
}
|
||
.build-choice__tip {
|
||
margin: 0 0 12px;
|
||
color: #606266;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
.build-choice__cards {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 8px;
|
||
}
|
||
.build-card {
|
||
width: 260px;
|
||
min-height: 88px;
|
||
padding: 14px 16px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||
}
|
||
.build-card.selected {
|
||
border-color: #1890ff;
|
||
background: #e6f7ff;
|
||
}
|
||
.build-card:hover:not(:disabled) {
|
||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
|
||
}
|
||
.build-card:disabled {
|
||
opacity: 0.55;
|
||
cursor: not-allowed;
|
||
}
|
||
.build-card--pull:hover:not(:disabled) { border-color: #67c23a; }
|
||
.build-card--import:hover:not(:disabled) { border-color: #409eff; }
|
||
.build-card--import-fresh:hover:not(:disabled) { border-color: #e6a23c; }
|
||
.build-card__title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 10px;
|
||
}
|
||
.build-card--pull .build-card__title { color: #67c23a; }
|
||
.build-card--import .build-card__title { color: #409eff; }
|
||
.build-card--import-fresh .build-card__title { color: #e6a23c; }
|
||
.build-card__desc {
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
color: #909399;
|
||
}
|
||
.card-actions {
|
||
flex: none;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.filters {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 12px;
|
||
}
|
||
.unit {
|
||
margin-left: 8px;
|
||
color: #909399;
|
||
}
|
||
</style>
|