2026-08-01 12:17:33 +08:00
< script setup lang = "ts" >
2026-08-04 21:01:39 +08:00
import { computed , onMounted , reactive , ref , watch } from 'vue'
2026-08-06 15:32:02 +08:00
import { ElMessage , ElMessageBox , type FormInstance , type FormRules } from 'element-plus'
import { Building2 , Eye , MapPin , Pencil , Plus , Power , Upload } from '@lucide/vue'
2026-08-03 18:49:18 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-06 15:32:02 +08:00
import BatchImportDialog from '@/shared/components/BatchImportDialog.vue'
2026-08-03 18:49:18 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-08-01 12:17:33 +08:00
import {
changePlatformPartnerStatus ,
createPlatformPartner ,
2026-08-03 14:58:17 +08:00
downloadPlatformPartnerImportTemplate ,
getPlatformPartner ,
importPlatformPartners ,
2026-08-01 12:17:33 +08:00
listPlatformPartners ,
updatePlatformPartner ,
type PlatformPartnerResource ,
type PlatformPartnerStatus ,
2026-08-03 14:58:17 +08:00
type PlatformPartnerLegacyType ,
2026-08-01 12:17:33 +08:00
type PlatformPartnerType ,
type SavePlatformPartnerRequest ,
2026-08-03 18:49:18 +08:00
} from '@/modules/master-data/counterparties/api/platformPartners'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-01 12:17:33 +08:00
const pageSize = ref ( 20 )
const access = useAccessPreviewStore ()
const loading = ref ( false )
const saving = ref ( false )
const editorOpen = ref ( false )
2026-08-03 14:58:17 +08:00
const detailOpen = ref ( false )
const detailLoading = ref ( false )
const detail = ref < PlatformPartnerResource | null >( null )
const importOpen = ref ( false )
2026-08-01 12:17:33 +08:00
const formRef = ref < FormInstance >()
const rows = ref < PlatformPartnerResource [] >([])
const page = ref ( 1 )
const total = ref ( 0 )
const loadError = ref < ApiErrorDetails | null >( null )
const statusLoadingIds = ref ( new Set < string >())
2026-08-04 09:05:19 +08:00
let loadSequence = 0
2026-08-01 12:17:33 +08:00
type PartnerFilters = QueryModel & {
keyword : string
type : PlatformPartnerType | ''
status : PlatformPartnerStatus | ''
}
const filters = ref < PartnerFilters >({
keyword : '' ,
type : '' ,
status : '' ,
})
const appliedFilters = ref < PartnerFilters >({ ... filters . value })
2026-08-04 21:01:39 +08:00
const queryFields = computed < QueryField [] >(() => [
2026-08-06 19:03:13 +08:00
{ key : 'keyword' , type : 'input' , label : '往来单位关键词' , placeholder : '单位名称、编码、联系人或电话' },
2026-08-01 12:17:33 +08:00
{
key : 'type' ,
type : 'select' ,
2026-08-06 19:03:13 +08:00
label : '单位类型' ,
2026-08-01 12:17:33 +08:00
placeholder : '全部类型' ,
options : [
{ label : '客户' , value : 'CUSTOMER' },
{ label : '供应商' , value : 'SUPPLIER' },
2026-08-03 14:58:17 +08:00
{ label : '承运商' , value : 'CARRIER' },
{ label : '报关行' , value : 'CUSTOMS_BROKER' },
{ label : '发货单位' , value : 'SHIPPER' },
{ label : '收货单位' , value : 'CONSIGNEE' },
2026-08-01 12:17:33 +08:00
],
},
{
key : 'status' ,
type : 'select' ,
2026-08-06 19:03:13 +08:00
label : '状态' ,
2026-08-01 12:17:33 +08:00
placeholder : '全部状态' ,
options : [
{ label : '启用' , value : 'ACTIVE' },
{ label : '停用' , value : 'INACTIVE' },
],
},
2026-08-04 21:01:39 +08:00
])
2026-08-01 12:17:33 +08:00
interface PartnerForm extends SavePlatformPartnerRequest {
id : string
}
const emptyForm = () : PartnerForm => ({
id : '' ,
partnerCode : '' ,
partnerName : '' ,
partnerType : 'CUSTOMER' ,
2026-08-03 14:58:17 +08:00
types : [ 'CUSTOMER' ],
2026-08-01 12:17:33 +08:00
contactName : '' ,
contactPhone : '' ,
contactEmail : null ,
countryCode : 'CN' ,
province : null ,
city : '' ,
district : null ,
addressLine : '' ,
postalCode : null ,
status : 'ACTIVE' ,
2026-08-03 14:58:17 +08:00
remark : null ,
2026-08-01 12:17:33 +08:00
})
const form = reactive < PartnerForm >( emptyForm ())
const editorLabel = computed (() => form . id ? '编辑往来单位' : '新增往来单位' )
const editorPermission = computed (() => form . id ? 'platform:counterparty:edit' : 'platform:counterparty:create' )
const { captureBaseline } = useUnsavedWorkRegistration ({
key : 'partner-editor' ,
label : editorLabel ,
isOpen : editorOpen ,
model : form ,
requiredPermission : editorPermission ,
})
const rules : FormRules < PartnerForm > = {
partnerCode : [{ required : true , whitespace : true , message : '请输入往来单位编码' , trigger : 'blur' }],
partnerName : [{ required : true , whitespace : true , message : '请输入往来单位名称' , trigger : 'blur' }],
2026-08-03 14:58:17 +08:00
types : [{ required : true , type : 'array' , min : 1 , message : '请至少选择一个单位类型' , trigger : 'change' }],
2026-08-01 12:17:33 +08:00
contactName : [{ required : true , whitespace : true , message : '请输入联系人' , trigger : 'blur' }],
contactPhone : [{ required : true , whitespace : true , message : '请输入联系电话' , trigger : 'blur' }],
contactEmail : [{ type : 'email' , message : '请输入有效的邮箱地址' , trigger : 'blur' }],
countryCode : [
{ required : true , whitespace : true , message : '请输入国家二字码' , trigger : 'blur' },
{ pattern : /^[A-Z]{2}$/ , message : '国家码须为两个大写英文字母' , trigger : 'blur' },
],
city : [{ required : true , whitespace : true , message : '请输入城市' , trigger : 'blur' }],
addressLine : [{ required : true , whitespace : true , message : '请输入详细地址' , trigger : 'blur' }],
status : [{ required : true , message : '请选择状态' , trigger : 'change' }],
}
const typeLabels : Record < PlatformPartnerType , string > = {
CUSTOMER : '客户' ,
SUPPLIER : '供应商' ,
2026-08-03 14:58:17 +08:00
CARRIER : '承运商' ,
CUSTOMS_BROKER : '报关行' ,
SHIPPER : '发货单位' ,
CONSIGNEE : '收货单位' ,
}
const typeOptions = Object . entries ( typeLabels ). map (([ value , label ]) => ({ value : value as PlatformPartnerType , label }))
function partnerTypeLabel ( type : PlatformPartnerLegacyType ) {
return type === 'BOTH' ? '客户 / 供应商' : typeLabels [ type ]
2026-08-01 12:17:33 +08:00
}
const partnerColumns : DataTableColumn [] = [
2026-08-03 14:58:17 +08:00
{ key : 'actions' , label : '操作' , width : 210 , fixed : 'left' , slotName : 'actions' },
2026-08-01 12:17:33 +08:00
{ key : 'partner' , label : '往来单位' , minWidth : 210 , slotName : 'partner' },
{ key : 'type' , label : '类型' , minWidth : 115 , slotName : 'type' },
{ key : 'contact' , label : '联系人' , minWidth : 165 , slotName : 'contact' },
{ key : 'address' , label : '地址' , minWidth : 240 , slotName : 'address' },
{ key : 'status' , label : '状态' , width : 90 , slotName : 'status' },
2026-08-06 19:03:13 +08:00
{ key : 'updatedAt' , prop : 'updatedAt' , label : '更新时间' , minWidth : 140 , formatter : ( row : PlatformPartnerResource ) => formatUpdatedAt ( row . updatedAt ) },
2026-08-01 12:17:33 +08:00
]
watch (
() => access . can ( editorPermission . value ),
( canWrite ) => {
if ( canWrite || ! editorOpen . value ) return
editorOpen . value = false
Object . assign ( form , emptyForm ())
},
{ flush : 'sync' },
)
function queryParams () {
return {
page : page . value ,
size : pageSize . value ,
keyword : appliedFilters . value . keyword . trim () || undefined ,
type : appliedFilters . value . type || undefined ,
status : appliedFilters . value . status || undefined ,
}
}
2026-08-04 21:01:39 +08:00
async function loadPartners () {
2026-08-04 09:05:19 +08:00
const sequence = ++ loadSequence
2026-08-01 12:17:33 +08:00
loading . value = true
loadError . value = null
try {
2026-08-06 14:55:17 +08:00
const result = await listPlatformPartners ( queryParams ())
2026-08-04 09:05:19 +08:00
if ( sequence !== loadSequence ) return
2026-08-01 12:17:33 +08:00
rows . value = result . records
total . value = result . total
} catch ( error ) {
2026-08-04 09:05:19 +08:00
if ( sequence !== loadSequence ) return
rows . value = []
total . value = 0
2026-08-01 12:17:33 +08:00
loadError . value = getApiErrorDetails ( error , '往来单位加载失败,请稍后重试' )
} finally {
2026-08-04 09:05:19 +08:00
if ( sequence === loadSequence ) loading . value = false
2026-08-01 12:17:33 +08:00
}
}
function searchPartners ( model : QueryModel ) {
appliedFilters . value = {
keyword : String ( model . keyword || '' ),
type : ( model . type || '' ) as PlatformPartnerType | '' ,
status : ( model . status || '' ) as PlatformPartnerStatus | '' ,
}
if ( page . value === 1 ) {
void loadPartners ()
return
}
page . value = 1
}
function openCreate () {
Object . assign ( form , emptyForm ())
captureBaseline ()
editorOpen . value = true
requestAnimationFrame (() => formRef . value ? . clearValidate ())
}
function nullable ( value : string | null ) {
return value ? . trim () || null
}
function openEdit ( row : PlatformPartnerResource ) {
Object . assign ( form , {
id : String ( row . id ),
partnerCode : row . partnerCode ,
partnerName : row . partnerName ,
2026-08-03 14:58:17 +08:00
partnerType : row . types [ 0 ],
types : [... row . types ],
2026-08-01 12:17:33 +08:00
contactName : row . contactName ,
contactPhone : row . contactPhone ,
contactEmail : row . contactEmail ,
countryCode : row . countryCode ,
province : row . province ,
city : row . city ,
district : row . district ,
addressLine : row . addressLine ,
postalCode : row . postalCode ,
status : row . status ,
2026-08-03 14:58:17 +08:00
remark : row . remark ,
2026-08-01 12:17:33 +08:00
})
captureBaseline ()
editorOpen . value = true
requestAnimationFrame (() => formRef . value ? . clearValidate ())
}
function savePayload () : SavePlatformPartnerRequest {
2026-08-03 14:58:17 +08:00
const types = [... form . types ]
2026-08-01 12:17:33 +08:00
return {
partnerCode : form . partnerCode . trim (),
partnerName : form . partnerName . trim (),
2026-08-03 14:58:17 +08:00
partnerType : types [ 0 ],
types ,
2026-08-01 12:17:33 +08:00
contactName : form . contactName . trim (),
contactPhone : form . contactPhone . trim (),
contactEmail : nullable ( form . contactEmail ),
countryCode : form . countryCode . trim (). toUpperCase (),
province : nullable ( form . province ),
city : form . city . trim (),
district : nullable ( form . district ),
addressLine : form . addressLine . trim (),
postalCode : nullable ( form . postalCode ),
status : form . status ,
2026-08-03 14:58:17 +08:00
remark : nullable ( form . remark ),
2026-08-01 12:17:33 +08:00
}
}
2026-08-03 14:58:17 +08:00
async function openDetail ( row : PlatformPartnerResource ) {
detailOpen . value = true
detailLoading . value = true
detail . value = null
try {
2026-08-06 14:55:17 +08:00
detail . value = await getPlatformPartner ( row . id )
2026-08-03 14:58:17 +08:00
} catch ( error ) {
const meta = getApiErrorDetails ( error , '往来单位详情加载失败' )
ElMessage . error ( meta . requestId ? ` ${ meta . message } ( requestId: ${ meta . requestId } ) ` : meta . message )
detailOpen . value = false
} finally {
detailLoading . value = false
}
}
function openImport () {
importOpen . value = true
}
2026-08-01 12:17:33 +08:00
async function savePartner () {
const valid = await formRef . value ? . validate (). catch (() => false )
if ( ! valid ) return
saving . value = true
try {
const payload = savePayload ()
if ( form . id ) {
2026-08-06 14:55:17 +08:00
await updatePlatformPartner ( form . id , payload )
2026-08-01 12:17:33 +08:00
ElMessage . success ( '往来单位已更新' )
} else {
2026-08-06 14:55:17 +08:00
await createPlatformPartner ( payload )
2026-08-01 12:17:33 +08:00
ElMessage . success ( '往来单位已新增' )
}
editorOpen . value = false
await loadPartners ()
} catch ( error ) {
const details = getApiErrorDetails ( error , '往来单位保存失败,请重试' )
ElMessage . error ( details . requestId ? ` ${ details . message } ( requestId: ${ details . requestId } ) ` : details . message )
} finally {
saving . value = false
}
}
async function toggleStatus ( row : PlatformPartnerResource ) {
const nextStatus : PlatformPartnerStatus = row . status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
const action = nextStatus === 'ACTIVE' ? '启用' : '停用'
try {
await ElMessageBox . confirm (
` ${ action } 后 ${ nextStatus === 'ACTIVE' ? '可继续用于新业务' : '不再用于新业务,历史数据不受影响' } 。` ,
` ${ action } “ ${ row . partnerName } ”` ,
{ type : nextStatus === 'ACTIVE' ? 'success' : 'warning' , confirmButtonText : action , cancelButtonText : '取消' },
)
} catch {
return
}
const id = String ( row . id )
statusLoadingIds . value = new Set ( statusLoadingIds . value ). add ( id )
try {
2026-08-06 14:55:17 +08:00
await changePlatformPartnerStatus ( row . id , nextStatus )
2026-08-01 12:17:33 +08:00
ElMessage . success ( `往来单位已 ${ action } ` )
await loadPartners ()
} catch ( error ) {
const details = getApiErrorDetails ( error , `往来单位 ${ action } 失败,请重试` )
ElMessage . error ( details . requestId ? ` ${ details . message } ( requestId: ${ details . requestId } ) ` : details . message )
} finally {
const next = new Set ( statusLoadingIds . value )
next . delete ( id )
statusLoadingIds . value = next
}
}
function formatUpdatedAt ( value ?: string | null ) {
return value ? value . replace ( 'T' , ' ' ). slice ( 0 , 16 ) : '-'
}
watch ([ page , pageSize ], () => { void loadPartners () })
2026-08-04 21:01:39 +08:00
onMounted (() => {
void loadPartners ()
2026-08-04 09:05:19 +08:00
})
2026-08-01 12:17:33 +08:00
</ script >
< template >
< div class = "business-page partners-page" >
2026-08-06 19:03:13 +08:00
< QueryToolbar v-model = "filters" class="surface-panel" ariaLabel="往来单位筛选" :fields="queryFields" preference-key="partners-query" @search="searchPartners" />
2026-08-01 12:17:33 +08:00
2026-08-04 21:01:39 +08:00
2026-08-01 12:17:33 +08:00
< section v-if = "loadError" class="surface-panel partners-error" role="alert" >
< div >
< strong > 往来单位列表暂时无法加载 </ strong >
< p >{{ loadError . message }}</ p >
< ApiErrorMeta :details = "loadError" />
</ div >
2026-08-04 09:05:19 +08:00
< el-button type = "primary" :loading = "loading" @click ="loadPartners()" > 重新加载 </ el -button >
2026-08-01 12:17:33 +08:00
</ section >
< section v-else class = "surface-panel table-panel" :aria-busy = "loading" >
2026-08-03 11:45:45 +08:00
< div class = "partners-table-header" >
< div class = "partners-table-heading" >
< h2 > 往来单位列表 </ h2 >
2026-08-06 14:55:17 +08:00
< span > 平台公共数据 · 共 {{ total }} 个往来单位 </ span >
2026-08-03 11:45:45 +08:00
</ div >
2026-08-03 14:58:17 +08:00
< div class = "partners-primary-actions" >
2026-08-04 21:01:39 +08:00
< el-button v-if = "access.can('platform:counterparty:import')" :disabled="loading" @click="openImport"><Upload :size="16" /> 批量导入 </ el-button >
< el-button v-if = "access.can('platform:counterparty:create')" type="primary" :disabled="loading" @click="openCreate"><Plus :size="16" /> 新增往来单位 </ el-button >
2026-08-03 14:58:17 +08:00
</ div >
2026-08-03 11:45:45 +08:00
</ div >
2026-08-01 12:17:33 +08:00
< DataTable
v -model:page = "page"
v -model:page-size = "pageSize"
:data = "rows"
:columns = "partnerColumns"
2026-08-06 19:03:13 +08:00
preference -key =" partners -table "
2026-08-01 12:17:33 +08:00
:loading = "loading"
:total = "total"
: page -sizes ="[ 20 , 50 , 100 ]"
aria -label =" 往来单位列表 "
row -key =" id "
empty -text =" 暂无符合条件的往来单位 "
>
< template # actions = "{ row }" >
2026-08-03 14:58:17 +08:00
< div class = "table-actions" >
< el-button link type = "primary" @click ="openDetail(row as PlatformPartnerResource)" >< Eye :size = "15" /> 详情 </ el-button >
2026-08-04 21:01:39 +08:00
< el-button v-if = "access.can('platform:counterparty:edit')" link type="primary" :disabled="loading" @click="openEdit(row as PlatformPartnerResource)"><Pencil :size="15" /> 编辑 </ el-button >
< el-button v-if = "access.can('platform:counterparty:status')" link :disabled="loading" :type="(row as PlatformPartnerResource).status === 'ACTIVE' ? 'danger' : 'success'" :loading="statusLoadingIds.has(String((row as PlatformPartnerResource).id))" @click="toggleStatus(row as PlatformPartnerResource)"><Power :size="15" />{{ ( row as PlatformPartnerResource ). status === 'ACTIVE' ? '停用' : '启用' }}</ el-button >
2026-08-01 12:17:33 +08:00
</ div >
</ template >
< template # partner = "{ row }" >< div class = "partner-cell" >< span >< Building2 :size = "18" /></ span >< div >< strong >{{ ( row as PlatformPartnerResource ). partnerName }}</ strong >< small >{{ ( row as PlatformPartnerResource ). partnerCode }}</ small ></ div ></ div ></ template >
2026-08-03 14:58:17 +08:00
< template # type = "{ row }" >< div class = "partner-types" >< el-tag v-for = "type in (row as PlatformPartnerResource).types" :key="type" effect="plain" >{{ partnerTypeLabel ( type ) }} </ el -tag ></ div ></ template >
2026-08-01 12:17:33 +08:00
< template # contact = "{ row }" >< strong >{{ ( row as PlatformPartnerResource ). contactName }}</ strong >< span class = "partner-subline" >{{ ( row as PlatformPartnerResource ). contactPhone }}</ span >< span v-if = "(row as PlatformPartnerResource).contactEmail" class="partner-subline" >{{ ( row as PlatformPartnerResource ) .contactEmail }}</ span ></ template >
< template # address = "{ row }" >< div class = "partner-address" >< MapPin :size = "15" />< span >{{ [( row as PlatformPartnerResource ). province , ( row as PlatformPartnerResource ). city , ( row as PlatformPartnerResource ). district , ( row as PlatformPartnerResource ). addressLine ]. filter ( Boolean ). join ( ' ' ) }}</ span ></ div ></ template >
< template # status = "{ row }" >< el-tag : type = "(row as PlatformPartnerResource).status === 'ACTIVE' ? 'success' : 'info'" effect = "light" >{{ ( row as PlatformPartnerResource ). status === 'ACTIVE' ? '启用' : '停用' }}</ el-tag ></ template >
2026-08-03 11:45:45 +08:00
< template # pagination -left />
2026-08-01 12:17:33 +08:00
</ DataTable >
</ section >
< el-drawer v-model = "editorOpen" :title="editorLabel" size="680px" class="business-drawer editor-drawer" :close-on-click-modal="false" >
< el-form ref = "formRef" :model = "form" :rules = "rules" label -position =" top " class = "drawer-form" :disabled = "saving" >
< div class = "form-section" >< h3 > 单位信息 </ h3 >< div class = "form-grid two-cols" >
< el-form-item label = "单位编码" prop = "partnerCode" required >< el-input v-model = "form.partnerCode" placeholder="如:CUS-001" maxlength="50" /></ el-form-item >
< el-form-item label = "单位名称" prop = "partnerName" required >< el-input v-model = "form.partnerName" placeholder="请输入工商登记或常用单位名称" maxlength="128" /></ el-form-item >
2026-08-03 14:58:17 +08:00
< el-form-item label = "单位类型" prop = "types" required >< el-select v-model = "form.types" multiple collapse-tags collapse-tags-tooltip placeholder="请选择一个或多个单位类型"><el-option v-for="option in typeOptions" :key="option.value" :label="option.label" :value="option.value" /></ el-select ></ el-form-item >
2026-08-01 12:17:33 +08:00
< el-form-item label = "状态" prop = "status" required >< el-radio-group v-model = "form.status"><el-radio-button value="ACTIVE">启用</el-radio-button><el-radio-button value="INACTIVE" > 停用 </ el -radio-button ></ el-radio-group ></ el-form-item >
</ div ></ div >
< div class = "form-section" >< h3 > 联系信息 </ h3 >< div class = "form-grid two-cols" >
< el-form-item label = "联系人" prop = "contactName" required >< el-input v-model = "form.contactName" placeholder="请输入主要联系人姓名" maxlength="64" /></ el-form-item >
< el-form-item label = "联系电话" prop = "contactPhone" required >< el-input v-model = "form.contactPhone" placeholder="请输入手机号或固定电话" maxlength="32" /></ el-form-item >
< el-form-item label = "联系邮箱" prop = "contactEmail" >< el-input v-model = "form.contactEmail" placeholder="如:contact@example.com" maxlength="128" /></ el-form-item >
</ div ></ div >
< div class = "form-section" >< h3 > 地址信息 </ h3 >< div class = "form-grid two-cols" >
< el-form-item label = "国家二字码" prop = "countryCode" required >< el-input v-model = "form.countryCode" placeholder="如:CN" maxlength="2" @input="form.countryCode = String($event).toUpperCase()" /></ el-form-item >
< el-form-item label = "省 / 州" prop = "province" >< el-input v-model = "form.province" placeholder="如:广东省" maxlength="64" /></ el-form-item >
< el-form-item label = "城市" prop = "city" required >< el-input v-model = "form.city" placeholder="如:深圳市" maxlength="64" /></ el-form-item >
< el-form-item label = "区 / 县" prop = "district" >< el-input v-model = "form.district" placeholder="如:宝安区" maxlength="64" /></ el-form-item >
< el-form-item label = "邮政编码" prop = "postalCode" >< el-input v-model = "form.postalCode" placeholder="如:518000" maxlength="20" /></ el-form-item >
</ div >< el-form-item label = "详细地址" prop = "addressLine" required >< el-input v-model = "form.addressLine" type="textarea" :rows="3" placeholder="请输入街道、园区、楼栋及门牌信息" maxlength="255" show -word-limit /></ el-form-item ></ div >
2026-08-03 14:58:17 +08:00
< div class = "form-section" >< h3 > 其他信息 </ h3 >< el-form-item label = "备注" prop = "remark" >< el-input v-model = "form.remark" type="textarea" :rows="3" placeholder="请输入内部说明(选填)" maxlength="500" show -word-limit /></ el-form-item ></ div >
2026-08-01 12:17:33 +08:00
</ el-form >
< template # footer >< div class = "drawer-footer" >< el-button :disabled = "saving" @click ="editorOpen = false" > 取消 </ el -button >< el-button type = "primary" :loading = "saving" @click ="savePartner" > 保存往来单位 </ el -button ></ div ></ template >
</ el-drawer >
2026-08-03 14:58:17 +08:00
< el-drawer v-model = "detailOpen" title="往来单位详情" size="680px" class="business-drawer detail-drawer" >
< div v-loading = "detailLoading" class="partner-detail" >
< template v-if = "detail" >
< section >< h3 > 单位信息 </ h3 >< dl class = "detail-grid" >
< div >< dt > 单位编码 </ dt >< dd >{{ detail . partnerCode }}</ dd ></ div >
< div >< dt > 单位名称 </ dt >< dd >{{ detail . partnerName }}</ dd ></ div >
< div >< dt > 状态 </ dt >< dd >< el-tag : type = "detail.status === 'ACTIVE' ? 'success' : 'info'" >{{ detail . status === 'ACTIVE' ? '启用' : '停用' }}</ el-tag ></ dd ></ div >
< div >< dt > 单位类型 </ dt >< dd class = "partner-types" >< el-tag v-for = "type in detail.types" :key="type" effect="plain" >{{ partnerTypeLabel ( type ) }} </ el -tag ></ dd ></ div >
</ dl ></ section >
< section >< h3 > 联系与地址 </ h3 >< dl class = "detail-grid" >
< div >< dt > 联系人 </ dt >< dd >{{ detail . contactName }}</ dd ></ div >< div >< dt > 联系电话 </ dt >< dd >{{ detail . contactPhone }}</ dd ></ div >
< div >< dt > 联系邮箱 </ dt >< dd >{{ detail . contactEmail || '-' }}</ dd ></ div >< div >< dt > 邮政编码 </ dt >< dd >{{ detail . postalCode || '-' }}</ dd ></ div >
< div class = "detail-span" >< dt > 详细地址 </ dt >< dd >{{ [ detail . countryCode , detail . province , detail . city , detail . district , detail . addressLine ]. filter ( Boolean ). join ( ' ' ) }}</ dd ></ div >
</ dl ></ section >
< section >< h3 > 其他信息 </ h3 >< dl class = "detail-grid" >
2026-08-06 14:55:17 +08:00
< div class = "detail-span" >< dt > 更新时间 </ dt >< dd >{{ formatUpdatedAt ( detail . updatedAt ) }}</ dd ></ div >
2026-08-03 14:58:17 +08:00
< div class = "detail-span" >< dt > 备注 </ dt >< dd >{{ detail . remark || '-' }}</ dd ></ div >
</ dl ></ section >
</ template >
</ div >
</ el-drawer >
2026-08-06 15:32:02 +08:00
< BatchImportDialog
v-model = "importOpen"
title = "批量导入往来单位"
entity -label =" 往来单位 "
template -file-name =" 往来单位导入模板 -v1 .xlsx "
:download-template = "downloadPlatformPartnerImportTemplate"
:import-file = "importPlatformPartners"
@success ="loadPartners"
/>
2026-08-01 12:17:33 +08:00
</ div >
</ template >
< style scoped >
. partners - error { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 18 px 20 px ; border - color : # fecdca ; background : # fffafa ; }
. partners - error strong { color : # 7 a271a ; font - size : 13 px ; }
. partners - error p { margin : 4 px 0 0 ; color : # b54708 ; font - size : 12 px ; overflow - wrap : anywhere ; }
2026-08-03 11:45:45 +08:00
. partners - table - header { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 16 px 18 px ; border - bottom : 1 px solid var ( -- cc - border ); }
. partners - table - heading { display : flex ; min - width : 0 ; align - items : baseline ; gap : 10 px ; }
. partners - table - heading h2 { margin : 0 ; color : var ( -- cc - text ); font - size : 16 px ; line - height : 1.4 ; }
. partners - table - heading span { color : var ( -- cc - muted ); font - size : 12 px ; white - space : nowrap ; }
2026-08-03 14:58:17 +08:00
. partners - primary - actions , . partner - types { display : flex ; flex - wrap : wrap ; gap : 8 px ; }
2026-08-01 12:17:33 +08:00
. partner - cell { display : flex ; align - items : center ; gap : 10 px ; }
. partner - cell > span { display : grid ; width : 34 px ; height : 34 px ; flex : 0 0 34 px ; place - items : center ; border - radius : 7 px ; background : # eef3fa ; color : var ( -- cc - primary ); }
. partner - cell > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px ; }
. partner - cell strong { overflow - wrap : anywhere ; }
. partner - cell small , . partner - subline { display : block ; color : var ( -- cc - muted ); font - size : 11 px ; line - height : 1.45 ; overflow - wrap : anywhere ; }
. partner - address { display : flex ; align - items : flex - start ; gap : 7 px ; line - height : 1.55 ; overflow - wrap : anywhere ; }
. partner - address svg { flex : 0 0 15 px ; margin - top : 3 px ; color : var ( -- cc - muted ); }
2026-08-06 14:55:17 +08:00
. partner - detail { min - height : 180 px ; padding : 20 px 24 px 28 px ; box - sizing : border - box ; }
. partner - detail section + section { margin - top : 28 px ; }
2026-08-03 14:58:17 +08:00
. partner - detail h3 { margin : 0 0 12 px ; color : var ( -- cc - text ); font - size : 15 px ; }
. detail - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 1 px ; margin : 0 ; overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : var ( -- cc - border ); }
. detail - grid > div { min - width : 0 ; padding : 12 px 14 px ; background : # fff ; }
. detail - grid . detail - span { grid - column : 1 / - 1 ; }
. detail - grid dt { margin - bottom : 5 px ; color : var ( -- cc - muted ); font - size : 12 px ; }
. detail - grid dd { margin : 0 ; color : var ( -- cc - text ); line - height : 1.55 ; overflow - wrap : anywhere ; }
2026-08-01 12:17:33 +08:00
@ media ( max - width : 720 px ) {
. partners - error { align - items : stretch ; flex - direction : column ; }
. partners - error . el - button { min - height : 44 px ; }
2026-08-03 11:45:45 +08:00
. partners - table - header { align - items : stretch ; flex - direction : column ; }
. partners - table - header . el - button { width : 100 % ; min - height : 40 px ; }
2026-08-03 14:58:17 +08:00
. partners - primary - actions { flex - direction : column ; }
2026-08-06 14:55:17 +08:00
. partner - detail { padding : 16 px ; }
2026-08-03 14:58:17 +08:00
. detail - grid { grid - template - columns : minmax ( 0 , 1 fr ); }
. detail - grid . detail - span { grid - column : auto ; }
2026-08-01 12:17:33 +08:00
}
</ style >