1153 lines
33 KiB
Vue
1153 lines
33 KiB
Vue
<!-- 渠道信息 — 只读查看(样式对齐采集协议单 ProtocolViewDialog) -->
|
||
<template>
|
||
<div class="channel-view-dialog-root">
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
:title="dialogTitle"
|
||
width="1100px"
|
||
class="dialog-form-layout channel-view-dialog"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
@closed="handleClosed"
|
||
>
|
||
<div class="channel-view-body" :class="{ 'is-sub-loading': loading && channel }">
|
||
<template v-if="channel">
|
||
<div v-loading="loading" class="detail-tabs">
|
||
<div
|
||
v-for="tab in viewTabs"
|
||
:key="tab.key"
|
||
class="detail-tab"
|
||
:class="{ active: activeTab === tab.key }"
|
||
@click="scrollToSection(tab.key)"
|
||
>
|
||
{{ tab.label }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kpi-strip">
|
||
<div class="kpi-card">
|
||
<div class="kpi-card__icon" aria-hidden="true">
|
||
<el-icon><OfficeBuilding /></el-icon>
|
||
</div>
|
||
<div class="kpi-card__main">
|
||
<div class="kpi-label">渠道名称</div>
|
||
<div class="kpi-value kpi-value--emphasis">{{ dash(channel.channelName) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-card__icon" aria-hidden="true">
|
||
<el-icon><CollectionTag /></el-icon>
|
||
</div>
|
||
<div class="kpi-card__main">
|
||
<div class="kpi-label">渠道类型</div>
|
||
<div class="kpi-value">
|
||
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
|
||
{{ channelTypeLabel(channel.channelType) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-card__icon" aria-hidden="true">
|
||
<el-icon><User /></el-icon>
|
||
</div>
|
||
<div class="kpi-card__main">
|
||
<div class="kpi-label">联系人</div>
|
||
<div class="kpi-value">{{ dash(channel.contactName) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-card__icon" aria-hidden="true">
|
||
<el-icon><Calendar /></el-icon>
|
||
</div>
|
||
<div class="kpi-card__main">
|
||
<div class="kpi-label">创建时间</div>
|
||
<div class="kpi-value">{{ dash(channel.createTime) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="view-section-basic" class="section-card section-card--white">
|
||
<div class="section-title"><h3>基础信息</h3></div>
|
||
<div class="data-grid data-grid--3">
|
||
<div class="data-item">
|
||
<div class="data-label">渠道名称</div>
|
||
<div class="data-value">{{ dash(channel.channelName) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">统一社会信用代码</div>
|
||
<div class="data-value">{{ dash(channel.creditCode) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">渠道类型</div>
|
||
<div class="data-value">
|
||
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
|
||
{{ channelTypeLabel(channel.channelType) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">联系人</div>
|
||
<div class="data-value">{{ dash(channel.contactName) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">联系电话</div>
|
||
<div class="data-value">{{ dash(channel.contactPhone) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">联系邮箱</div>
|
||
<div class="data-value">{{ dash(channel.contactEmail) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">创建时间</div>
|
||
<div class="data-value">{{ dash(channel.createTime) }}</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="protocolAttachmentFiles.length" class="attachment-row">
|
||
<div class="data-label">渠道协议附件</div>
|
||
<ProtocolAttachmentReadonly
|
||
:files="protocolAttachmentFiles"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
<div v-if="channel.remark" class="remark-row">
|
||
<div class="data-item data-item--full">
|
||
<div class="data-label">备注</div>
|
||
<div class="data-value data-value--muted">{{ dash(channel.remark) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="view-section-invoice" class="section-card section-card--green">
|
||
<div class="section-title"><h3>开票信息</h3></div>
|
||
<div v-if="invoiceList.length" class="sub-cards">
|
||
<div v-for="(row, idx) in invoiceList" :key="row.id || idx" class="sub-card">
|
||
<div class="sub-card-header">
|
||
<h4>{{ row.companyName || '未命名' }}</h4>
|
||
</div>
|
||
<div class="data-grid data-grid--2">
|
||
<div v-if="row.taxNo" class="data-item">
|
||
<div class="data-label">税号</div>
|
||
<div class="data-value">{{ dash(row.taxNo) }}</div>
|
||
</div>
|
||
<div v-if="row.phone || row.companyPhone" class="data-item">
|
||
<div class="data-label">电话</div>
|
||
<div class="data-value">{{ dash(row.phone || row.companyPhone) }}</div>
|
||
</div>
|
||
<div v-if="row.companyAddress" class="data-item data-item--full">
|
||
<div class="data-label">单位地址</div>
|
||
<div class="data-value data-value--muted">{{ dash(row.companyAddress) }}</div>
|
||
</div>
|
||
<div v-if="row.bankName" class="data-item">
|
||
<div class="data-label">开户银行</div>
|
||
<div class="data-value">{{ dash(row.bankName) }}</div>
|
||
</div>
|
||
<div v-if="row.bankAccount" class="data-item">
|
||
<div class="data-label">银行账号</div>
|
||
<div class="data-value">{{ dash(row.bankAccount) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无开票信息" :image-size="48" />
|
||
</div>
|
||
|
||
<div id="view-section-mail" class="section-card section-card--white">
|
||
<div class="section-title"><h3>邮寄地址</h3></div>
|
||
<div v-if="mailList.length" class="sub-cards">
|
||
<div v-for="(row, idx) in mailList" :key="row.id || idx" class="sub-card">
|
||
<div class="sub-card-header">
|
||
<h4>{{ row.mailAddress || '未命名' }}</h4>
|
||
</div>
|
||
<div class="data-grid data-grid--2">
|
||
<div v-if="row.mailContactName || row.mailContact" class="data-item">
|
||
<div class="data-label">联系人</div>
|
||
<div class="data-value">{{ dash(row.mailContactName || row.mailContact) }}</div>
|
||
</div>
|
||
<div v-if="row.mailContactPhone || row.mailPhone" class="data-item">
|
||
<div class="data-label">联系电话</div>
|
||
<div class="data-value">{{ dash(row.mailContactPhone || row.mailPhone) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无邮寄地址" :image-size="48" />
|
||
</div>
|
||
|
||
<div id="view-section-account" class="section-card section-card--white">
|
||
<div class="section-title"><h3>账户信息</h3></div>
|
||
<div class="account-balance-card">
|
||
<div class="account-balance-card__label">账户余额</div>
|
||
<div class="account-balance-card__value">
|
||
¥ {{ accountLoading ? '—' : formatAmount(accountData?.balanceAmount) }}
|
||
</div>
|
||
</div>
|
||
<div class="account-quotas-head">
|
||
<h4 class="account-quotas-title">产品份数</h4>
|
||
</div>
|
||
<div v-if="quotasLoading" v-loading="true" style="min-height: 120px"></div>
|
||
<div v-else-if="productQuotas.length" class="list-body list-wrap account-quota-list-wrap">
|
||
<el-table
|
||
:data="productQuotas"
|
||
class="list-table account-quota-table"
|
||
stripe
|
||
row-key="id"
|
||
max-height="360"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="projectName"
|
||
label="项目名称"
|
||
min-width="150"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="cellTypeName"
|
||
label="细胞类型"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="specText"
|
||
label="规格"
|
||
min-width="150"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="totalQty"
|
||
label="总份数"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="usedQty"
|
||
label="已用份数"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="remainingQty"
|
||
label="剩余份数"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
</el-table>
|
||
</div>
|
||
<el-empty v-else description="暂无产品份数数据" :image-size="48" />
|
||
</div>
|
||
|
||
<div id="view-section-agreement" class="section-card">
|
||
<div class="section-title"><h3>协议信息</h3></div>
|
||
<div v-if="agreementList.length" class="agreement-cards">
|
||
<div
|
||
v-for="(agr, idx) in agreementList"
|
||
:key="agr.id || idx"
|
||
class="agreement-card"
|
||
>
|
||
<div class="agreement-card__header">
|
||
<div class="agreement-card__tags">
|
||
<el-tag :type="agreementTypeTagType(agr.agreementType)" size="small">
|
||
{{ agreementTypeLabel(agr.agreementType) }}
|
||
</el-tag>
|
||
<el-tag :type="agreementStatusTagType(agr.status)" size="small">
|
||
{{ agreementStatusLabel(agr.status) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<div class="data-grid data-grid--4 agreement-card__info">
|
||
<div v-if="agr.settlementType" class="data-item">
|
||
<div class="data-label">结算方式</div>
|
||
<div class="data-value">
|
||
<el-tag :type="settlementTypeTagType(agr.settlementType)" size="small">
|
||
{{ settlementTypeLabel(agr.settlementType) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<div v-if="agr.creditLimit != null" class="data-item">
|
||
<div class="data-label">授信额度(元)</div>
|
||
<div class="data-value data-value--price">{{ dash(agr.creditLimit) }}</div>
|
||
</div>
|
||
<div v-if="agr.effectiveDate" class="data-item">
|
||
<div class="data-label">生效日期</div>
|
||
<div class="data-value">{{ dash(agr.effectiveDate) }}</div>
|
||
</div>
|
||
<div v-if="agr.expiryDate" class="data-item">
|
||
<div class="data-label">到期日期</div>
|
||
<div class="data-value">{{ dash(agr.expiryDate) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="agreement-card__products">
|
||
<div class="agreement-products-head">
|
||
<h4 class="agreement-products-title">细胞产品信息</h4>
|
||
</div>
|
||
<div
|
||
v-if="(agreementPricesMap[agr.id] || []).length > 0"
|
||
class="list-body list-wrap agreement-price-list-wrap"
|
||
>
|
||
<el-table
|
||
:data="agreementPricesMap[agr.id] || []"
|
||
class="list-table agreement-price-table"
|
||
stripe
|
||
row-key="id"
|
||
max-height="360"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="projectName"
|
||
label="项目名称"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="projectCode"
|
||
label="项目编码"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="cellTypeName"
|
||
label="细胞类型"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="specText"
|
||
label="规格"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
label="原价(元)"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row: priceRow }">
|
||
{{ formatPrice(priceRow.originalPrice) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="协议价(元)"
|
||
min-width="100"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row: priceRow }">
|
||
<span class="data-value--price">{{ formatPrice(priceRow.agreementPrice) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="remark"
|
||
label="备注"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
</el-table>
|
||
</div>
|
||
<el-empty v-else description="暂无细胞产品信息" :image-size="48" class="agreement-price-empty" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无协议信息" :image-size="48" />
|
||
</div>
|
||
|
||
</template>
|
||
<div v-else v-loading="true" style="min-height: 200px"></div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
|
||
<el-icon><Close /></el-icon>
|
||
关闭
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="previewVisible" title="文件预览" width="800px" align-center destroy-on-close>
|
||
<div v-if="previewType === 'image'" class="preview-container">
|
||
<img
|
||
:src="previewUrl"
|
||
alt="预览"
|
||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||
/>
|
||
</div>
|
||
<div v-else class="preview-container">
|
||
<video
|
||
:src="previewUrl"
|
||
controls
|
||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, ref, watch } from 'vue'
|
||
import { Calendar, Close, CollectionTag, OfficeBuilding, User } from '@element-plus/icons-vue'
|
||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||
import {
|
||
channelTypeLabel,
|
||
channelTypeTagType,
|
||
agreementTypeLabel,
|
||
agreementTypeTagType,
|
||
agreementStatusLabel,
|
||
agreementStatusTagType,
|
||
settlementTypeLabel,
|
||
settlementTypeTagType
|
||
} from '@/utils/status-tag-registry'
|
||
import {
|
||
fetchChannelInvoices,
|
||
fetchChannelMailAddresses,
|
||
fetchChannelAgreements,
|
||
fetchChannelAgreementPrices,
|
||
fetchChannelAccount,
|
||
fetchChannelProductQuotas
|
||
} from '@/api/admin'
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
channelId: string | null
|
||
listRow?: Record<string, any> | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', val: boolean): void
|
||
}>()
|
||
|
||
const viewTabs = [
|
||
{ key: 'basic', label: '基础信息' },
|
||
{ key: 'invoice', label: '开票信息' },
|
||
{ key: 'mail', label: '邮寄地址' },
|
||
{ key: 'agreement', label: '协议信息' },
|
||
{ key: 'account', label: '账户信息' }
|
||
] as const
|
||
|
||
const loading = ref(false)
|
||
const activeTab = ref('basic')
|
||
const invoiceList = ref<any[]>([])
|
||
const mailList = ref<any[]>([])
|
||
const agreementList = ref<any[]>([])
|
||
const agreementPricesMap = ref<Record<string, any[]>>({})
|
||
const accountData = ref<{ balanceAmount: number } | null>(null)
|
||
const productQuotas = ref<any[]>([])
|
||
const accountLoading = ref(false)
|
||
const quotasLoading = ref(false)
|
||
const accountLoaded = ref(false)
|
||
|
||
const channel = computed(() => props.listRow || null)
|
||
|
||
const dialogTitle = computed(() => {
|
||
const name = channel.value?.channelName
|
||
return name ? `查看渠道 — ${name}` : '查看渠道'
|
||
})
|
||
|
||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||
|
||
const protocolAttachmentFiles = computed(() => {
|
||
const row = channel.value
|
||
if (!row) return []
|
||
if (Array.isArray(row.attachmentFiles) && row.attachmentFiles.length) {
|
||
return row.attachmentFiles as Array<Record<string, any>>
|
||
}
|
||
const urls = String(row.protocolAttachments || '').trim()
|
||
if (!urls) return []
|
||
return urls
|
||
.split(',')
|
||
.map((u, i) => {
|
||
const url = u.trim()
|
||
if (!url) return null
|
||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||
return { id: `att_${i}`, fileUrl: url, fileName: name, fileSize: 0 }
|
||
})
|
||
.filter(Boolean) as Array<Record<string, any>>
|
||
})
|
||
|
||
function dash(val: unknown) {
|
||
if (val === null || val === undefined || val === '') return '—'
|
||
return String(val)
|
||
}
|
||
|
||
function formatPrice(val: unknown) {
|
||
if (val === null || val === undefined || val === '') return '—'
|
||
return String(val)
|
||
}
|
||
|
||
function normalizeAgreementRow(row: Record<string, any>) {
|
||
const status = String(row.agreementStatus ?? row.status ?? 'dsx').trim() || 'dsx'
|
||
return {
|
||
...row,
|
||
settlementType: row.settleMode ?? row.settlementType ?? '',
|
||
creditLimit: row.creditLimitAmount ?? row.creditLimit ?? null,
|
||
expiryDate: row.expireDate ?? row.expiryDate ?? '',
|
||
effectiveDate: row.effectiveDate ?? '',
|
||
status
|
||
}
|
||
}
|
||
|
||
function fullFileUrl(suffix: string) {
|
||
if (!suffix) return ''
|
||
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/')) {
|
||
return suffix
|
||
}
|
||
return `${minioBaseUrl}/${suffix}`
|
||
}
|
||
|
||
function isVideoFile(file: { fileType?: string; fileName?: string }) {
|
||
const t = (file.fileType || '').toLowerCase()
|
||
if (t.startsWith('video/')) return true
|
||
return /\.(mp4|webm|ogg|mov)$/i.test(file.fileName || '')
|
||
}
|
||
|
||
const previewVisible = ref(false)
|
||
const previewUrl = ref('')
|
||
const previewType = ref<'image' | 'video'>('image')
|
||
|
||
function openPreview(file: Record<string, any>) {
|
||
if (!file.fileUrl) return
|
||
previewUrl.value = fullFileUrl(file.fileUrl)
|
||
previewType.value = isVideoFile(file) ? 'video' : 'image'
|
||
previewVisible.value = true
|
||
}
|
||
|
||
function downloadFile(file: Record<string, any>) {
|
||
downloadAttachmentFile(file, { fullFileUrl })
|
||
}
|
||
|
||
function formatAmount(val: unknown) {
|
||
if (val === null || val === undefined || val === '') return '0.00'
|
||
const num = Number(val)
|
||
if (isNaN(num)) return '0.00'
|
||
return num.toFixed(2)
|
||
}
|
||
|
||
async function loadAccountData() {
|
||
if (!props.channelId || accountLoaded.value) return
|
||
accountLoading.value = true
|
||
quotasLoading.value = true
|
||
try {
|
||
const [accRes, quotaRes] = await Promise.all([
|
||
fetchChannelAccount(props.channelId),
|
||
fetchChannelProductQuotas(props.channelId)
|
||
])
|
||
accountData.value = (accRes as any)?.data ?? accRes ?? null
|
||
const quotaRows = Array.isArray(quotaRes) ? quotaRes : (quotaRes as any)?.list || []
|
||
productQuotas.value = quotaRows
|
||
accountLoaded.value = true
|
||
} catch {
|
||
accountData.value = null
|
||
productQuotas.value = []
|
||
} finally {
|
||
accountLoading.value = false
|
||
quotasLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadSubTables() {
|
||
if (!props.channelId) {
|
||
invoiceList.value = []
|
||
mailList.value = []
|
||
agreementList.value = []
|
||
return
|
||
}
|
||
loading.value = true
|
||
try {
|
||
const [invRes, mailRes, agrRes] = await Promise.all([
|
||
fetchChannelInvoices(props.channelId),
|
||
fetchChannelMailAddresses(props.channelId),
|
||
fetchChannelAgreements(props.channelId)
|
||
])
|
||
const invRows = Array.isArray(invRes) ? invRes : (invRes as any)?.list || []
|
||
const mailRows = Array.isArray(mailRes) ? mailRes : (mailRes as any)?.list || []
|
||
invoiceList.value = invRows.map((row: any) => ({
|
||
...row,
|
||
phone: row.companyPhone ?? row.phone ?? '',
|
||
companyPhone: row.companyPhone ?? row.phone ?? ''
|
||
}))
|
||
mailList.value = mailRows.map((row: any) => ({
|
||
...row,
|
||
mailContactName: row.mailContact ?? row.mailContactName ?? '',
|
||
mailContactPhone: row.mailPhone ?? row.mailContactPhone ?? ''
|
||
}))
|
||
const agrRows = Array.isArray(agrRes) ? agrRes : (agrRes as any)?.list || []
|
||
agreementList.value = agrRows.map((row: Record<string, any>) => normalizeAgreementRow(row))
|
||
const entries = await Promise.all(
|
||
agreementList.value.map(async (agr: any) => {
|
||
if (!agr.id) return [agr.id, []] as const
|
||
try {
|
||
const priceRes = await fetchChannelAgreementPrices(agr.id)
|
||
const prices = Array.isArray(priceRes)
|
||
? priceRes
|
||
: (priceRes as any)?.list || []
|
||
return [agr.id, prices] as const
|
||
} catch {
|
||
return [agr.id, []] as const
|
||
}
|
||
})
|
||
)
|
||
const map: Record<string, any[]> = {}
|
||
entries.forEach(([id, prices]) => {
|
||
if (id) map[id as string] = prices as any[]
|
||
})
|
||
agreementPricesMap.value = map
|
||
} catch {
|
||
invoiceList.value = []
|
||
mailList.value = []
|
||
agreementList.value = []
|
||
agreementPricesMap.value = {}
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function scrollToSection(key: string) {
|
||
activeTab.value = key
|
||
nextTick(() => {
|
||
document
|
||
.getElementById(`view-section-${key}`)
|
||
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||
})
|
||
}
|
||
|
||
function handleClosed() {
|
||
invoiceList.value = []
|
||
mailList.value = []
|
||
agreementList.value = []
|
||
agreementPricesMap.value = {}
|
||
accountData.value = null
|
||
productQuotas.value = []
|
||
accountLoaded.value = false
|
||
activeTab.value = 'basic'
|
||
}
|
||
|
||
watch(
|
||
() => [props.modelValue, props.channelId] as const,
|
||
([visible, id]) => {
|
||
if (visible && id) {
|
||
loadSubTables()
|
||
loadAccountData()
|
||
}
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.channel-view-body {
|
||
overflow: visible;
|
||
}
|
||
|
||
.detail-tabs {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1000;
|
||
display: flex;
|
||
gap: 4px;
|
||
background: #f0fdf4;
|
||
padding: 4px;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
width: fit-content;
|
||
isolation: isolate;
|
||
box-shadow: 0 1px 0 #f0fdf4;
|
||
}
|
||
|
||
.detail-tab {
|
||
padding: 10px 20px;
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
transition: all 0.3s;
|
||
|
||
&.active {
|
||
background: #fff;
|
||
color: #059669;
|
||
font-weight: 500;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
&:hover:not(.active) {
|
||
color: #059669;
|
||
}
|
||
}
|
||
|
||
/* KPI 摘要条(对齐 ProtocolViewDialog:统一薄荷绿通栏 + 图标徽章) */
|
||
.kpi-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 0;
|
||
margin-bottom: 24px;
|
||
padding: 6px;
|
||
background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 52%, #ecfdf5 100%);
|
||
border: 1px solid #a7f3d0;
|
||
border-radius: 12px;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.kpi-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 14px 18px;
|
||
background: transparent;
|
||
border-radius: 8px;
|
||
transition: background 0.2s;
|
||
|
||
&:not(:last-child) {
|
||
border-right: 1px solid rgba(167, 243, 208, 0.75);
|
||
}
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.55);
|
||
}
|
||
}
|
||
|
||
.kpi-card__icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
flex-shrink: 0;
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
border: 1px solid #a7f3d0;
|
||
color: #059669;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.kpi-card__main {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.kpi-label {
|
||
margin-bottom: 4px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #64748b;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #0f172a;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
|
||
&--emphasis {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #047857;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
}
|
||
|
||
.section-card {
|
||
border-radius: 12px;
|
||
padding: 22px 24px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||
transition:
|
||
transform 0.2s,
|
||
box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
&--white {
|
||
background: #fff;
|
||
}
|
||
|
||
&--green {
|
||
background: #f0fdf4;
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 18px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
|
||
&::before {
|
||
content: '';
|
||
width: 3px;
|
||
height: 18px;
|
||
background: #10b981;
|
||
border-radius: 2px;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
|
||
.data-grid {
|
||
display: grid;
|
||
gap: 14px 20px;
|
||
|
||
&--2 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
&--3 {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
&--4 {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.data-item {
|
||
min-width: 0;
|
||
|
||
&--full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.data-label {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.data-value {
|
||
font-size: 14px;
|
||
color: #1e293b;
|
||
font-weight: 500;
|
||
word-break: break-word;
|
||
|
||
&--muted {
|
||
color: #64748b;
|
||
font-weight: 400;
|
||
}
|
||
|
||
&--price {
|
||
color: #059669;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
.attachment-row {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.remark-row {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
.sub-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 14px;
|
||
}
|
||
|
||
.sub-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 16px 18px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
border: 1px solid rgba(0, 0, 0, 0.03);
|
||
transition:
|
||
transform 0.2s,
|
||
box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
|
||
}
|
||
}
|
||
|
||
.sub-card-header {
|
||
margin-bottom: 12px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
|
||
h4 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.agreement-cards {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.agreement-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
border: 1px solid #e2e8f0;
|
||
overflow: hidden;
|
||
transition:
|
||
border-color 0.2s,
|
||
box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
border-color: #a7f3d0;
|
||
box-shadow: 0 2px 12px rgba(16, 185, 129, 0.1);
|
||
}
|
||
}
|
||
|
||
.agreement-card__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 18px;
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.agreement-card__tags {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.agreement-card__info {
|
||
padding: 14px 18px;
|
||
}
|
||
|
||
.agreement-card__products {
|
||
padding: 14px 18px 16px;
|
||
border-top: 1px solid #f1f5f9;
|
||
background: #fafbfc;
|
||
}
|
||
|
||
.agreement-products-head {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.agreement-products-title {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
line-height: 1.4;
|
||
|
||
&::before {
|
||
content: '';
|
||
display: inline-block;
|
||
width: 3px;
|
||
height: 14px;
|
||
background: #10b981;
|
||
border-radius: 2px;
|
||
margin-right: 8px;
|
||
vertical-align: -2px;
|
||
}
|
||
}
|
||
|
||
.agreement-price-list-wrap {
|
||
margin-bottom: 0;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.agreement-price-empty {
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.agreement-price-table.list-table {
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
|
||
&::before,
|
||
&::after {
|
||
display: none !important;
|
||
}
|
||
|
||
:deep(.el-table__inner-wrapper) {
|
||
border: none !important;
|
||
|
||
&::before {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-right: none !important;
|
||
}
|
||
|
||
:deep(.el-table__body tr:last-child td.el-table__cell) {
|
||
border-bottom: none !important;
|
||
}
|
||
}
|
||
|
||
.preview-container {
|
||
text-align: center;
|
||
}
|
||
|
||
/* 账户信息 */
|
||
.account-balance-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 18px 22px;
|
||
background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 100%);
|
||
border: 1px solid #a7f3d0;
|
||
border-radius: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.account-balance-card__label {
|
||
font-size: 14px;
|
||
color: #64748b;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.account-balance-card__value {
|
||
font-size: 26px;
|
||
font-weight: 700;
|
||
color: #059669;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.account-quotas-head {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.account-quotas-title {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
line-height: 1.4;
|
||
|
||
&::before {
|
||
content: '';
|
||
display: inline-block;
|
||
width: 3px;
|
||
height: 14px;
|
||
background: #10b981;
|
||
border-radius: 2px;
|
||
margin-right: 8px;
|
||
vertical-align: -2px;
|
||
}
|
||
}
|
||
|
||
.account-quota-list-wrap {
|
||
margin-bottom: 0;
|
||
border-radius: 8px;
|
||
background: #fff;
|
||
}
|
||
|
||
.account-quota-table.list-table {
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
|
||
&::before,
|
||
&::after {
|
||
display: none !important;
|
||
}
|
||
|
||
:deep(.el-table__inner-wrapper) {
|
||
border: none !important;
|
||
|
||
&::before {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
:deep(.el-table__cell) {
|
||
border-right: none !important;
|
||
}
|
||
|
||
:deep(.el-table__body tr:last-child td.el-table__cell) {
|
||
border-bottom: none !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.kpi-strip {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.kpi-strip .kpi-card {
|
||
border-right: none !important;
|
||
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
|
||
|
||
&:nth-last-child(-n + 2) {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.data-grid--3,
|
||
.data-grid--4 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.sub-cards {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.kpi-strip {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.kpi-strip .kpi-card:nth-last-child(-n + 2) {
|
||
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
|
||
}
|
||
|
||
.kpi-strip .kpi-card:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
</style>
|