This commit is contained in:
2026-08-13 11:57:44 +08:00
parent 7beca3f985
commit 835ac0ad12
87 changed files with 3721 additions and 295 deletions
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
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, scopeLabels, settlementLabels, statusLabels, type Supplier } from '@/modules/suppliers/types'
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)
const items = computed(() => supplier.value ? [
['供应商名称', supplier.value.name], ['供应商类别', categoryLabels[supplier.value.category]], ['国内/国外', supplier.value.scope ? scopeLabels[supplier.value.scope] : '—'], ['地区', supplier.value.region],
['法人代表', 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 || '—'],
] : [])
async function load() { if (!canView.value) return; loading.value = true; error.value = null; try { supplier.value = await getSupplier(supplierId.value) } catch (cause) { error.value = getApiErrorDetails(cause, '供应商详情加载失败') } finally { loading.value = false } }
onMounted(load)
</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>