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-27 17:39:28 +08:00
import { ElMessage , ElMessageBox } 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-27 17:39:28 +08:00
import { confirmProcurement , 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-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 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 )
const confirming = ref ( false ), confirmError = ref ( '' )
const canConfirm = computed (() => canCreate . value && workflow . value ? . status === 'WAIT_CUSTOMER_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
let confirmAttempt : { 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 [] = [
{ key : 'actions' , label : '操作' , width : 90 , 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' },
{ prop : 'status' , label : '履约进度' , width : 180 , slotName : 'status' }, { prop : 'createdAt' , label : '下单时间' , width : 185 , slotName : 'createdAt' },
]
function rowActions ( row : object ) : TableAction [] { return [{ key : 'detail' , label : '详情' , run : () => openDetail ( row as ProcurementSummary ) }] }
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 = '' } }
}
2026-10-27 17:39:28 +08:00
async function confirmReceived () {
if ( ! canConfirm . value || confirming . value || ! workflow . value ) return
const owner = identity . value , targetOrder = orderNo . value , version = workflow . value . version
confirming . value = true ; confirmError . value = ''
try {
await ElMessageBox . confirm ( '确认本单收货数量及照片,确认后平台将安排出库。' , '确认采购结果' , { confirmButtonText : '确认' , cancelButtonText : '取消' , type : 'warning' })
if ( ! active || owner !== identity . value || targetOrder !== orderNo . value ) return
const fingerprint = JSON . stringify ([ owner , targetOrder , version ])
if ( confirmAttempt ? . fingerprint !== fingerprint ) confirmAttempt = { fingerprint , key : procurementRequestKey () }
const result = await confirmProcurement ( targetOrder , { version , idempotencyKey : confirmAttempt . key })
if ( ! active || owner !== identity . value || targetOrder !== orderNo . value ) return
workflow . value = result ; confirmAttempt = null
if ( detail . value ) detail . value = { ... detail . value , order : { ... detail . value . order , status : result . status , version : result . version } }
ElMessage . success ( '采购结果已确认' )
} catch ( cause ) {
if ( cause !== 'cancel' && cause !== 'close' && active && owner === identity . value && targetOrder === orderNo . value ) confirmError . value = message ( cause )
} finally { confirming . value = false }
}
2026-10-10 02:19:39 +08:00
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-27 17:39:28 +08:00
function resetForm () { submitError . value = '' ; submittedOrder . value = '' ; Object . assign ( form , emptyProcurementDraft ()); 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-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 () }
const result = await createProcurement ({ ... payload , idempotencyKey : createAttempt . key })
2026-10-10 02:19:39 +08:00
if ( ! active || owner !== identity . value ) return
2026-10-27 17:39:28 +08:00
createAttempt = null ; submittedOrder . value = result . order . orderNo ; ElMessage . success ( '采购订单已提交' )
2026-10-10 02:19:39 +08:00
if ( canList . value ) await router . push ({ name : 'cross-border-procurement-detail' , params : { orderNo : result . order . orderNo } })
2026-10-22 23:06:02 +08:00
} catch ( cause ) { if ( active && owner === identity . value ) submitError . value = message ( cause ) }
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 }
if ( values [ 4 ] !== previous ? .[ 4 ] || values [ 1 ] !== previous ? .[ 1 ]) confirmAttempt = null
++ requestSequence ; inFlight = '' ; loading . value = false ; error . value = '' ; detail . value = null ; workflow . value = null ; confirmError . value = ''
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-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-27 17:39:28 +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 v-if="canConfirm" type="primary" :loading="confirming" @click="confirmReceived">确认采购结果</el-button><el-button :disabled="loading || confirming" @click="loadDetail" > 刷新 </ el -button ></ div ></ header >
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-27 17:39:28 +08:00
< template v-else-if = "detail && workflow"><el-alert v-if="confirmError" :title="confirmError" type="error" :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" >
< div >< h1 > 海外采购下单 </ h1 >< p > 一单一个商品及规格 ; 预算仅授权样品货款 , 运费及关税另行核定 。 </ p ></ div >
< 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 >
< p > 一单一个商品及规格 </ p >
</ 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 >
< p > 请明确填写 ; 免费样品填 0 </ p >
</ 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 >
< p > 预算仅授权样品货款 </ p >
</ article >
</ section >
2026-10-22 23:06:02 +08:00
< el-form :model = "form" label -position =" top " :disabled = "submitting" @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" >
< el-form-item class = "procurement-direction" label = "采购方向" for = "procurement-direction" 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 >
< 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 >
< el-form-item label = "商品名称" for = "procurement-product" required >< el-input id = "procurement-product" v -model .trim =" form.productName " maxlength = "255" placeholder = "请输入商品名称" /></ el-form-item >
< el-form-item label = "商品规格" for = "procurement-specification" >< el-input id = "procurement-specification" v -model .trim =" form.productSpecification " maxlength = "500" placeholder = "规格、型号" /></ el-form-item >
< el-form-item label = "采购数量(件)" for = "procurement-quantity" 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" 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" 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 >
< el-form-item class = "full" label = "商品链接" for = "procurement-url" >< el-input id = "procurement-url" v -model .trim =" form.productUrl " maxlength = "2000" placeholder = "https://..." /></ el-form-item >
</ div >
< div class = "procurement-budget-tip" > 货款预算总上限 : {{ money ( budgetTotal , form . currency ) }} 。 零预算仅授权免费样品 ; 实际货款按供应商实付核定 。 </ div >
</ 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" >
< el-form-item label = "收件人" for = "procurement-contact" required >< el-input id = "procurement-contact" v -model .trim =" form.contactName " maxlength = "128" placeholder = "实际收件人姓名" /></ el-form-item >
< el-form-item label = "联系电话" for = "procurement-phone" 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" required >< el-input id = "procurement-address" v -model .trim =" form.receivingAddress " maxlength = "1000" : placeholder = "form.procurementDirection === 'DOMESTIC_TO_OVERSEAS' ? '国家、州/省、市、邮编及详细地址' : '省、市、区县、街道及详细门牌'" /></ el-form-item >
< el-form-item class = "full" label = "采购要求" for = "procurement-requirements" >
< el-input id = "procurement-requirements" v-model = "form.remark" type="textarea" :rows="4" maxlength="1000" resize="vertical" aria-describedby="procurement-requirements-count" />
< 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-29 11:39:01 +08:00
< footer class = "procurement-form-actions" >< el-button v-if = "canList" :disabled="submitting" @click="returnToList">返回列表</el-button><el-button type="primary" :loading="submitting" :disabled="!companyName || !identity" @click="submit" > 提交采购订单 </ el -button ></ footer >
2026-10-10 02:19:39 +08:00
</ template >
</ section >
</ 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 ;
-- el - text - color - placeholder : # 6 b7280 ;
min - width : 0 ;
color : # 1 f2937 ;
font : 14 px / 1.5 "PingFang SC" , "Microsoft YaHei" , "Noto Sans SC" , sans - serif ;
}
. procurement - create - heading { display : flex ; justify - content : space - between ; align - items : flex - start ; flex - wrap : wrap ; gap : 16 px ; margin - bottom : 20 px ; }
. procurement - create - heading h1 { margin : 0 0 6 px ; font - size : 26 px ; line - height : 1.4 ; font - weight : 700 ; }
. procurement - create - heading p { margin : 0 ; color : # 6 b7280 ; }
. procurement - summary { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); gap : 16 px ; margin - bottom : 26 px ; }
. procurement - summary - card { display : flex ; flex - direction : column ; gap : 14 px ; min - width : 0 ; padding : 18 px 20 px ; border : 1 px solid # e3e8f0 ; border - radius : 10 px ; background : # fff ; }
. 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 ; }
. procurement - summary - card p { margin : 0 ; color : # 6 b7280 ; font - size : 13 px ; }
. procurement - form - section { margin - bottom : 26 px ; }
. procurement - form - section h3 { display : flex ; align - items : center ; gap : 10 px ; margin : 0 0 14 px ; font - size : 18 px ; font - weight : 700 ; }
. 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 ; }
. procurement - form - card { padding : 24 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 : 20 px ; }
. procurement - form - grid . full { grid - column : 1 / - 1 ; }
. procurement - create : deep (. el - form - item ) { min - width : 0 ; margin : 0 ; }
. procurement - create : deep (. el - form - item__label ) { height : auto ; margin - bottom : 8 px ; padding : 0 ; color : # 374151 ; font - size : 14 px ; font - weight : 600 ; line - height : 20 px ; }
. 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 ; }
. procurement - create : deep (. procurement - direction ) { width : calc (( 100 % - 40 px ) / 3 ); margin - bottom : 20 px ; }
. 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 ; }
. procurement - create : deep (. el - input__inner :: placeholder ), . procurement - create : deep (. el - textarea__inner :: placeholder ) { color : # 6 b7280 ; }
. 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 ; }
. 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 ; }
. procurement - budget - tip { display : inline - flex ; max - width : 100 % ; margin - top : 18 px ; padding : 8 px 14 px ; border - radius : 999 px ; background : # fff7e6 ; color : # 8 a5a00 ; font - size : 13 px ; overflow - wrap : anywhere ; }
. 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 >