Files
admin-web/src/modules/master-data/counterparties/pages/PartnersView.vue
T
2026-10-21 13:22:42 +08:00

517 lines
25 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 '@/styles/adminWorkbench.css'
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Building2, FileUp, MapPin, Plus, Save } from '@lucide/vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import BatchImportDialog from '@/shared/components/BatchImportDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import FormSection from '@/shared/components/FormSection.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import LineClampText from '@/shared/components/LineClampText.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'
import {
changePlatformPartnerStatus,
createPlatformPartner,
downloadPlatformPartnerImportTemplate,
getPlatformPartner,
importPlatformPartners,
listPlatformPartners,
updatePlatformPartner,
type PlatformPartnerResource,
type PlatformPartnerCarrierCode,
type PlatformPartnerStatus,
type PlatformPartnerType,
type SavePlatformPartnerRequest,
} from '@/modules/master-data/counterparties/api/platformPartners'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const pageSize = ref(20)
const access = useAccessPreviewStore()
const loading = ref(false)
const saving = ref(false)
const editorOpen = ref(false)
const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref<PlatformPartnerResource | null>(null)
const importOpen = ref(false)
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>())
let loadSequence = 0
let detailSequence = 0
type PartnerFilters = QueryModel & {
keyword: string
type: PlatformPartnerType | ''
status: PlatformPartnerStatus | ''
}
const filters = ref<PartnerFilters>({
keyword: '',
type: '',
status: '',
})
const appliedFilters = ref<PartnerFilters>({ ...filters.value })
const queryFields = computed<QueryField[]>(() => [
{ key: 'keyword', type: 'input', label: '往来单位名称', placeholder: '请输入往来单位名称' },
{
key: 'type',
type: 'select',
label: '单位类型',
placeholder: '全部类型',
options: [
{ label: '船公司', value: 'CARRIER' },
{ label: '发货单位', value: 'SHIPPER' },
{ label: '收货单位', value: 'CONSIGNEE' },
],
},
{
key: 'status',
type: 'select',
label: '状态',
placeholder: '全部状态',
options: [
{ label: '启用', value: 'ACTIVE' },
{ label: '停用', value: 'INACTIVE' },
],
},
])
interface PartnerForm extends SavePlatformPartnerRequest {
id: string
}
const emptyForm = (): PartnerForm => ({
id: '',
partnerCode: '',
partnerName: '',
partnerType: 'SHIPPER',
types: ['SHIPPER'],
carrierCode: null,
contactName: '',
contactPhone: '',
contactEmail: null,
countryCode: 'CN',
province: null,
city: '',
district: null,
addressLine: '',
postalCode: null,
status: 'ACTIVE',
remark: null,
})
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' }],
types: [{ required: true, type: 'array', min: 1, message: '请至少选择一个单位类型', trigger: 'change' }],
carrierCode: [{ validator: (_rule, value, callback) => form.types.includes('CARRIER') && !value ? callback(new Error('请选择船司标准编码')) : callback(), trigger: 'change' }],
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> = {
CARRIER: '船公司',
SHIPPER: '发货单位',
CONSIGNEE: '收货单位',
}
const typeOptions = Object.entries(typeLabels).map(([value, label]) => ({ value: value as PlatformPartnerType, label }))
const carrierCodeLabels: Record<PlatformPartnerCarrierCode, string> = {
MATSON: '美森', SML: 'SML', ZIM: '以星', COSCO: '中远', EMC: '长荣', HEDE: '合德', OTHER: '其他',
}
const carrierCodeOptions = (Object.entries(carrierCodeLabels) as [PlatformPartnerCarrierCode, string][]).map(([value, label]) => ({ value, label: `${label} (${value})` }))
function formatCarrierCode(value?: PlatformPartnerCarrierCode | null) { return value ? `${carrierCodeLabels[value]} (${value})` : '—' }
function partnerTypeLabel(type: PlatformPartnerType) {
return typeLabels[type]
}
const partnerColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
{ key: 'partner', label: '单位名称', minWidth: 210, slotName: 'partner' },
{ key: 'partnerCode', prop: 'partnerCode', label: '单位编码', minWidth: 130, defaultVisible: false },
{ key: 'type', label: '类型', minWidth: 115, slotName: 'type' },
{ key: 'contact', prop: 'contactName', label: '联系人', minWidth: 130 },
{ key: 'contactPhone', prop: 'contactPhone', label: '电话', minWidth: 145 },
{ key: 'contactEmail', prop: 'contactEmail', label: '邮箱', minWidth: 190, defaultVisible: false, formatter: (row: PlatformPartnerResource) => row.contactEmail || '—' },
{ key: 'address', label: '地址', minWidth: 240, slotName: 'address', defaultVisible: false },
{ key: 'status', label: '状态', width: 90, slotName: 'status' },
{ key: 'updatedAt', prop: 'updatedAt', label: '更新时间', minWidth: 140, defaultVisible: false, formatter: (row: PlatformPartnerResource) => formatUpdatedAt(row.updatedAt) },
]
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,
}
}
async function loadPartners() {
const sequence = ++loadSequence
loading.value = true
loadError.value = null
try {
const result = await listPlatformPartners(queryParams())
if (sequence !== loadSequence) return
rows.value = result.records
total.value = result.total
} catch (error) {
if (sequence !== loadSequence) return
rows.value = []
total.value = 0
loadError.value = getApiErrorDetails(error, '往来单位加载失败,请稍后重试')
} finally {
if (sequence === loadSequence) loading.value = false
}
}
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,
partnerType: row.types[0],
types: [...row.types],
carrierCode: row.carrierCode,
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,
remark: row.remark,
})
captureBaseline()
editorOpen.value = true
requestAnimationFrame(() => formRef.value?.clearValidate())
}
function savePayload(): SavePlatformPartnerRequest {
const types = [...form.types]
return {
partnerCode: form.partnerCode.trim(),
partnerName: form.partnerName.trim(),
partnerType: types[0],
types,
carrierCode: types.includes('CARRIER') ? form.carrierCode : null,
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,
remark: nullable(form.remark),
}
}
async function openDetail(row: PlatformPartnerResource) {
const sequence = ++detailSequence
detailOpen.value = true
detailLoading.value = true
detail.value = null
try {
const result = await getPlatformPartner(row.id)
if (sequence !== detailSequence || !detailOpen.value) return
detail.value = result
} catch (error) {
if (sequence !== detailSequence) return
const meta = getApiErrorDetails(error, '往来单位详情加载失败')
ElMessage.error(meta.requestId ? `${meta.message}(requestId: ${meta.requestId})` : meta.message)
detailOpen.value = false
} finally {
if (sequence === detailSequence) detailLoading.value = false
}
}
function openImport() {
importOpen.value = true
}
async function savePartner() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
saving.value = true
try {
const payload = savePayload()
if (form.id) {
await updatePlatformPartner(form.id, payload)
ElMessage.success('往来单位已更新')
} else {
await createPlatformPartner(payload)
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}”`,
{ customClass: 'admin-density-confirm admin-workbench', type: nextStatus === 'ACTIVE' ? 'success' : 'warning', confirmButtonText: action, cancelButtonText: '取消' },
)
} catch {
return
}
const id = String(row.id)
statusLoadingIds.value = new Set(statusLoadingIds.value).add(id)
try {
await changePlatformPartnerStatus(row.id, nextStatus)
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() })
watch(() => form.types.includes('CARRIER'), (carrier) => {
if (carrier) return
form.carrierCode = null
formRef.value?.clearValidate('carrierCode')
})
watch(detailOpen, (open) => {
if (open) return
detailSequence += 1
detailLoading.value = false
detail.value = null
})
onMounted(() => {
void loadPartners()
})
</script>
<template>
<AdminPage as="div" class="partners-page admin-density admin-workbench" spacing="normal">
<ListPageLayout aria-label="往来单位管理">
<section class="workbench-table is-page-surface" :aria-busy="loading"><DataTable appearance="integrated" column-settings-mode="show" :results-visible="!loadError"
v-model:page="page"
v-model:page-size="pageSize"
:data="rows"
:columns="partnerColumns"
preference-key="partners-table"
:loading="loading"
:total="total"
:page-sizes="[20, 50, 100]"
aria-label="往来单位列表"
row-key="id"
empty-text="暂无符合条件的往来单位"
>
<template #toolbar-start><h1>往来单位</h1></template>
<template #filters>
<QueryToolbar v-model="filters" ariaLabel="往来单位筛选" :fields="queryFields" preference-key="partners-query" @search="searchPartners" />
</template>
<template #notices><template v-if="loadError">
<LoadErrorState
title="往来单位列表暂时无法加载"
:message="loadError.message"
:loading="loading"
@retry="loadPartners()"
>
<template #details><ApiErrorMeta :details="loadError" /></template>
</LoadErrorState>
</template></template>
<template #toolbar-actions><div class="partners-primary-actions"><el-button v-if="access.can('platform:counterparty:import')" :disabled="loading" @click="openImport"><FileUp :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></template>
<template #actions="{ row }"><div class="table-actions density-actions row-actions admin-text-actions">
<el-button data-tone="neutral" class="density-action" size="small" @click="openDetail(row as PlatformPartnerResource)">详情</el-button>
<el-button data-tone="neutral" class="density-action" v-if="access.can('platform:counterparty:edit')" size="small" :disabled="loading" @click="openEdit(row as PlatformPartnerResource)">编辑</el-button>
<el-button class="density-action" v-if="access.can('platform:counterparty:status')" size="small" :disabled="loading" :data-tone="(row as PlatformPartnerResource).status === 'ACTIVE' ? 'danger' : 'success'" :loading="statusLoadingIds.has(String((row as PlatformPartnerResource).id))" @click="toggleStatus(row as PlatformPartnerResource)">{{ (row as PlatformPartnerResource).status === 'ACTIVE' ? '停用' : '启用' }}</el-button>
</div></template>
<template #partner="{ row }"><div class="partner-cell"><span><Building2 :size="18" /></span><div><LineClampText :content="(row as PlatformPartnerResource).partnerName" /></div></div></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 #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>
<template #pagination-left>平台公共数据 · 共 {{ total }} 个往来单位</template>
</DataTable></section>
</ListPageLayout>
<AppDrawer
v-model="editorOpen"
:title="editorLabel"
size="min(680px, 94vw)"
class="editor-drawer admin-density admin-density-drawer admin-workbench is-drawer"
:confirm-loading="saving"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="density-form" :disabled="saving">
<FormSection class="density-form-section" title="单位信息"><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>
<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>
<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>
<el-form-item v-if="form.types.includes('CARRIER')" label="船司标准编码" prop="carrierCode" required><el-select v-model="form.carrierCode" placeholder="请选择船司标准编码" style="width:100%"><el-option v-for="option in carrierCodeOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></el-form-item>
</div></FormSection>
<FormSection class="density-form-section" title="联系信息"><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></FormSection>
<FormSection class="density-form-section" title="地址信息"><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></FormSection>
<FormSection class="density-form-section" title="其他信息" :bordered="false"><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></FormSection>
</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"><Save :size="16" />保存往来单位</el-button>
</div>
</template>
</AppDrawer>
<AppDrawer v-model="detailOpen" title="往来单位详情" size="min(680px, 94vw)" class="detail-drawer admin-density admin-density-drawer admin-workbench is-drawer" :loading="detailLoading" :show-footer="false">
<div class="partner-detail">
<DetailPageLayout class="partner-detail-layout" v-if="detail" aria-label="往来单位详情">
<FormSection class="density-form-section" title="单位信息"><dl class="detail-grid density-fields">
<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>
<div v-if="detail.types.includes('CARRIER')"><dt>船司标准编码</dt><dd>{{ formatCarrierCode(detail.carrierCode) }}</dd></div>
</dl></FormSection>
<FormSection class="density-form-section" title="联系与地址"><dl class="detail-grid density-fields">
<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></FormSection>
<FormSection class="density-form-section" title="其他信息" :bordered="false"><dl class="detail-grid density-fields">
<div class="detail-span"><dt>更新时间</dt><dd>{{ formatUpdatedAt(detail.updatedAt) }}</dd></div>
<div class="detail-span"><dt>备注</dt><dd>{{ detail.remark || '-' }}</dd></div>
</dl></FormSection>
</DetailPageLayout>
</div>
</AppDrawer>
<BatchImportDialog class="admin-density-dialog admin-workbench is-dialog"
v-model="importOpen"
title="批量导入往来单位"
entity-label="往来单位"
template-file-name="往来单位导入模板-v1.xlsx"
:download-template="downloadPlatformPartnerImportTemplate"
:import-file="importPlatformPartners"
@success="loadPartners"
/>
</AdminPage>
</template>
<style scoped>
.partner-detail-layout { gap: 0; }
.partners-page { min-width: 0; }
.partners-primary-actions, .partner-types { display: flex; flex-wrap: wrap; gap: var(--admin-space-3); }
.partner-cell { display: flex; align-items: center; gap: var(--admin-space-4); }
.partner-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: var(--admin-radius); background: var(--workbench-surface-soft); color: var(--workbench-primary); }
.partner-cell > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.partner-cell :deep(.line-clamp-text) { width: 100%; }
.partner-address { display: flex; align-items: flex-start; gap: 7px; line-height: 1.55; overflow-wrap: anywhere; }
.partner-address svg { flex: 0 0 15px; margin-top: 3px; color: var(--workbench-muted); }
.partner-detail { min-width: 0; }
.partner-detail .detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.detail-grid .detail-span { grid-column: 1 / -1; }
@media (max-width: 720px) {
.partners-primary-actions :deep(.el-button) { width: 100%; min-height: 40px; }
.partners-primary-actions { flex-direction: column; }
.partner-detail { padding: 16px; }
.detail-grid { grid-template-columns: minmax(0, 1fr); }
.detail-grid .detail-span { grid-column: auto; }
}
</style>