Files
client-web/src/modules/cross-border/views/ProcurementView.vue
T

295 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
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'
import { confirmProcurement, createProcurement, getProcurement, getProcurementWorkflow, listProcurements, type ProcurementDetail, type ProcurementSummary, type ProcurementWorkflow } from '../api/procurement'
import { procurementDirections, directionLabel, regionText, buildProcurementCreate, dateTime, draftBudgetTotal, emptyProcurementDraft, money, procurementStatuses, statusLabel, statusTone } from '../procurementPresentation'
import { procurementRequestKey } from '../procurementSubmission'
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())
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))
const submitError = ref(''), submittedOrder = ref('')
const submitting = ref(false), loading = ref(false), error = ref('')
const detail = shallowRef<ProcurementDetail | null>(null)
const workflow = shallowRef<ProcurementWorkflow | null>(null)
const confirming = ref(false), confirmError = ref('')
const canConfirm = computed(() => canCreate.value && workflow.value?.status === 'WAIT_CUSTOMER_CONFIRM')
const rows = ref<ProcurementSummary[]>([]), total = ref(0)
const query = reactive({ page: 1, size: 20, orderNo: '', purchaseCategory: '', productName: '', status: '' })
let requestSequence = 0, active = true, inFlight = ''
let createAttempt: { fingerprint: string; key: string } | null = null
let confirmAttempt: { fingerprint: string; key: string } | null = null
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' },
{ key: 'status', label: '履约进度', type: 'select', options: Object.entries(procurementStatuses).map(([value, label]) => ({ value, label })) },
]
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
{ prop: 'procurementDirection', label: '采购方向', minWidth: 170, slotName: 'direction' },
{ 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
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 }
}
catch (cause) { if (sequence === requestSequence) error.value = message(cause) }
finally { if (sequence === requestSequence) { loading.value = false; inFlight = '' } }
}
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 }
}
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() }
function resetForm() { submitError.value = ''; submittedOrder.value = ''; Object.assign(form, emptyProcurementDraft()); createAttempt = null }
async function submit() {
if (submitting.value || !canCreate.value || !identity.value || submittedOrder.value) return
const owner = identity.value; submitting.value = true; submitError.value = ''
try {
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 })
if (!active || owner !== identity.value) return
createAttempt = null; submittedOrder.value = result.order.orderNo; ElMessage.success('采购订单已提交')
if (canList.value) await router.push({ name: 'cross-border-procurement-detail', params: { orderNo: result.order.orderNo } })
} catch (cause) { if (active && owner === identity.value) submitError.value = message(cause) }
finally { submitting.value = false }
}
watch(() => [mode.value, orderNo.value, query.page, query.size, identity.value], (values, previous) => {
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 = ''
if (mode.value === 'create') resetForm()
else if (mode.value === 'detail') void loadDetail()
else void loadList()
}, { immediate: true })
onBeforeUnmount(() => { active = false; ++requestSequence })
</script>
<template>
<section class="procurement-page client-ui" :class="{ 'procurement-page--create': mode === 'create' }" :aria-busy="loading || submitting">
<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>
<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>
<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="采购订单详情">
<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>
<div v-if="error" class="procurement-error" role="alert"><span>{{ error }}</span><el-button :disabled="loading" @click="loadDetail">重新加载</el-button></div>
<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 />
</section>
<section v-else class="procurement-create" aria-label="采购下单">
<header class="procurement-create-heading">
<div><h1>海外采购下单</h1><p>一单一个商品及规格;预算仅授权样品货款,运费及关税另行核定。</p></div>
<el-button v-if="canList" :disabled="submitting" @click="returnToList">返回列表</el-button>
</header>
<el-result v-if="!canCreate" icon="warning" title="没有采购下单权限" sub-title="请联系企业管理员配置权限" />
<el-result v-else-if="submittedOrder" icon="success" title="采购订单已提交" :sub-title="submittedOrder"><template #extra><el-button @click="resetForm">新建另一单</el-button></template></el-result>
<template v-else>
<el-alert v-if="submitError" class="submit-error" :title="submitError" type="error" :closable="false" />
<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>
<el-form :model="form" label-position="top" :disabled="submitting" @submit.prevent="submit">
<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>
</el-form>
<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>
</template>
</section>
</section>
</template>
<style scoped>
.procurement-page { display: grid; gap: var(--client-section-gap); min-width: 0; }
.procurement-table-card, .procurement-detail { min-width: 0; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; }
.procurement-table-card { overflow: hidden; }.procurement-detail { padding: 16px 20px 24px; }
.procurement-heading { display: flex; justify-content: space-between; align-items: center; gap: 20px; min-height: 54px; border-bottom: 1px solid var(--border); padding-bottom: 12px; }
.procurement-heading > div { display: flex; align-items: center; gap: 14px; min-width: 0; }.procurement-heading h2 { margin: 0; color: var(--navy); font-size: 18px; overflow-wrap: anywhere; }
.procurement-heading p { margin: 7px 0 0; font-size: 13px; color: var(--muted); }
.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: 160px; display: flex; justify-content: center; align-items: center; gap: 16px; color: #b42318; }.procurement-error span { max-width: 900px; overflow-wrap: anywhere; }
.procurement-page--create { width: 100%; background: #f4f7fc; }
.procurement-create {
--client-action: #2f7bff;
--client-action-hover: #1d5fe0;
--client-border: #d5dce8;
--client-control-height: 44px;
--client-font-size: 14px;
--el-color-primary: #2f7bff;
--el-text-color-regular: #1f2937;
--el-text-color-placeholder: #6b7280;
min-width: 0;
color: #1f2937;
font: 14px/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: 16px; margin-bottom: 20px; }
.procurement-create-heading h1 { margin: 0 0 6px; font-size: 26px; line-height: 1.4; font-weight: 700; }
.procurement-create-heading p { margin: 0; color: #6b7280; }
.procurement-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-bottom: 26px; }
.procurement-summary-card { display: flex; flex-direction: column; gap: 14px; min-width: 0; padding: 18px 20px; border: 1px solid #e3e8f0; border-radius: 10px; background: #fff; }
.procurement-summary-title { display: flex; align-items: center; gap: 12px; }
.procurement-summary-title h2 { margin: 0; font-size: 15px; font-weight: 600; }
.procurement-summary-icon { display: grid; width: 44px; height: 44px; flex: 0 0 44px; place-items: center; border-radius: 8px; }
.procurement-summary-icon--quantity { color: #2f7bff; background: #eaf2ff; }
.procurement-summary-icon--price { color: #16a34a; background: #e8f8f0; }
.procurement-summary-icon--currency { color: #d97706; background: #fff5e0; }
.procurement-summary-icon svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.procurement-summary-value { color: #1d5fe0; font-size: 30px; font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.procurement-summary-value small { margin-left: 4px; color: #6b7280; font-size: 14px; font-weight: 500; }
.procurement-summary-card p { margin: 0; color: #6b7280; font-size: 13px; }
.procurement-form-section { margin-bottom: 26px; }
.procurement-form-section h3 { display: flex; align-items: center; gap: 10px; margin: 0 0 14px; font-size: 18px; font-weight: 700; }
.procurement-section-dot { width: 9px; height: 9px; flex: 0 0 9px; border-radius: 50%; background: #2f7bff; }
.procurement-section-dot--recipient { background: #0ea5a4; }
.procurement-form-card { padding: 24px; border: 1px solid #e3e8f0; border-radius: 10px; background: #fff; }
.procurement-form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
.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: 8px; padding: 0; color: #374151; font-size: 14px; font-weight: 600; line-height: 20px; }
.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: 20px; }
.procurement-create :deep(.procurement-direction) { width: calc((100% - 40px) / 3); margin-bottom: 20px; }
.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: 1px solid #d5dce8; border-radius: 6px; background: #fff; box-shadow: none; font-family: inherit; font-size: 14px; }
.procurement-create :deep(.el-input__wrapper), .procurement-create :deep(.el-select__wrapper) { min-height: 44px; padding: 10px 14px; }
.procurement-create :deep(.el-input__inner) { height: 22px; color: #1f2937; line-height: 22px; }
.procurement-create :deep(.el-textarea__inner) { min-height: 110px !important; padding: 11px 14px; color: #1f2937; line-height: 20px; }
.procurement-create :deep(.el-input__inner::placeholder), .procurement-create :deep(.el-textarea__inner::placeholder) { color: #6b7280; }
.procurement-create :deep(.el-input__wrapper.is-focus), .procurement-create :deep(.el-select__wrapper.is-focused), .procurement-create :deep(.el-textarea__inner:focus) { outline: 2px solid #2f7bff; 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: 4px; color: #6b7280; font-size: 13px; text-align: right; }
.procurement-budget-tip { display: inline-flex; max-width: 100%; margin-top: 18px; padding: 8px 14px; border-radius: 999px; background: #fff7e6; color: #8a5a00; font-size: 13px; overflow-wrap: anywhere; }
.procurement-form-actions { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 12px; }
.procurement-create :deep(.el-button) { margin-left: 0; padding: 10px 18px; border-radius: 6px; color: #1d5fe0; box-shadow: none; }
.procurement-create :deep(.el-button--primary) { height: auto; min-height: 44px; padding: 12px 28px; color: #fff; }
.procurement-create :deep(.el-button.is-disabled) { color: #98a2b3; }
.submit-error { margin: 0 0 20px; }
:global(.procurement-create-select.el-popper) { border: 1px solid #d5dce8; box-shadow: none; }
@media (max-width: 767px) {
.procurement-form-grid, .procurement-summary { grid-template-columns: 1fr; }.procurement-heading, .procurement-heading > div { flex-wrap: wrap; }
.procurement-create :deep(.procurement-direction) { width: 100%; }
}
</style>