Files
admin-web/src/modules/suppliers/pages/SupplierDetailView.vue
T

65 lines
6.0 KiB
Vue
Raw Normal View History

2026-08-13 11:57:44 +08:00
<script setup lang="ts">
2026-08-14 14:27:03 +08:00
import { computed, ref, watch } from 'vue'
2026-08-13 11:57:44 +08:00
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, FileText, Pencil } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import SupplierLicenseDialog from '@/modules/suppliers/components/SupplierLicenseDialog.vue'
import { getSupplier } from '@/modules/suppliers/api/platformSuppliers'
import { categoryLabels, formatCarrierCodes, settlementLabels, statusLabels, type Supplier } from '@/modules/suppliers/types'
2026-08-13 11:57:44 +08:00
const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore()
const canView = computed(() => access.can('platform:supplier:list'))
const supplierId = computed(() => typeof route.params.id === 'string' ? route.params.id : '')
const supplier = ref<Supplier | null>(null); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const licenseOpen = ref(false)
2026-08-14 14:27:03 +08:00
let loadSequence = 0
2026-08-13 11:57:44 +08:00
const items = computed(() => supplier.value ? [
['供应商名称', supplier.value.name], ['供应商类别', categoryLabels[supplier.value.category]], ['可代理船公司', supplier.value.category === 'BOOKING' ? formatCarrierCodes(supplier.value.carrierCodes) : '—'], ['地区', supplier.value.region],
2026-08-13 11:57:44 +08:00
['法人代表', supplier.value.legalRepresentative], ['联系人', supplier.value.contactName], ['联系电话', supplier.value.phone], ['电子邮箱', supplier.value.email || '—'],
['营业执照号', supplier.value.businessLicenseNo || '—'], ['结算周期', settlementLabels[supplier.value.settlementPeriod]], ['公司地址', supplier.value.address || '—'], ['备注', supplier.value.remark || '—'],
] : [])
2026-08-14 14:27:03 +08:00
async function load() {
const requestedId = supplierId.value
const sequence = ++loadSequence
if (!canView.value || !requestedId) { supplier.value = null; loading.value = false; error.value = null; return }
supplier.value = null
loading.value = true
error.value = null
try {
const result = await getSupplier(requestedId)
if (sequence !== loadSequence || requestedId !== supplierId.value) return
supplier.value = result
} catch (cause) {
if (sequence !== loadSequence || requestedId !== supplierId.value) return
error.value = getApiErrorDetails(cause, '供应商详情加载失败')
} finally {
if (sequence === loadSequence) loading.value = false
}
}
watch(supplierId, () => { void load() }, { immediate: true })
2026-08-13 11:57:44 +08:00
</script>
<template>
<AdminPage width="comfortable"><DetailPageLayout aria-label="供应商详情">
<template #header><div class="detail-header"><div><h2>{{ supplier?.name || '供应商详情' }}</h2><StatusBadge v-if="supplier" :status="statusLabels[supplier.status]" /></div><div class="detail-actions"><el-button @click="router.push({ name: 'supplier-list' })"><ArrowLeft :size="16" />返回</el-button><el-button v-if="supplier && access.can('platform:supplier:edit')" type="primary" @click="router.push({ name: 'supplier-edit', params: { id: supplier.id } })"><Pencil :size="16" />编辑</el-button></div></div></template>
<LoadErrorState v-if="!canView" title="暂无供应商查看权限" message="请联系管理员开通供应商查看权限。" :show-retry="false" />
<LoadErrorState v-else-if="error" title="供应商详情加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<section v-else v-loading="loading" class="surface-panel detail-panel">
<dl v-if="supplier" class="detail-grid"><div v-for="item in items" :key="item[0]"><dt>{{ item[0] }}</dt><dd>{{ item[1] }}</dd></div></dl>
<div v-if="supplier" class="license-row"><div><strong>营业执照</strong><span>{{ supplier.licenseFileId ? '已关联资质文件' : '未上传资质文件' }}</span></div><el-button type="primary" plain @click="licenseOpen = true"><FileText :size="16" />查看营业执照</el-button></div>
</section>
<SupplierLicenseDialog v-model="licenseOpen" :supplier="supplier" />
</DetailPageLayout></AdminPage>
</template>
<style scoped>
.detail-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 20px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }.detail-header > div { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; }.detail-header h2 { margin: 0; font-size: 16px; overflow-wrap: anywhere; }.detail-actions :deep(.el-button + .el-button) { margin-left: 0; }
.detail-panel { padding: 22px; }.detail-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; }.detail-grid div { min-width: 0; padding: 14px 16px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }.detail-grid div:nth-child(3n) { border-right: 0; }.detail-grid dt { margin-bottom: 6px; color: var(--cc-muted); font-size: 12px; }.detail-grid dd { margin: 0; line-height: 1.55; overflow-wrap: anywhere; }.license-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 20px; padding: 16px; border: 1px dashed var(--cc-border); border-radius: 6px; background: #fafafa; }.license-row strong,.license-row span { display: block; }.license-row span { margin-top: 4px; color: var(--cc-muted); font-size: 12px; }
@media (max-width: 720px) { .detail-header,.license-row { align-items: stretch; flex-direction: column; }.detail-actions { display: grid !important; grid-template-columns: 1fr 1fr; }.detail-actions :deep(.el-button) { min-height: 44px; }.detail-grid { grid-template-columns: 1fr; }.detail-grid div,.detail-grid div:nth-child(3n) { border-right: 0; }.detail-panel { padding: 16px; } }
</style>