@@ -1,5 +1,5 @@
< script setup lang = "ts" >
import { computed , reactive , ref , watch } from 'vue'
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox , type FormInstance , type FormRules , type UploadFile } from 'element-plus'
import { Building2 , Download , Eye , MapPin , Pencil , Plus , Power , Upload } from '@lucide/vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -23,7 +23,7 @@ import {
type SavePlatformPartnerRequest ,
} from '@/modules/master-data/counterparties/api/platformPartners'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { PlatformTenantSelector } from '@/modules/iam'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
const pageSize = ref ( 20 )
const access = useAccessPreviewStore ( )
@@ -42,25 +42,36 @@ const formRef = ref<FormInstance>()
const rows = ref < PlatformPartnerResource [ ] > ( [ ] )
const page = ref ( 1 )
const total = ref ( 0 )
const selectedTenantId = ref ( '' )
const loadedTenantId = ref < string | null > ( null )
const tenants = ref < PlatformTenantResource [ ] > ( [ ] )
const tenantLoadError = ref < ApiErrorDetails | null > ( null )
const editorTenantId = ref ( '' )
const loadError = ref < ApiErrorDetails | null > ( null )
const statusLoadingIds = ref ( new Set < string > ( ) )
let loadSequence = 0
type PartnerFilters = QueryModel & {
tenantId : string
keyword : string
type : PlatformPartnerType | ''
status : PlatformPartnerStatus | ''
}
const filters = ref < PartnerFilters > ( {
tenantId : '' ,
keyword : '' ,
type : '' ,
status : '' ,
} )
const appliedFilters = ref < PartnerFilters > ( { ... filters . value } )
const queryFields : QueryField [ ] = [
const queryFields = computed < QueryField [ ] > ( ( ) => [
{
key : 'tenantId' ,
type : 'select' ,
width : 180 ,
placeholder : tenantLoadError . value ? '企业列表加载失败' : '全部企业' ,
props : { 'aria-label' : '所属企业' , filterable : true , disabled : Boolean ( tenantLoadError . value ) } ,
options : tenants . value . map ( ( tenant ) => ( { value : String ( tenant . id ) , label : ` ${ tenant . tenantName } ( ${ tenant . tenantCode || tenant . id } ) ` } ) ) ,
} ,
{ key : 'keyword' , type : 'input' , placeholder : '单位名称、编码、联系人或电话' } ,
{
key : 'type' ,
@@ -84,7 +95,7 @@ const queryFields: QueryField[] = [
{ label : '停用' , value : 'INACTIVE' } ,
] ,
} ,
]
] )
interface PartnerForm extends SavePlatformPartnerRequest {
id : string
@@ -119,16 +130,12 @@ const { captureBaseline } = useUnsavedWorkRegistration({
requiredPermission : editorPermission ,
} )
const tenantReady = computed ( ( ) => Boolean ( selectedTenantId . value )
&& loadedTenantId . value === selectedTenantId . value
&& ! loading . value )
const tenantSwitchDisabled = computed ( ( ) => editorOpen . value
|| detailOpen . value
|| importOpen . value
|| saving . value
|| detailLoading . value
|| importing . value
|| statusLoadingIds . value . size > 0 )
const selectedEnterpriseId = computed ( ( ) => appliedFilters . value . tenantId )
const targetEnterpriseId = computed ( ( ) => filters . value . tenantId )
const selectedEnterpriseName = computed ( ( ) => {
if ( ! selectedEnterpriseId . value ) return '全部企业'
return tenants . value . find ( ( tenant ) => String ( tenant . id ) === selectedEnterpriseId . value ) ? . tenantName || ` 企业 ${ selectedEnterpriseId . value } `
} )
const rules : FormRules < PartnerForm > = {
partnerCode : [ { required : true , whitespace : true , message : '请输入往来单位编码' , trigger : 'blur' } ] ,
@@ -161,6 +168,7 @@ function partnerTypeLabel(type: PlatformPartnerLegacyType) {
const partnerColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 210 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'partner' , label : '往来单位' , minWidth : 210 , slotName : 'partner' } ,
{ key : 'tenant' , label : '所属企业' , minWidth : 190 , showOverflowTooltip : true , slotName : 'tenant' } ,
{ key : 'type' , label : '类型' , minWidth : 115 , slotName : 'type' } ,
{ key : 'contact' , label : '联系人' , minWidth : 165 , slotName : 'contact' } ,
{ key : 'address' , label : '地址' , minWidth : 240 , slotName : 'address' } ,
@@ -188,30 +196,52 @@ function queryParams() {
}
}
function apiTenantId ( value : string ) {
function apiTenantId ( value : string | undefined ) {
if ( ! value ) return undefined
const numeric = Number ( value )
return Number . isSafeInteger ( numeric ) ? numeric : value
}
function requireCurrentTenant ( ) {
if ( ! tenantReady . value ) {
ElMessage . warning ( '往来单位数据尚未加载完成,请稍后重试' )
function requireSelectedEnterprise ( action : string ) {
if ( ! targetEnterpriseId . value ) {
ElMessage . warning ( ` 请先在筛选条件中选择所属企业,再 ${ action } ` )
return null
}
return apiTenantId ( selectedTenantId . value )
return apiTenantId ( targetEnterpriseId . value ) !
}
async function loadPartners ( tenantId = selectedTenantId . value ) {
if ( ! tenantId ) return
function tenantName ( tenantId : number | string ) {
return tenants . value . find ( ( tenant ) => String ( tenant . id ) === String ( tenantId ) ) ? . tenantName || ` 企业 ${ tenantId } `
}
async function loadTenants ( ) {
tenantLoadError . value = null
try {
const records : PlatformTenantResource [ ] = [ ]
let currentPage = 1
let pages = 1
do {
const result = await listPlatformTenants ( { page : currentPage , size : 200 } )
records . push ( ... result . records )
pages = Math . max ( 1 , result . pages )
currentPage += 1
} while ( currentPage <= pages )
tenants . value = records
} catch ( error ) {
tenants . value = [ ]
tenantLoadError . value = getApiErrorDetails ( error , '企业列表加载失败,暂时无法按所属企业筛选' )
}
}
async function loadPartners ( ) {
const sequence = ++ loadSequence
loading . value = true
loadError . value = null
try {
const result = await listPlatformPartners ( apiTenantId ( tenantId ) , queryParams ( ) )
const result = await listPlatformPartners ( apiTenantId ( appliedFilters . value . tenantId ) , queryParams ( ) )
if ( sequence !== loadSequence ) return
rows . value = result . records
total . value = result . total
loadedTenantId . value = tenantId
} catch ( error ) {
if ( sequence !== loadSequence ) return
rows . value = [ ]
@@ -224,6 +254,7 @@ async function loadPartners(tenantId = selectedTenantId.value) {
function searchPartners ( model : QueryModel ) {
appliedFilters . value = {
tenantId : String ( model . tenantId || '' ) ,
keyword : String ( model . keyword || '' ) ,
type : ( model . type || '' ) as PlatformPartnerType | '' ,
status : ( model . status || '' ) as PlatformPartnerStatus | '' ,
@@ -236,7 +267,9 @@ function searchPartners(model: QueryModel) {
}
function openCreate ( ) {
if ( ! requireCurrentTenant ( ) ) return
const tenantId = requireSelectedEnterprise ( '新增往来单位' )
if ( ! tenantId ) return
editorTenantId . value = String ( tenantId )
Object . assign ( form , emptyForm ( ) )
captureBaseline ( )
editorOpen . value = true
@@ -248,7 +281,7 @@ function nullable(value: string | null) {
}
function openEdit ( row : PlatformPartnerResource ) {
if ( ! requireCurrentTenant ( ) ) return
editorTenantId . value = String ( row . tenantId )
Object . assign ( form , {
id : String ( row . id ) ,
partnerCode : row . partnerCode ,
@@ -294,8 +327,7 @@ function savePayload(): SavePlatformPartnerRequest {
}
async function openDetail ( row : PlatformPartnerResource ) {
const tenantId = requireCurrentTenant ( )
if ( ! tenantId ) return
const tenantId = apiTenantId ( String ( row . tenantId ) ) !
detailOpen . value = true
detailLoading . value = true
detail . value = null
@@ -311,6 +343,7 @@ async function openDetail(row: PlatformPartnerResource) {
}
function openImport ( ) {
if ( ! requireSelectedEnterprise ( '批量导入' ) ) return
importFile . value = null
importResult . value = null
importOpen . value = true
@@ -343,7 +376,7 @@ async function downloadTemplate() {
}
async function submitImport ( ) {
const tenantId = requireCurrentTenant ( )
const tenantId = requireSelectedEnterprise ( '批量导入' )
if ( ! tenantId ) return
if ( ! importFile . value ) {
ElMessage . warning ( '请先选择 XLSX 文件' )
@@ -382,7 +415,7 @@ function downloadImportErrors() {
}
async function savePartner ( ) {
const tenantId = requireCurrentTenant ( )
const tenantId = form . id ? apiTenantId ( editorTenantId . value ) : requireSelectedEnterprise ( '新增往来单位' )
if ( ! tenantId ) return
const valid = await formRef . value ? . validate ( ) . catch ( ( ) => false )
if ( ! valid ) return
@@ -407,8 +440,7 @@ async function savePartner() {
}
async function toggleStatus ( row : PlatformPartnerResource ) {
const tenantId = requireCurrentTenant ( )
if ( ! tenantId ) return
const tenantId = apiTenantId ( String ( row . tenantId ) ) !
const nextStatus : PlatformPartnerStatus = row . status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
const action = nextStatus === 'ACTIVE' ? '启用' : '停用'
try {
@@ -441,26 +473,23 @@ function formatUpdatedAt(value?: string | null) {
}
watch ( [ page , pageSize ] , ( ) => { void loadPartners ( ) } )
watch ( selectedTenantId , ( tenantId ) => {
editorOpen . value = false
detailOpen . value = false
importOpen . value = false
detail . value = null
importFile . value = null
importResult . value = null
rows . value = [ ]
total . value = 0
loadedTenantId . value = null
page . value = 1
void loadPartners ( tenantId )
onMounted ( ( ) => {
void loadTenants ( )
void loadPartners ( )
} )
< / script >
< template >
< div class = "business-page partners-page" >
< PlatformTenantSelector v-model = "selectedTenantId" :disabled="tenantSwitchDisabled" / >
< QueryToolbar v-model = "filters" class="surface-panel" ariaLabel="往来单位筛选" :fields="queryFields" @search="searchPartners" / >
< el-alert v-if = "tenantLoadError" type="warning" :closable="false" show -icon >
< template # title > 所属企业列表加载失败 < / template >
< p > { { tenantLoadError . message } } ; 往来单位仍按全部企业展示 。 < / p >
< ApiErrorMeta :details = "tenantLoadError" / >
< el-button link type = "warning" @click ="loadTenants" > 重新加载企业列表 < / el -button >
< / el-alert >
< section v-if = "loadError" class="surface-panel partners-error" role="alert" >
< div >
< strong > 往来单位列表暂时无法加载 < / strong >
@@ -474,11 +503,11 @@ watch(selectedTenantId, (tenantId) => {
< div class = "partners-table-header" >
< div class = "partners-table-heading" >
< h2 > 往来单位列表 < / h2 >
< span > 租户 { { selectedTenantId || '—' } } · 共 { { total } } 个往来单位 < / span >
< span > { { selectedEnterpriseName } } · 共 { { total } } 个往来单位 < / span >
< / div >
< div class = "partners-primary-actions" >
< el-button v-if = "access.can('platform:counterparty:import')" :disabled="loading || !tenantReady " @click="openImport"><Upload :size="16" / > 批量导入 < / el-button >
< el-button v-if = "access.can('platform:counterparty:create')" type="primary" :disabled="loading || !tenantReady " @click="openCreate"><Plus :size="16" / > 新增往来单位 < / el-button >
< 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 >
< / div >
< / div >
< DataTable
@@ -496,11 +525,12 @@ watch(selectedTenantId, (tenantId) => {
< template # actions = "{ row }" >
< div class = "table-actions" >
< el-button link type = "primary" @click ="openDetail(row as PlatformPartnerResource)" > < Eye :size = "15" / > 详情 < / el-button >
< el-button v-if = "access.can('platform:counterparty:edit')" link type="primary" :disabled="!tenantReady " @click="openEdit(row as PlatformPartnerResource)"><Pencil :size="15" / > 编辑 < / el-button >
< el-button v-if = "access.can('platform:counterparty:status')" link :disabled="!tenantReady " :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 >
< 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 >
< / 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 >
< template # tenant = "{ row }" > < span > { { tenantName ( ( row as PlatformPartnerResource ) . tenantId ) } } < / span > < / template >
< 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 >
< 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 >