feat(web-admin): driver feedback dialog brand qty entry
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
2a215b2e1f
commit
b7236329c2
5 files changed
+458
-69
No files matched your search
@@ -88,8 +88,16 @@ class PlanProductRead(PlanProductBase):
|
||||
|
||||
class PlanBase(BaseModel):
|
||||
belongs_company: str = Field(..., alias="belongsCompany", description="归属公司 shanxia-安庆山峡 yundong-安徽云栋")
|
||||
plan_type: str = Field(..., alias="planType", description="单据类型 direct-直销单 inbound-入库单 outbound-出库单")
|
||||
follow_status: str = Field(default="draft", alias="followStatus", description="跟进状态")
|
||||
plan_type: str = Field(
|
||||
default="",
|
||||
alias="planType",
|
||||
description="单据类型 direct-直销单 inbound-入库单 outbound-出库单;未选择可为空",
|
||||
)
|
||||
follow_status: str = Field(
|
||||
default="pending_arrange",
|
||||
alias="followStatus",
|
||||
description="跟进状态 wait_notice/pending_arrange/arranged/completed/cancelled",
|
||||
)
|
||||
transport_status: str = Field(default="pending", alias="transportStatus", description="运输安排状态")
|
||||
archive_status: str = Field(default="unarchived", alias="archiveStatus", description="归档状态")
|
||||
complete_status: str = Field(default="no", alias="completeStatus", description="完成状态")
|
||||
@@ -130,7 +138,7 @@ class PlanCreate(PlanBase):
|
||||
create_date: Optional[date] = Field(
|
||||
default=None, alias="createDate", description="创建日期;缺省则服务端用当天"
|
||||
)
|
||||
plan_date: date = Field(..., alias="planDate", description="计划日期")
|
||||
plan_date: Optional[date] = Field(default=None, alias="planDate", description="计划日期(非必填)")
|
||||
products: Optional[list[PlanProductCreate]] = Field(default=None, description="产品明细列表")
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
@@ -199,7 +207,7 @@ class PlanRead(PlanBase, PlanResourceSnapshotMixin):
|
||||
id: str
|
||||
plan_no: str = Field(alias="planNo")
|
||||
create_date: date = Field(alias="createDate")
|
||||
plan_date: date = Field(alias="planDate")
|
||||
plan_date: Optional[date] = Field(default=None, alias="planDate")
|
||||
hazard_cert_ready: int = Field(default=0, alias="hazardCertReady", description="危化品证书是否已人工确认齐备")
|
||||
hazard_cert_ready_by: Optional[str] = Field(default=None, alias="hazardCertReadyBy", description="危化品证书确认人")
|
||||
hazard_cert_ready_time: Optional[datetime] = Field(default=None, alias="hazardCertReadyTime", description="危化品证书确认时间")
|
||||
@@ -256,7 +264,7 @@ class CreateFromContractRequest(BaseModel):
|
||||
create_date: Optional[date] = Field(
|
||||
default=None, alias="createDate", description="创建日期;缺省当天"
|
||||
)
|
||||
plan_date: date = Field(..., alias="planDate", description="计划日期")
|
||||
plan_date: Optional[date] = Field(default=None, alias="planDate", description="计划日期(非必填)")
|
||||
follow_user_id: Optional[str] = Field(default=None, alias="followUserId", description="跟进人ID")
|
||||
follow_user_name: Optional[str] = Field(default=None, max_length=100, alias="followUserName", description="跟进人姓名")
|
||||
model_config = {"populate_by_name": True}
|
||||
@@ -332,11 +340,19 @@ class PlanArchiveBody(BaseModel):
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class PlanDriverFeedbackProductItem(BaseModel):
|
||||
plan_product_id: str = Field(alias="planProductId")
|
||||
actual_qty: Decimal = Field(alias="actualQty", ge=0)
|
||||
brand_qtys: Optional[list[PlanArchiveBrandQtyItem]] = Field(default=None, alias="brandQtys")
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class PlanDriverFeedbackUpdateBody(BaseModel):
|
||||
feedback_qty: Optional[Decimal] = Field(default=None, alias="feedbackQty", ge=0, description="回传数量")
|
||||
feedback_remark: Optional[str] = Field(default=None, alias="feedbackRemark", max_length=500, description="回传备注")
|
||||
feedback_images: Optional[list[str]] = Field(default=None, alias="feedbackImages", description="回传附件URLs")
|
||||
feedback_time: OptionalPlanDateTime = Field(default=None, alias="feedbackTime", description="回传时间")
|
||||
products: Optional[list[PlanDriverFeedbackProductItem]] = None
|
||||
feedback_qty: Optional[Decimal] = Field(default=None, alias="feedbackQty", ge=0, description="回传数量")
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from app.schemas.pl.plan import (
|
||||
PlanTransportCreateBlock,
|
||||
)
|
||||
from app.services.pl_brand_archive import persist_plan_product_brand_qtys, validate_brand_qty_rows
|
||||
from app.services.pl_feedback_brand_qty import persist_transport_product_feedback
|
||||
from app.services.pl_freight_settlement_service import sync_draft_settlement_freight_for_transport
|
||||
from app.services.pl_transport_resource_snapshot_service import (
|
||||
apply_resource_snapshot_json,
|
||||
@@ -966,6 +967,7 @@ def create_transport_for_plan(
|
||||
driver_id=driver_id,
|
||||
)
|
||||
plan.transport_status = "arranged"
|
||||
plan.follow_status = "arranged"
|
||||
plan.freight_amount = freight_amount or 0
|
||||
|
||||
return transport
|
||||
@@ -1316,7 +1318,28 @@ def apply_plan_driver_feedback(
|
||||
if not transport:
|
||||
raise HTTPException(status_code=404, detail="运输单不存在")
|
||||
|
||||
if payload.feedback_qty is not None:
|
||||
if payload.products is not None:
|
||||
products_payload = [
|
||||
{
|
||||
"planProductId": item.plan_product_id,
|
||||
"actualQty": item.actual_qty,
|
||||
"brandQtys": [b.model_dump(by_alias=True) for b in (item.brand_qtys or [])],
|
||||
}
|
||||
for item in payload.products
|
||||
]
|
||||
try:
|
||||
persist_transport_product_feedback(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
transport=transport,
|
||||
plan_id=plan.id,
|
||||
products=products_payload,
|
||||
user_id=user_id,
|
||||
enforce_brand=True,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
elif payload.feedback_qty is not None:
|
||||
transport.feedback_qty = payload.feedback_qty
|
||||
if payload.feedback_remark is not None:
|
||||
transport.feedback_remark = payload.feedback_remark.strip() if payload.feedback_remark else None
|
||||
|
||||
@@ -416,7 +416,7 @@ export interface Plan {
|
||||
id: string
|
||||
planNo: string
|
||||
createDate: string
|
||||
planDate: string
|
||||
planDate?: string | null
|
||||
belongsCompany: string
|
||||
planType: string
|
||||
followStatus: string
|
||||
@@ -509,9 +509,9 @@ export interface PlanTransportCreateBlock {
|
||||
export interface PlanCreateFullPayload {
|
||||
contractId: string
|
||||
createDate: string
|
||||
planDate: string
|
||||
planDate?: string | null
|
||||
belongsCompany: string
|
||||
planType: string
|
||||
planType?: string
|
||||
contractNo?: string
|
||||
supplierId?: string
|
||||
supplierName?: string
|
||||
@@ -591,11 +591,18 @@ export interface PlanArchiveBody {
|
||||
freight?: PlanArchiveFreightBlock
|
||||
}
|
||||
|
||||
export interface PlanDriverFeedbackProductItem {
|
||||
planProductId: string
|
||||
actualQty: number
|
||||
brandQtys?: PlanArchiveBrandQtyItem[]
|
||||
}
|
||||
|
||||
export interface PlanDriverFeedbackUpdateBody {
|
||||
feedbackQty?: number | null
|
||||
feedbackRemark?: string | null
|
||||
feedbackImages?: string[]
|
||||
feedbackTime?: string | null
|
||||
products?: PlanDriverFeedbackProductItem[]
|
||||
}
|
||||
|
||||
export interface PlanSiteFeedbackUpdateBody {
|
||||
@@ -626,6 +633,9 @@ export const planApi = {
|
||||
setHazardCertReady: (id: string, ready: boolean) =>
|
||||
request.patch<Plan>(`/transport/plan/${id}/hazard-cert-ready`, { ready }),
|
||||
cancel: (id: string) => request.post<Plan>(`/transport/plan/${id}/cancel`),
|
||||
waitNotice: (id: string) => request.post<Plan>(`/transport/plan/${id}/wait-notice`),
|
||||
cancelWaitNotice: (id: string) =>
|
||||
request.post<Plan>(`/transport/plan/${id}/cancel-wait-notice`),
|
||||
getArrangeInfo: (id: string) => request.get<PlanArrangeInfo>(`/transport/plan/${id}/arrange-info`),
|
||||
archive: (id: string, data?: PlanArchiveBody) =>
|
||||
request.post<Plan>(`/transport/plan/${id}/archive`, data),
|
||||
@@ -636,7 +646,7 @@ export const planApi = {
|
||||
delete: (id: string) => request.delete(`/transport/plan/${id}`),
|
||||
createFromContract: (data: {
|
||||
contractId: string
|
||||
planDate: string
|
||||
planDate?: string | null
|
||||
createDate?: string
|
||||
followUserId?: string
|
||||
followUserName?: string
|
||||
@@ -776,6 +786,7 @@ export interface PlIoOrderItem {
|
||||
productNo?: string
|
||||
productName?: string
|
||||
brandSummary?: string
|
||||
brandName?: string
|
||||
specification?: string
|
||||
unit?: string
|
||||
unitLabel?: string
|
||||
@@ -783,6 +794,7 @@ export interface PlIoOrderItem {
|
||||
actualQty?: number
|
||||
qtySource?: string
|
||||
packageSpec?: string
|
||||
deliveryMethod?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
:title="dialogTitle"
|
||||
width="640px"
|
||||
width="720px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -14,50 +14,90 @@
|
||||
@closed="handleClosed"
|
||||
@opened="handleOpened"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-position="top"
|
||||
class="form-grid-2"
|
||||
>
|
||||
<el-form-item label="回传时间">
|
||||
<el-date-picker
|
||||
v-model="formData.feedbackTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="不填则保存为当前时间"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="回传数量">
|
||||
<el-input-number
|
||||
v-model="formData.feedbackQty"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
placeholder="回传数量"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="回传备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="formData.feedbackRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="回传备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="回传附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
v-model="formData.feedbackImages"
|
||||
folder="pl/transport/feedback"
|
||||
accept="image/*,video/*,.pdf"
|
||||
:limit="15"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-loading="loading" class="pl-driver-feedback-form">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-position="top"
|
||||
class="form-grid-2"
|
||||
>
|
||||
<el-form-item label="回传时间">
|
||||
<el-date-picker
|
||||
v-model="formData.feedbackTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="不填则保存为当前时间"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="回传备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="formData.feedbackRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="回传备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="回传附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
v-model="formData.feedbackImages"
|
||||
folder="pl/transport/feedback"
|
||||
accept="image/*,video/*,.pdf"
|
||||
:limit="15"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<section class="pl-driver-feedback-products">
|
||||
<h4 class="pl-driver-feedback-products__title">产品回传数量</h4>
|
||||
<p v-if="!productRows.length && !loading" class="pl-driver-feedback-products__empty">
|
||||
暂无产品明细
|
||||
</p>
|
||||
<div
|
||||
v-for="(row, idx) in productRows"
|
||||
:key="row.planProductId"
|
||||
class="pl-driver-feedback-product"
|
||||
>
|
||||
<div class="pl-driver-feedback-product__head">
|
||||
<span class="pl-driver-feedback-product__name">{{ row.productName || '产品' }}</span>
|
||||
<span class="pl-driver-feedback-product__badge">
|
||||
产品 {{ idx + 1 }}/{{ productRows.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pl-driver-feedback-product__actual">
|
||||
<span class="pl-driver-feedback-product__actual-label">实际数量</span>
|
||||
<el-input-number
|
||||
v-model="row.actualQty"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
:controls="false"
|
||||
:disabled="Boolean(row.brandMode)"
|
||||
class="pl-driver-feedback-product__actual-input"
|
||||
@change="() => onActualQtyChange(row)"
|
||||
/>
|
||||
<span class="pl-driver-feedback-product__unit">{{ row.unitLabel || row.unit || '吨' }}</span>
|
||||
</div>
|
||||
<BrandQtyChipsField
|
||||
v-if="row.brandMode"
|
||||
v-model="row.brandQtyRows"
|
||||
:brand-mode="brandModeForField(row)"
|
||||
:required-brand-id="row.brandId"
|
||||
:brand-options="row.brandOptions"
|
||||
:unit="row.unitLabel || row.unit || '吨'"
|
||||
variant="admin"
|
||||
:actual-qty="Number(row.actualQty ?? 0)"
|
||||
@update:actual-qty="(val) => syncRowActualQty(row, val)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="productRows.length > 1" class="pl-driver-feedback-total">
|
||||
<span>合计回传</span>
|
||||
<strong>{{ totalFeedbackQty }}</strong>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -75,18 +115,43 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { fetchProductDetail, type ProductDetail } from '@/api/crm/product'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { planApi, type PlanDriverFeedbackUpdateBody } from '@/api/pl'
|
||||
import BrandQtyChipsField, {
|
||||
type BrandOption,
|
||||
type BrandQtyRow
|
||||
} from '@/components/business/brand-qty-chips-field/index.vue'
|
||||
import {
|
||||
planApi,
|
||||
type Plan,
|
||||
type PlanDriverFeedbackUpdateBody,
|
||||
type PlanProduct
|
||||
} from '@/api/pl'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { parsePlFeedbackImages } from '@/utils/pl-display'
|
||||
|
||||
interface DriverFeedbackProductRow {
|
||||
planProductId: string
|
||||
productName: string
|
||||
unit: string
|
||||
unitLabel: string
|
||||
brandMode?: string
|
||||
brandId?: string
|
||||
brandName?: string
|
||||
brandOptions: BrandOption[]
|
||||
actualQty: number
|
||||
brandQtyRows: BrandQtyRow[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
planId: string
|
||||
isEdit?: boolean
|
||||
feedbackQty?: number | null
|
||||
feedbackRemark?: string | null
|
||||
feedbackImages?: string | string[] | null
|
||||
feedbackTime?: string | null
|
||||
feedbackQty?: number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -94,54 +159,236 @@ const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: value => emit('update:visible', value)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const productRows = ref<DriverFeedbackProductRow[]>([])
|
||||
|
||||
const formData = reactive({
|
||||
feedbackTime: '' as string,
|
||||
feedbackQty: undefined as number | undefined,
|
||||
feedbackRemark: '',
|
||||
feedbackImages: ''
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() => (props.isEdit ? '编辑司机回传信息' : '补充司机回传信息'))
|
||||
|
||||
function resetForm() {
|
||||
const totalFeedbackQty = computed(() =>
|
||||
Number(
|
||||
productRows.value.reduce((sum, row) => sum + Number(row.actualQty ?? 0), 0).toFixed(3)
|
||||
)
|
||||
)
|
||||
|
||||
function brandModeForField(row: DriverFeedbackProductRow): 'any' | 'required' | null {
|
||||
if (row.brandMode === 'any' || row.brandMode === 'required') return row.brandMode
|
||||
return null
|
||||
}
|
||||
|
||||
function syncRowActualQty(row: DriverFeedbackProductRow, val: number) {
|
||||
row.actualQty = Number(val)
|
||||
}
|
||||
|
||||
function onActualQtyChange(row: DriverFeedbackProductRow) {
|
||||
if (row.brandMode === 'required' && row.brandQtyRows.length) {
|
||||
row.brandQtyRows[0].qty = Number(row.actualQty ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
function brandQtySum(rows: BrandQtyRow[]) {
|
||||
return rows.reduce((sum, row) => sum + Number(row.qty || 0), 0)
|
||||
}
|
||||
|
||||
function resolveUnitLabel(unit?: string) {
|
||||
const code = (unit || '').trim()
|
||||
return dictStore.getLabel('crm_product_unit', code) || code || '吨'
|
||||
}
|
||||
|
||||
function mapBrandQtyRows(
|
||||
prod: PlanProduct,
|
||||
brandOptions: BrandOption[],
|
||||
seedActualQty: number
|
||||
): BrandQtyRow[] {
|
||||
if (prod.brandMode === 'required') {
|
||||
const brandId = prod.brandId || brandOptions[0]?.brandId || ''
|
||||
const brandName =
|
||||
prod.brandName ||
|
||||
brandOptions.find(o => o.brandId === brandId)?.brandName ||
|
||||
'指定品牌'
|
||||
return brandId ? [{ brandId, brandName, qty: seedActualQty }] : []
|
||||
}
|
||||
if (prod.brandMode === 'any') {
|
||||
const rows = brandOptions.map(opt => ({
|
||||
brandId: opt.brandId,
|
||||
brandName: opt.brandName,
|
||||
qty: 0
|
||||
}))
|
||||
if (rows.length === 1) {
|
||||
rows[0].qty = seedActualQty
|
||||
}
|
||||
return rows
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
async function loadBrandOptions(productId: string): Promise<BrandOption[]> {
|
||||
if (!productId) return []
|
||||
try {
|
||||
const res = await fetchProductDetail({ id: productId })
|
||||
const detail = pickBxData<ProductDetail>(res)
|
||||
return (detail?.brands || [])
|
||||
.filter(b => (b.status || 'on') === 'on' && b.id && b.brandName)
|
||||
.map(b => ({
|
||||
brandId: String(b.id),
|
||||
brandName: String(b.brandName)
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSeedActualQty(
|
||||
prod: PlanProduct,
|
||||
transportFeedbackQty: number | null | undefined,
|
||||
productCount: number
|
||||
) {
|
||||
if (prod.actualQty != null) return Number(prod.actualQty)
|
||||
if (productCount === 1 && transportFeedbackQty != null) {
|
||||
return Number(transportFeedbackQty)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
async function buildProductRows(plan: Plan, transportFeedbackQty?: number | null) {
|
||||
const products = Array.isArray(plan.products) ? plan.products : []
|
||||
const brandOptionMap = new Map<string, BrandOption[]>()
|
||||
|
||||
await Promise.all(
|
||||
[...new Set(products.filter(p => p.brandMode === 'any' && p.productId).map(p => p.productId))].map(
|
||||
async productId => {
|
||||
brandOptionMap.set(productId, await loadBrandOptions(productId))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
productRows.value = products.map(prod => {
|
||||
const brandOptions =
|
||||
prod.brandMode === 'any'
|
||||
? brandOptionMap.get(prod.productId) || []
|
||||
: prod.brandId
|
||||
? [{ brandId: prod.brandId, brandName: prod.brandName || '指定品牌' }]
|
||||
: []
|
||||
const seedActualQty = resolveSeedActualQty(prod, transportFeedbackQty, products.length)
|
||||
const brandQtyRows = mapBrandQtyRows(prod, brandOptions, seedActualQty)
|
||||
const actualQty = prod.brandMode
|
||||
? Number(brandQtySum(brandQtyRows).toFixed(3))
|
||||
: seedActualQty
|
||||
|
||||
return {
|
||||
planProductId: prod.id,
|
||||
productName: prod.productName || '',
|
||||
unit: prod.unit || '吨',
|
||||
unitLabel: resolveUnitLabel(prod.unit),
|
||||
brandMode: prod.brandMode,
|
||||
brandId: prod.brandId,
|
||||
brandName: prod.brandName,
|
||||
brandOptions,
|
||||
actualQty,
|
||||
brandQtyRows
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resetMetaForm() {
|
||||
formData.feedbackTime = props.feedbackTime ? String(props.feedbackTime) : ''
|
||||
formData.feedbackQty =
|
||||
props.feedbackQty != null && props.feedbackQty !== undefined
|
||||
? Number(props.feedbackQty)
|
||||
: undefined
|
||||
formData.feedbackRemark = props.feedbackRemark ? String(props.feedbackRemark) : ''
|
||||
const images = parsePlFeedbackImages(props.feedbackImages)
|
||||
formData.feedbackImages = images.length ? images.join(',') : ''
|
||||
}
|
||||
|
||||
function handleOpened() {
|
||||
resetForm()
|
||||
async function handleOpened() {
|
||||
if (!props.planId) return
|
||||
loading.value = true
|
||||
productRows.value = []
|
||||
resetMetaForm()
|
||||
try {
|
||||
const res = await planApi.get(props.planId)
|
||||
const plan = pickBxData<Plan>(res)
|
||||
if (!plan?.id) {
|
||||
throw new Error('运输计划数据无效')
|
||||
}
|
||||
await buildProductRows(plan, props.feedbackQty)
|
||||
} catch (e: unknown) {
|
||||
innerVisible.value = false
|
||||
const message = e instanceof Error ? e.message : '加载失败'
|
||||
ElMessage.error(message.includes('加载') ? message : `加载运输计划失败:${message}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
formRef.value?.resetFields()
|
||||
productRows.value = []
|
||||
}
|
||||
|
||||
function validateProductRows(): string | null {
|
||||
if (!productRows.value.length) {
|
||||
return '暂无产品明细,无法保存'
|
||||
}
|
||||
for (const row of productRows.value) {
|
||||
if (row.actualQty == null || Number(row.actualQty) < 0) {
|
||||
return `「${row.productName || '产品'}」请填写有效的实际数量`
|
||||
}
|
||||
if (!row.brandMode) continue
|
||||
const positiveRows = (row.brandQtyRows || []).filter(r => Number(r.qty) > 0)
|
||||
if (!positiveRows.length) {
|
||||
return `「${row.productName || '产品'}」请至少填写一个品牌数量`
|
||||
}
|
||||
const total = Number(brandQtySum(row.brandQtyRows).toFixed(3))
|
||||
const actual = Number(row.actualQty ?? 0)
|
||||
if (Math.abs(total - actual) >= 0.0005) {
|
||||
return `「${row.productName || '产品'}」品牌数量合计须等于实际数量`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!props.planId) return
|
||||
const validationMessage = validateProductRows()
|
||||
if (validationMessage) {
|
||||
ElMessage.warning(validationMessage)
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const imageList = formData.feedbackImages
|
||||
? formData.feedbackImages.split(',').map(item => item.trim()).filter(Boolean)
|
||||
: []
|
||||
const body: PlanDriverFeedbackUpdateBody = {
|
||||
feedbackQty: formData.feedbackQty ?? null,
|
||||
feedbackRemark: formData.feedbackRemark.trim() || null,
|
||||
feedbackImages: imageList,
|
||||
feedbackTime: formData.feedbackTime || null
|
||||
feedbackTime: formData.feedbackTime || null,
|
||||
products: productRows.value.map(row => ({
|
||||
planProductId: row.planProductId,
|
||||
actualQty: Number(row.actualQty ?? 0),
|
||||
brandQtys: row.brandMode
|
||||
? (row.brandQtyRows || [])
|
||||
.filter(r => Number(r.qty) > 0)
|
||||
.map(r => ({
|
||||
brandId: r.brandId,
|
||||
brandName: r.brandName,
|
||||
qty: Number(r.qty)
|
||||
}))
|
||||
: undefined
|
||||
}))
|
||||
}
|
||||
await planApi.updateDriverFeedback(props.planId, body)
|
||||
ElMessage.success('司机回传信息已保存')
|
||||
@@ -162,4 +409,95 @@ async function handleSubmit() {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-products__title {
|
||||
margin: 0 0 10px;
|
||||
padding-left: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--section-bar-title-color, var(--primary-dark, #059669));
|
||||
border-left: 3px solid var(--section-bar-border-color, var(--primary, #10b981));
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-products__empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product {
|
||||
padding: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
|
||||
& + & {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product__badge {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product__actual {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product__actual-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product__actual-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-product__unit {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-driver-feedback-total {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
font-size: 13px;
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -470,10 +470,10 @@
|
||||
v-model:visible="driverFeedbackDialogVisible"
|
||||
:plan-id="planId"
|
||||
:is-edit="hasDriverFeedback"
|
||||
:feedback-qty="transport?.feedbackQty"
|
||||
:feedback-remark="transport?.feedbackRemark"
|
||||
:feedback-images="transport?.feedbackImages"
|
||||
:feedback-time="transport?.feedbackTime"
|
||||
:feedback-qty="transport?.feedbackQty"
|
||||
@success="handleFeedbackSaved"
|
||||
/>
|
||||
<PlPlanSiteFeedbackDialog
|
||||
|
||||
Reference in new issue
Block a user