2026-08-03 11:44:50 +08:00
< script setup lang = "ts" >
2026-10-10 02:19:39 +08:00
import { computed , onBeforeUnmount , reactive , ref , shallowRef , watch } from 'vue'
import { useRoute , useRouter } from 'vue-router'
2026-10-30 11:20:03 +08:00
import { ElMessage , type FormInstance , type FormRules } from 'element-plus'
2026-10-10 02:19:39 +08:00
import { ApiError } from '@/api/types'
import { accessTokenIdentity } from '@/api/session'
import { useAuthStore } from '@/modules/auth/public'
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/components/common/QueryToolbar.vue'
import type { TableAction } from '@/components/common/tableActions'
import ProcurementDetailPanel from '../components/ProcurementDetailPanel.vue'
2026-10-30 11:20:03 +08:00
import ProcurementDecisionDialog from '../components/ProcurementDecisionDialog.vue'
import { createProcurement , getProcurement , getProcurementWorkflow , listProcurements , type ProcurementDetail , type ProcurementSummary , type ProcurementWorkflow } from '../api/procurement'
2026-10-29 11:39:01 +08:00
import { procurementDirections , directionLabel , regionText , buildProcurementCreate , dateTime , draftBudgetTotal , emptyProcurementDraft , money , procurementStatuses , statusLabel , statusTone } from '../procurementPresentation'
2026-10-30 17:59:48 +08:00
import OrderFundingStatus from '../components/OrderFundingStatus.vue'
2026-10-22 23:06:02 +08:00
import { procurementRequestKey } from '../procurementSubmission'
2026-10-10 02:19:39 +08:00
const props = withDefaults ( defineProps < { createPage ?: boolean ; detailPage ?: boolean } > (), { createPage : false , detailPage : false })
const auth = useAuthStore (), route = useRoute (), router = useRouter ()
const mode = computed (() => props . detailPage ? 'detail' : props . createPage ? 'create' : 'list' )
const orderNo = computed (() => typeof route . params . orderNo === 'string' ? route . params . orderNo : '' )
const canCreate = computed (() => auth . hasPermission ( 'merchant:cross-border:procurement:create' ))
const canList = computed (() => auth . hasPermission ( 'merchant:cross-border:procurement:list' ))
const identity = computed (() => auth . token ? accessTokenIdentity ( auth . token ) : null )
const companyName = computed (() => auth . profile . tenantName || ( auth . user ? . tenantName && ! /^企业租户 \d+$/ . test ( auth . user . tenantName ) ? auth . user . tenantName : '' ))
const form = reactive ( emptyProcurementDraft ())
2026-10-29 14:14:56 +08:00
const formRef = ref < FormInstance >()
const formRules : FormRules < typeof form > = {
procurementDirection : [{ required : true , message : '请选择采购方向' , trigger : 'change' }],
productName : [{ required : true , whitespace : true , message : '请输入商品名称' , trigger : 'blur' }],
2026-10-30 11:20:03 +08:00
productUrl : [{ required : true , whitespace : true , message : '请输入商品链接' , trigger : 'blur' }],
2026-10-29 14:14:56 +08:00
purchaseQuantity : [{ required : true , whitespace : true , message : '请输入采购数量' , trigger : 'blur' }],
budgetUnitPrice : [{ required : true , whitespace : true , message : '请输入预算单价' , trigger : 'blur' }],
currency : [{ required : true , message : '请选择预算币种' , trigger : 'change' }],
contactName : [{ required : true , whitespace : true , message : '请输入收件人姓名' , trigger : 'blur' }],
contactPhone : [{ required : true , whitespace : true , message : '请输入联系电话' , trigger : 'blur' }],
receivingAddress : [{ required : true , whitespace : true , message : '请输入完整收货地址' , trigger : 'blur' }],
}
2026-10-29 11:39:01 +08:00
const summaryQuantity = computed (() => /^ \ d + $ / . test ( form . purchaseQuantity ) && Number . isSafeInteger ( Number ( form . purchaseQuantity )) && Number ( form . purchaseQuantity ) > 0 ? String ( Number ( form . purchaseQuantity )) : '—' )
const summaryUnitPrice = computed (() => draftBudgetTotal ({ ... form , purchaseQuantity : '1' }) ?? '—' )
const summaryCurrencyName = computed (() => form . currency === 'USD' ? '美元' : '人民币' )
const budgetTotal = computed (() => summaryQuantity . value === '—' ? null : draftBudgetTotal ( form ))
2026-10-22 23:06:02 +08:00
const submitError = ref ( '' ), submittedOrder = ref ( '' )
2026-10-10 02:19:39 +08:00
const submitting = ref ( false ), loading = ref ( false ), error = ref ( '' )
const detail = shallowRef < ProcurementDetail | null >( null )
2026-10-27 17:39:28 +08:00
const workflow = shallowRef < ProcurementWorkflow | null >( null )
2026-10-30 11:20:03 +08:00
const decisionTarget = shallowRef < ProcurementSummary | null >( null )
const decision = ref < 'confirm' | 'reject' > ( 'confirm' )
2026-10-10 02:19:39 +08:00
const rows = ref < ProcurementSummary [] >([]), total = ref ( 0 )
const query = reactive ({ page : 1 , size : 20 , orderNo : '' , purchaseCategory : '' , productName : '' , status : '' })
let requestSequence = 0 , active = true , inFlight = ''
2026-10-27 17:39:28 +08:00
let createAttempt : { fingerprint : string ; key : string } | null = null
2026-10-10 02:19:39 +08:00
const queryModel = computed < QueryModel >({
get : () => ({ orderNo : query . orderNo , purchaseCategory : query . purchaseCategory , productName : query . productName , status : query . status }),
set : value => { for ( const key of [ 'orderNo' , 'purchaseCategory' , 'productName' , 'status' ] as const ) query [ key ] = String ( value [ key ] || '' ) },
})
const queryFields : QueryField [] = [
{ key : 'orderNo' , label : '采购单号' , type : 'input' }, { key : 'purchaseCategory' , label : '采购品类' , type : 'input' },
{ key : 'productName' , label : '商品名称' , type : 'input' },
2026-10-27 17:39:28 +08:00
{ key : 'status' , label : '履约进度' , type : 'select' , options : Object . entries ( procurementStatuses ). map (([ value , label ]) => ({ value , label })) },
2026-10-10 02:19:39 +08:00
]
const columns : DataTableColumn [] = [
2026-10-30 11:20:03 +08:00
{ key : 'actions' , label : '操作' , width : 260 , fixed : 'left' , slotName : 'actions' },
2026-10-26 13:43:20 +08:00
{ prop : 'procurementDirection' , label : '采购方向' , minWidth : 170 , slotName : 'direction' },
2026-10-10 02:19:39 +08:00
{ prop : 'orderNo' , label : '采购单号' , minWidth : 250 , slotName : 'orderNo' },
{ prop : 'purchaseCategory' , label : '采购品类' , width : 120 }, { prop : 'productName' , label : '商品名称' , minWidth : 200 },
{ prop : 'productSpecification' , label : '商品规格' , minWidth : 160 }, { prop : 'purchaseQuantity' , label : '采购数量(件)' , width : 135 },
{ prop : 'budgetUnitPrice' , label : '预算单价' , width : 155 , slotName : 'budget' },
2026-10-30 17:59:48 +08:00
{ prop : 'status' , label : '履约进度' , width : 180 , slotName : 'status' }, { prop : 'reviewRemark' , label : '失败 / 拒绝原因' , minWidth : 220 , slotName : 'reviewRemark' }, { prop : 'createdAt' , label : '下单时间' , width : 185 , slotName : 'createdAt' },
2026-10-10 02:19:39 +08:00
]
2026-10-30 11:20:03 +08:00
function rowActions ( row : object ) : TableAction [] {
const order = row as ProcurementSummary
const actions : TableAction [] = [{ key : 'detail' , label : '详情' , run : () => openDetail ( order ) }]
if ( canCreate . value && order . status === 'WAIT_CUSTOMER_CONFIRM' ) {
actions . push ({ key : 'reject' , label : '驳回' , run : () => beginDecision ( order , 'reject' ) }, { key : 'confirm' , label : '确认采购结果' , run : () => beginDecision ( order , 'confirm' ) })
}
return actions
}
function beginDecision ( order : ProcurementSummary , kind : 'confirm' | 'reject' ) {
if ( ! canCreate . value || order . status !== 'WAIT_CUSTOMER_CONFIRM' ) return
decision . value = kind ; decisionTarget . value = order
}
function decisionChanged () { decisionTarget . value = null ; void loadList () }
2026-10-10 02:19:39 +08:00
function openDetail ( row : ProcurementSummary ) { if ( row . orderNo ) void router . push ({ name : 'cross-border-procurement-detail' , params : { orderNo : row . orderNo } }) }
function returnToList () { if ( canList . value ) void router . push ({ name : 'cross-border-procurement' }) }
function message ( cause : unknown ) {
if ( cause instanceof ApiError && (( cause . status || 0 ) >= 500 || cause . code . startsWith ( 'B' ))) return '服务暂不可用,请稍后重试'
return cause instanceof Error ? cause . message : '请求失败,请稍后重试'
}
async function loadList () {
const params = { page : query . page , size : query . size , orderNo : query . orderNo || undefined , productName : query . productName || undefined , purchaseCategory : query . purchaseCategory || undefined , status : query . status || undefined }
const key = JSON . stringify ([ identity . value , params ])
if ( inFlight === key ) return
const sequence = ++ requestSequence ; inFlight = key ; loading . value = true ; error . value = ''
try { const result = await listProcurements ( params ); if ( sequence === requestSequence ) { rows . value = result . records ; total . value = result . total } }
catch ( cause ) { if ( sequence === requestSequence ) { rows . value = []; total . value = 0 ; error . value = message ( cause ) } }
finally { if ( sequence === requestSequence ) { loading . value = false ; inFlight = '' } }
}
async function loadDetail () {
if ( ! orderNo . value ) { error . value = '采购单号缺失' ; return }
const key = JSON . stringify ([ identity . value , orderNo . value ])
if ( inFlight === key ) return
2026-10-27 17:39:28 +08:00
const sequence = ++ requestSequence ; inFlight = key ; loading . value = true ; error . value = '' ; detail . value = null ; workflow . value = null
try {
const [ result , currentWorkflow ] = await Promise . all ([ getProcurement ( orderNo . value ), getProcurementWorkflow ( orderNo . value )])
if ( sequence === requestSequence ) { detail . value = result ; workflow . value = currentWorkflow }
}
2026-10-10 02:19:39 +08:00
catch ( cause ) { if ( sequence === requestSequence ) error . value = message ( cause ) }
finally { if ( sequence === requestSequence ) { loading . value = false ; inFlight = '' } }
}
function search () { if ( query . page === 1 ) void loadList (); else query . page = 1 }
function resetQuery () { const reload = query . page === 1 ; Object . assign ( query , { page : 1 , orderNo : '' , purchaseCategory : '' , productName : '' , status : '' }); if ( reload ) void loadList () }
2026-10-29 14:14:56 +08:00
function resetForm () { submitError . value = '' ; submittedOrder . value = '' ; Object . assign ( form , emptyProcurementDraft ()); formRef . value ? . clearValidate (); createAttempt = null }
2026-10-10 02:19:39 +08:00
async function submit () {
2026-10-22 23:06:02 +08:00
if ( submitting . value || ! canCreate . value || ! identity . value || submittedOrder . value ) return
const owner = identity . value ; submitting . value = true ; submitError . value = ''
2026-10-10 02:19:39 +08:00
try {
2026-10-29 14:14:56 +08:00
if ( ! formRef . value || ! await formRef . value . validate (). catch (() => false )) return
if ( ! active || owner !== identity . value ) return
2026-10-27 17:39:28 +08:00
const payload = buildProcurementCreate ( form , companyName . value , '' )
const fingerprint = JSON . stringify ([ owner , payload ])
if ( createAttempt ? . fingerprint !== fingerprint ) createAttempt = { fingerprint , key : procurementRequestKey () }
2026-10-29 13:37:28 +08:00
if ( ! active || owner !== identity . value ) return
2026-10-27 17:39:28 +08:00
const result = await createProcurement ({ ... payload , idempotencyKey : createAttempt . key })
2026-10-10 02:19:39 +08:00
if ( ! active || owner !== identity . value ) return
2026-10-30 17:59:48 +08:00
createAttempt = null ; submittedOrder . value = result . order . orderNo
ElMessage . success ( result . order . status === 'FUNDING_PENDING' ? '采购订单已受理,资金校验完成后进入平台审核' : '采购订单已提交' )
2026-10-30 11:20:03 +08:00
if ( canList . value ) await router . push ({ name : 'cross-border-procurement' })
2026-10-30 17:59:48 +08:00
} catch ( cause ) {
if ( /已释放|已过期/ . test ( message ( cause ))) createAttempt = null
if ( active && owner === identity . value ) submitError . value = message ( cause )
}
2026-10-22 23:06:02 +08:00
finally { submitting . value = false }
2026-10-10 02:19:39 +08:00
}
watch (() => [ mode . value , orderNo . value , query . page , query . size , identity . value ], ( values , previous ) => {
2026-10-27 17:39:28 +08:00
if ( values [ 4 ] !== previous ? .[ 4 ]) { rows . value = []; total . value = 0 ; createAttempt = null }
2026-10-30 11:20:03 +08:00
decisionTarget . value = null
++ requestSequence ; inFlight = '' ; loading . value = false ; error . value = '' ; detail . value = null ; workflow . value = null
2026-10-22 23:06:02 +08:00
if ( mode . value === 'create' ) resetForm ()
2026-10-10 02:19:39 +08:00
else if ( mode . value === 'detail' ) void loadDetail ()
else void loadList ()
}, { immediate : true })
onBeforeUnmount (() => { active = false ; ++ requestSequence })
2026-08-03 11:44:50 +08:00
</ script >
2026-10-10 02:19:39 +08:00
< template >
2026-10-29 11:39:01 +08:00
< section class = "procurement-page client-ui" : class = "{ 'procurement-page--create': mode === 'create' }" : aria -busy =" loading || submitting " >
2026-10-10 02:19:39 +08:00
< template v-if = "mode === 'list'" >
< QueryToolbar v-model = "queryModel" layout="grid" ariaLabel="采购订单查询条件" :fields="queryFields" :search-on-reset="false" @search="search" @reset="resetQuery" />
< div class = "procurement-table-card" >
< DataTable v -model:page = "query.page" v -model:page-size = "query.size" :data = "rows" :columns = "columns" :row-actions = "rowActions" :loading = "loading" :total = "total" : page -sizes ="[ 10 , 20 , 50 , 100 ]" row -key =" orderNo " empty -text =" 暂无采购订单 " >
< template # toolbar -actions >< el-button v-if = "canCreate" type="primary" @click="router.push({ name: 'cross-border-procurement-create' })" > 新增下单 </ el -button ></ template >
< template v-if = "error" #error><div class="procurement-error" role="alert"><span>{{ error }}</span><el-button :disabled="loading" @click="loadList" > 重新加载 </ el -button ></ div ></ template >
< template # orderNo = "{ row }" >< button class = "procurement-link" type = "button" @click ="openDetail(row as ProcurementSummary)" >{{ ( row as ProcurementSummary ) .orderNo || '—' }}</ button ></ template >
< template # budget = "{ row }" >{{ money (( row as ProcurementSummary ). budgetUnitPrice , ( row as ProcurementSummary ). currency ) }}</ template >
2026-10-26 13:43:20 +08:00
< template # direction = "{ row }" >{{ directionLabel (( row as ProcurementSummary ). procurementDirection ) }}</ template >
< template # status = "{ row }" >< el-tag : type = "statusTone((row as ProcurementSummary).status)" size = "small" >{{ statusLabel (( row as ProcurementSummary ). status , ( row as ProcurementSummary ). procurementDirection ) }}</ el-tag ></ template >
2026-10-30 17:59:48 +08:00
< template # reviewRemark = "{ row }" >< OrderFundingStatus v-if = "(row as ProcurementSummary).status === 'FUNDING_FAILED'" kind="procurement" :order-no="(row as ProcurementSummary).orderNo" :status="(row as ProcurementSummary).status" :can-retry="false" compact /><span v-else-if="(row as ProcurementSummary).status === 'REJECTED'" >{{ ( row as ProcurementSummary ) .reviewRemark || ' 未填写原因 ' }}</ span >< span v-else > — </ span ></ template >
2026-10-10 02:19:39 +08:00
< template # createdAt = "{ row }" >{{ dateTime (( row as ProcurementSummary ). createdAt ) }}</ template >
</ DataTable >
</ div >
</ template >
< section v-else-if = "mode === 'detail'" v-loading="loading" class="procurement-detail" aria-label="采购订单详情" >
2026-10-30 11:20:03 +08:00
< header class = "procurement-heading" >< div >< el-button link type = "primary" @click ="returnToList" > ← 返回列表 </ el -button >< h2 >{{ detail ? . order . orderNo || '采购订单详情' }}</ h2 ></ div >< div >< el-tag v-if = "detail" :type="statusTone(workflow?.status || detail.order.status)">{{ statusLabel(workflow?.status || detail.order.status, detail.order.procurementDirection) }}</el-tag><el-button :disabled="loading" @click="loadDetail" > 刷新 </ el -button ></ div ></ header >
2026-10-30 17:59:48 +08:00
< OrderFundingStatus v-if = "detail" kind="procurement" :order-no="detail.order.orderNo" :status="detail.order.status" :can-retry="canCreate" @changed="loadDetail" />
2026-10-10 02:19:39 +08:00
< div v-if = "error" class="procurement-error" role="alert"><span>{{ error }}</span><el-button :disabled="loading" @click="loadDetail" > 重新加载 </ el -button ></ div >
2026-10-30 11:20:03 +08:00
< template v-else-if = "detail && workflow"><el-alert v-if="workflow.rejectionReason" title="采购结果驳回原因" :description="workflow.rejectionReason" type="warning" :closable="false" /><ProcurementDetailPanel :detail="detail" :workflow="workflow" /></template><el-skeleton v-else :rows="8" animated />
2026-10-10 02:19:39 +08:00
</ section >
< section v-else class = "procurement-create" aria -label =" 采购下单 " >
2026-10-29 11:39:01 +08:00
< header class = "procurement-create-heading" >
2026-10-29 12:03:31 +08:00
< div >< h1 > 海外采购下单 </ h1 ></ div >
2026-10-29 11:39:01 +08:00
< el-button v-if = "canList" :disabled="submitting" @click="returnToList" > 返回列表 </ el -button >
</ header >
2026-10-10 02:19:39 +08:00
< el-result v-if = "!canCreate" icon="warning" title="没有采购下单权限" sub-title="请联系企业管理员配置权限" />
2026-10-22 23:06:02 +08:00
< el-result v-else-if = "submittedOrder" icon="success" title="采购订单已提交" :sub-title="submittedOrder"><template #extra><el-button @click="resetForm" > 新建另一单 </ el -button ></ template ></ el-result >
2026-10-10 02:19:39 +08:00
< template v-else >
< el-alert v-if = "submitError" class="submit-error" :title="submitError" type="error" :closable="false" />
2026-10-29 11:39:01 +08:00
< section class = "procurement-summary" aria -label =" 采购预算概览 " >
< article class = "procurement-summary-card" >
< div class = "procurement-summary-title" >
< span class = "procurement-summary-icon procurement-summary-icon--quantity" aria -hidden =" true " >< svg viewBox = "0 0 24 24" >< path d = "M12 3l8 4.5v9L12 21l-8-4.5v-9z" />< path d = "M4 7.5l8 4.5 8-4.5M12 12v9" /></ svg ></ span >
< h2 > 采购数量 </ h2 >
</ div >
< div class = "procurement-summary-value" >{{ summaryQuantity }}< small > 件 </ small ></ div >
</ article >
< article class = "procurement-summary-card" >
< div class = "procurement-summary-title" >
< span class = "procurement-summary-icon procurement-summary-icon--price" aria -hidden =" true " >< svg viewBox = "0 0 24 24" >< path d = "M3 12h4l3-8 4 16 3-8h4" /></ svg ></ span >
< h2 > 预算单价 </ h2 >
</ div >
< div class = "procurement-summary-value" >{{ summaryUnitPrice }}</ div >
</ article >
< article class = "procurement-summary-card" >
< div class = "procurement-summary-title" >
< span class = "procurement-summary-icon procurement-summary-icon--currency" aria -hidden =" true " >< svg viewBox = "0 0 24 24" >< circle cx = "12" cy = "12" r = "9" />< path d = "M12 7v10M9 9.5c0-1 1.3-1.7 3-1.7s3 .7 3 1.7-1.3 1.5-3 2-3 1-3 2.2 1.3 1.8 3 1.8 3-.8 3-1.8" /></ svg ></ span >
< h2 > 预算币种 </ h2 >
</ div >
< div class = "procurement-summary-value" >{{ form . currency }}< small >{{ summaryCurrencyName }}</ small ></ div >
</ article >
</ section >
2026-10-29 14:14:56 +08:00
< el-form ref = "formRef" :model = "form" :rules = "formRules" label -position =" top " :disabled = "submitting" scroll -to-error @submit.prevent ="submit" >
2026-10-29 11:39:01 +08:00
< section class = "procurement-form-section" aria -labelledby =" procurement -product-heading " >
< h3 id = "procurement-product-heading" >< span class = "procurement-section-dot" aria -hidden =" true " /> 商品与预算 </ h3 >
< div class = "procurement-form-card" >
2026-10-29 14:14:56 +08:00
< el-form-item class = "procurement-direction" label = "采购方向" for = "procurement-direction" prop = "procurementDirection" required >< el-select id = "procurement-direction" v-model = "form.procurementDirection" popper-class="procurement-create-select"><el-option v-for="(label, value) in procurementDirections" :key="value" :label="label" :value="value" /></ el-select ></ el-form-item >
2026-10-29 11:39:01 +08:00
< div class = "procurement-form-grid" >
< el-form-item label = "采购品类" for = "procurement-category" >< el-input id = "procurement-category" v -model .trim =" form.purchaseCategory " maxlength = "128" placeholder = "请输入品类" /></ el-form-item >
2026-10-29 14:14:56 +08:00
< el-form-item label = "商品名称" for = "procurement-product" prop = "productName" required >< el-input id = "procurement-product" v -model .trim =" form.productName " maxlength = "255" placeholder = "请输入商品名称" /></ el-form-item >
2026-10-29 11:39:01 +08:00
< el-form-item label = "商品规格" for = "procurement-specification" >< el-input id = "procurement-specification" v -model .trim =" form.productSpecification " maxlength = "500" placeholder = "规格、型号" /></ el-form-item >
2026-10-29 14:14:56 +08:00
< el-form-item label = "采购数量(件)" for = "procurement-quantity" prop = "purchaseQuantity" required >< el-input id = "procurement-quantity" v -model .trim =" form.purchaseQuantity " inputmode = "numeric" maxlength = "18" placeholder = "正整数" /></ el-form-item >
< el-form-item label = "预算单价" for = "procurement-price" prop = "budgetUnitPrice" required >< el-input id = "procurement-price" v -model .trim =" form.budgetUnitPrice " inputmode = "decimal" maxlength = "20" placeholder = "请明确填写;免费样品填 0" /></ el-form-item >
< el-form-item label = "预算币种" for = "procurement-currency" prop = "currency" required >< el-select id = "procurement-currency" v-model = "form.currency" popper-class="procurement-create-select"><el-option label="美元 USD" value="USD" /><el-option label="人民币 CNY" value="CNY" /></ el-select ></ el-form-item >
2026-10-30 11:20:03 +08:00
< el-form-item class = "full" label = "商品链接" for = "procurement-url" prop = "productUrl" required >< el-input id = "procurement-url" v -model .trim =" form.productUrl " maxlength = "2000" placeholder = "https://..." /></ el-form-item >
2026-10-29 11:39:01 +08:00
</ div >
2026-10-29 12:03:31 +08:00
< div class = "procurement-budget-total" > 货款预算总上限 : {{ money ( budgetTotal , form . currency ) }}</ div >
2026-10-29 11:39:01 +08:00
</ div >
</ section >
< section class = "procurement-form-section" aria -labelledby =" procurement -recipient-heading " >
< h3 id = "procurement-recipient-heading" >< span class = "procurement-section-dot procurement-section-dot--recipient" aria -hidden =" true " />{{ regionText ( '最终国内收件资料' , form . procurementDirection ) }}</ h3 >
< div class = "procurement-form-card procurement-form-grid" >
2026-10-29 14:14:56 +08:00
< el-form-item label = "收件人" for = "procurement-contact" prop = "contactName" required >< el-input id = "procurement-contact" v -model .trim =" form.contactName " maxlength = "128" placeholder = "实际收件人姓名" /></ el-form-item >
< el-form-item label = "联系电话" for = "procurement-phone" prop = "contactPhone" required >< el-input id = "procurement-phone" v -model .trim =" form.contactPhone " maxlength = "64" type = "tel" placeholder = "快递可联系的电话" /></ el-form-item >
< el-form-item class = "full" : label = "regionText('完整国内地址', form.procurementDirection)" for = "procurement-address" prop = "receivingAddress" required >< el-input id = "procurement-address" v -model .trim =" form.receivingAddress " maxlength = "1000" : placeholder = "form.procurementDirection === 'DOMESTIC_TO_OVERSEAS' ? '国家、州/省、市、邮编及详细地址' : '省、市、区县、街道及详细门牌'" /></ el-form-item >
2026-10-29 11:39:01 +08:00
< el-form-item class = "full" label = "采购要求" for = "procurement-requirements" >
2026-10-30 11:20:03 +08:00
< el-input id = "procurement-requirements" v-model = "form.remark" type="textarea" :rows="4" maxlength="1000" resize="vertical" placeholder="填写采购要求(选填)" aria-describedby="procurement-requirements-count" />
2026-10-29 11:39:01 +08:00
< div id = "procurement-requirements-count" class = "procurement-character-count" >{{ form . remark . length }} / 1000 </ div >
</ el-form-item >
</ div >
</ section >
2026-10-10 02:19:39 +08:00
</ el-form >
2026-10-30 17:59:48 +08:00
< footer class = "procurement-form-actions" >< el-button type = "primary" :loading = "submitting" : disabled = "!companyName || !identity" @click ="submit" > 提交采购订单 </ el -button ></ footer >
2026-10-10 02:19:39 +08:00
</ template >
</ section >
2026-10-30 11:20:03 +08:00
< ProcurementDecisionDialog v-if = "decisionTarget" :key="`${identity}:${decisionTarget.orderNo}:${decision}`" :order="decisionTarget" :decision="decision" @close="decisionTarget = null" @changed="decisionChanged" />
2026-10-10 02:19:39 +08:00
</ section >
</ template >
< style scoped >
. procurement - page { display : grid ; gap : var ( -- client - section - gap ); min - width : 0 ; }
2026-10-29 11:39:01 +08:00
. procurement - table - card , . procurement - detail { min - width : 0 ; border : 1 px solid var ( -- client - border ); border - radius : var ( -- client - radius ); background : # fff ; }
. procurement - table - card { overflow : hidden ; }. procurement - detail { padding : 16 px 20 px 24 px ; }
2026-10-10 02:19:39 +08:00
. procurement - heading { display : flex ; justify - content : space - between ; align - items : center ; gap : 20 px ; min - height : 54 px ; border - bottom : 1 px solid var ( -- border ); padding - bottom : 12 px ; }
. procurement - heading > div { display : flex ; align - items : center ; gap : 14 px ; min - width : 0 ; }. procurement - heading h2 { margin : 0 ; color : var ( -- navy ); font - size : 18 px ; overflow - wrap : anywhere ; }
2026-10-29 11:39:01 +08:00
. procurement - heading p { margin : 7 px 0 0 ; font - size : 13 px ; color : var ( -- muted ); }
2026-10-10 02:19:39 +08:00
. procurement - link { padding : 0 ; border : 0 ; background : transparent ; color : var ( -- client - action ); font : inherit ; cursor : pointer ; text - align : left ; }. procurement - link : hover { text - decoration : underline ; }
. procurement - error { min - height : 160 px ; display : flex ; justify - content : center ; align - items : center ; gap : 16 px ; color : # b42318 ; }. procurement - error span { max - width : 900 px ; overflow - wrap : anywhere ; }
2026-10-29 11:39:01 +08:00
. procurement - page -- create { width : 100 % ; background : # f4f7fc ; }
. procurement - create {
-- client - action : # 2 f7bff ;
-- client - action - hover : # 1 d5fe0 ;
-- client - border : # d5dce8 ;
-- client - control - height : 44 px ;
-- client - font - size : 14 px ;
-- el - color - primary : # 2 f7bff ;
-- el - text - color - regular : # 1 f2937 ;
2026-10-29 14:14:56 +08:00
-- el - text - color - placeholder : # b0b7c3 ;
2026-10-29 11:39:01 +08:00
min - width : 0 ;
color : # 1 f2937 ;
font : 14 px / 1.5 "PingFang SC" , "Microsoft YaHei" , "Noto Sans SC" , sans - serif ;
}
2026-10-29 12:03:31 +08:00
. procurement - create - heading { display : flex ; justify - content : space - between ; align - items : center ; flex - wrap : wrap ; gap : 16 px ; margin - bottom : 12 px ; }
. procurement - create - heading h1 { margin : 0 ; font - size : 26 px ; line - height : 1.4 ; font - weight : 700 ; }
. procurement - summary { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 16 px ; margin - bottom : 12 px ; }
. procurement - summary - card { display : flex ; flex - direction : column ; gap : 8 px ; min - width : 0 ; padding : 12 px 16 px ; border : 1 px solid # e3e8f0 ; border - radius : 10 px ; background : # fff ; }
2026-10-29 11:39:01 +08:00
. procurement - summary - title { display : flex ; align - items : center ; gap : 12 px ; }
. procurement - summary - title h2 { margin : 0 ; font - size : 15 px ; font - weight : 600 ; }
. procurement - summary - icon { display : grid ; width : 44 px ; height : 44 px ; flex : 0 0 44 px ; place - items : center ; border - radius : 8 px ; }
. procurement - summary - icon -- quantity { color : # 2 f7bff ; background : # eaf2ff ; }
. procurement - summary - icon -- price { color : # 16 a34a ; background : # e8f8f0 ; }
. procurement - summary - icon -- currency { color : # d97706 ; background : # fff5e0 ; }
. procurement - summary - icon svg { width : 20 px ; height : 20 px ; fill : none ; stroke : currentColor ; stroke - width : 1.8 ; stroke - linecap : round ; stroke - linejoin : round ; }
. procurement - summary - value { color : # 1 d5fe0 ; font - size : 30 px ; font - weight : 700 ; line - height : 1.2 ; font - variant - numeric : tabular - nums ; overflow - wrap : anywhere ; }
. procurement - summary - value small { margin - left : 4 px ; color : # 6 b7280 ; font - size : 14 px ; font - weight : 500 ; }
2026-10-29 12:03:31 +08:00
. procurement - form - section { margin - bottom : 12 px ; }
. procurement - form - section h3 { display : flex ; align - items : center ; gap : 10 px ; margin : 0 0 10 px ; font - size : 18 px ; font - weight : 700 ; }
2026-10-29 11:39:01 +08:00
. procurement - section - dot { width : 9 px ; height : 9 px ; flex : 0 0 9 px ; border - radius : 50 % ; background : # 2 f7bff ; }
. procurement - section - dot -- recipient { background : # 0 ea5a4 ; }
2026-10-29 12:03:31 +08:00
. procurement - form - card { padding : 16 px ; border : 1 px solid # e3e8f0 ; border - radius : 10 px ; background : # fff ; }
. procurement - form - grid { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 12 px 20 px ; }
2026-10-29 11:39:01 +08:00
. procurement - form - grid . full { grid - column : 1 / - 1 ; }
. procurement - create : deep (. el - form - item ) { min - width : 0 ; margin : 0 ; }
2026-10-29 12:03:31 +08:00
. procurement - create : deep (. el - form - item__label ) { height : auto ; margin - bottom : 6 px ; padding : 0 ; color : # 374151 ; font - size : 14 px ; font - weight : 600 ; line - height : 20 px ; }
2026-10-29 11:39:01 +08:00
. procurement - create : deep (. el - form - item . is - required . el - form - item__label :: before ) { color : # e5484d ; }
. procurement - create : deep (. el - form - item__content ) { min - width : 0 ; line - height : 20 px ; }
2026-10-29 14:14:56 +08:00
. procurement - create : deep (. el - form - item__error ) { position : absolute ; top : auto ; bottom : calc ( 100 % + 6 px ); right : 0 ; left : auto ; width : auto ; max - width : calc ( 100 % - 120 px ); padding : 0 ; font - size : 12 px ; line - height : 20 px ; text - align : right ; }
2026-10-29 12:03:31 +08:00
. procurement - create : deep (. procurement - direction ) { width : calc (( 100 % - 40 px ) / 3 ); margin - bottom : 12 px ; }
2026-10-29 11:39:01 +08:00
. procurement - create : deep (. el - select ) { width : 100 % ; }
. procurement - create : deep (. el - input__wrapper ), . procurement - create : deep (. el - select__wrapper ), . procurement - create : deep (. el - textarea__inner ) { border : 1 px solid # d5dce8 ; border - radius : 6 px ; background : # fff ; box - shadow : none ; font - family : inherit ; font - size : 14 px ; }
. procurement - create : deep (. el - input__wrapper ), . procurement - create : deep (. el - select__wrapper ) { min - height : 44 px ; padding : 10 px 14 px ; }
. procurement - create : deep (. el - input__inner ) { height : 22 px ; color : # 1 f2937 ; line - height : 22 px ; }
. procurement - create : deep (. el - textarea__inner ) { min - height : 110 px ! important ; padding : 11 px 14 px ; color : # 1 f2937 ; line - height : 20 px ; }
2026-10-29 14:14:56 +08:00
. procurement - create : deep (. el - input__inner :: placeholder ), . procurement - create : deep (. el - textarea__inner :: placeholder ) { color : var ( -- el - text - color - placeholder ); opacity : 1 ; }
2026-10-29 11:39:01 +08:00
. procurement - create : deep (. el - input__wrapper . is - focus ), . procurement - create : deep (. el - select__wrapper . is - focused ), . procurement - create : deep (. el - textarea__inner : focus ) { outline : 2 px solid # 2 f7bff ; outline - offset : 0 ; }
2026-10-29 14:14:56 +08:00
. procurement - create : deep (. el - form - item . is - error . el - input__wrapper ), . procurement - create : deep (. el - form - item . is - error . el - select__wrapper ), . procurement - create : deep (. el - form - item . is - error . el - textarea__inner ) { border - color : # e5484d ; box - shadow : none ; }
2026-10-29 11:39:01 +08:00
. procurement - create : deep (. el - input . is - disabled . el - input__wrapper ), . procurement - create : deep (. el - select__wrapper . is - disabled ), . procurement - create : deep (. el - textarea . is - disabled . el - textarea__inner ) { background : # f6f9ff ; }
. procurement - character - count { width : 100 % ; margin - top : 4 px ; color : # 6 b7280 ; font - size : 13 px ; text - align : right ; }
2026-10-29 12:03:31 +08:00
. procurement - budget - total { margin - top : 10 px ; color : # 6 b7280 ; font - size : 13 px ; line - height : 20 px ; overflow - wrap : anywhere ; }
2026-10-29 11:39:01 +08:00
. procurement - form - actions { display : flex ; justify - content : flex - end ; flex - wrap : wrap ; gap : 12 px ; }
. procurement - create : deep (. el - button ) { margin - left : 0 ; padding : 10 px 18 px ; border - radius : 6 px ; color : # 1 d5fe0 ; box - shadow : none ; }
. procurement - create : deep (. el - button -- primary ) { height : auto ; min - height : 44 px ; padding : 12 px 28 px ; color : # fff ; }
. procurement - create : deep (. el - button . is - disabled ) { color : # 98 a2b3 ; }
. submit - error { margin : 0 0 20 px ; }
: global (. procurement - create - select . el - popper ) { border : 1 px solid # d5dce8 ; box - shadow : none ; }
2026-10-10 02:19:39 +08:00
@ media ( max - width : 767 px ) {
2026-10-29 11:39:01 +08:00
. procurement - form - grid , . procurement - summary { grid - template - columns : 1 fr ; }. procurement - heading , . procurement - heading > div { flex - wrap : wrap ; }
. procurement - create : deep (. procurement - direction ) { width : 100 % ; }
2026-10-10 02:19:39 +08:00
}
</ style >