816 lines
23 KiB
Vue
816 lines
23 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 v-loading="loading" class="channel-view-body">
|
||
<template v-if="channel">
|
||
<div 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-label">渠道名称</div>
|
||
<div class="kpi-value">{{ dash(channel.channelName) }}</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">渠道类型</div>
|
||
<div class="kpi-value">
|
||
<span class="badge" :class="channelTypeBadgeClass(channel.channelType)">
|
||
{{ channelTypeLabel(channel.channelType) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">联系人</div>
|
||
<div class="kpi-value">{{ dash(channel.contactName) }}</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">创建时间</div>
|
||
<div class="kpi-value">{{ dash(channel.createTime) }}</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">
|
||
<span class="badge" :class="channelTypeBadgeClass(channel.channelType)">
|
||
{{ channelTypeLabel(channel.channelType) }}
|
||
</span>
|
||
</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-agreement" class="section-card section-card--gray">
|
||
<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">
|
||
<span class="badge" :class="agreementTypeBadgeClass(agr.agreementType)">
|
||
{{ agreementTypeLabel(agr.agreementType) }}
|
||
</span>
|
||
<span class="badge" :class="agreementStatusBadgeClass(agr.status)">
|
||
{{ agreementStatusLabel(agr.status) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="data-grid data-grid--4">
|
||
<div v-if="agr.settlementType" class="data-item">
|
||
<div class="data-label">结算方式</div>
|
||
<div class="data-value">
|
||
<span class="badge" :class="settlementTypeBadgeClass(agr.settlementType)">
|
||
{{ settlementTypeLabel(agr.settlementType) }}
|
||
</span>
|
||
</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>
|
||
</div>
|
||
<el-empty v-else description="暂无协议信息" :image-size="48" />
|
||
</div>
|
||
</template>
|
||
</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 { Close } from '@element-plus/icons-vue'
|
||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||
import {
|
||
fetchChannelInvoices,
|
||
fetchChannelMailAddresses,
|
||
fetchChannelAgreements
|
||
} 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: '协议信息' }
|
||
] as const
|
||
|
||
const channelTypeMap: Record<string, string> = {
|
||
yy: '医院',
|
||
hzjg: '合作机构',
|
||
zs: '诊所',
|
||
zfjg: '政府机构',
|
||
qt: '其他'
|
||
}
|
||
|
||
const agreementTypeMap: Record<string, string> = {
|
||
kyxy: '框架协议',
|
||
zsxy: '中试协议',
|
||
lcxy: '临床协议'
|
||
}
|
||
|
||
const settlementTypeMap: Record<string, string> = {
|
||
yj: '月结',
|
||
jj: '季结',
|
||
nj: '年结',
|
||
dcjs: '单次结算'
|
||
}
|
||
|
||
const agreementStatusMap: Record<string, string> = {
|
||
dsx: '待生效',
|
||
sxz: '生效中',
|
||
yzf: '已作废'
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const activeTab = ref('basic')
|
||
const invoiceList = ref<any[]>([])
|
||
const mailList = ref<any[]>([])
|
||
const agreementList = ref<any[]>([])
|
||
|
||
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 urls = String(channel.value?.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 channelTypeLabel(type: string) {
|
||
const key = String(type || '').trim()
|
||
if (!key) return '—'
|
||
return channelTypeMap[key] || key
|
||
}
|
||
|
||
function channelTypeBadgeClass(type: string): string {
|
||
const m: Record<string, string> = {
|
||
yy: 'badge--blue',
|
||
hzjg: 'badge--green',
|
||
zs: 'badge--amber',
|
||
zfjg: 'badge--red',
|
||
qt: 'badge--gray'
|
||
}
|
||
return m[String(type || '')] || 'badge--gray'
|
||
}
|
||
|
||
function agreementTypeLabel(type: string) {
|
||
return agreementTypeMap[String(type || '')] || dash(type)
|
||
}
|
||
|
||
function agreementTypeBadgeClass(type: string): string {
|
||
const m: Record<string, string> = {
|
||
kyxy: 'badge--blue',
|
||
zsxy: 'badge--green',
|
||
lcxy: 'badge--amber'
|
||
}
|
||
return m[String(type || '')] || 'badge--gray'
|
||
}
|
||
|
||
function settlementTypeLabel(type: string) {
|
||
return settlementTypeMap[String(type || '')] || dash(type)
|
||
}
|
||
|
||
function settlementTypeBadgeClass(type: string): string {
|
||
const m: Record<string, string> = {
|
||
yj: 'badge--gray',
|
||
jj: 'badge--green',
|
||
nj: 'badge--amber',
|
||
dcjs: 'badge--red'
|
||
}
|
||
return m[String(type || '')] || 'badge--gray'
|
||
}
|
||
|
||
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 agreementStatusLabel(status: string) {
|
||
return agreementStatusMap[String(status || '')] || dash(status)
|
||
}
|
||
|
||
function agreementStatusBadgeClass(status: string): string {
|
||
const m: Record<string, string> = {
|
||
dsx: 'badge--amber',
|
||
sxz: 'badge--green',
|
||
yzf: 'badge--red'
|
||
}
|
||
return m[String(status || '')] || 'badge--gray'
|
||
}
|
||
|
||
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>) {
|
||
const url = fullFileUrl(file.fileUrl)
|
||
if (!url) return
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = file.fileName || ''
|
||
a.target = '_blank'
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
document.body.removeChild(a)
|
||
}
|
||
|
||
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))
|
||
} catch {
|
||
invoiceList.value = []
|
||
mailList.value = []
|
||
agreementList.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 = []
|
||
activeTab.value = 'basic'
|
||
}
|
||
|
||
watch(
|
||
() => [props.modelValue, props.channelId] as const,
|
||
([visible, id]) => {
|
||
if (visible && id) loadSubTables()
|
||
}
|
||
)
|
||
</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-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.kpi-card {
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
padding: 18px 20px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||
border-left: 3px solid #10b981;
|
||
transition:
|
||
transform 0.2s,
|
||
box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.1);
|
||
}
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
margin-bottom: 6px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
&--gray {
|
||
background: #f8fafc;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 2px 10px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
line-height: 1.6;
|
||
|
||
&--green {
|
||
background: #ecfdf5;
|
||
color: #059669;
|
||
}
|
||
|
||
&--amber {
|
||
background: #fffbeb;
|
||
color: #d97706;
|
||
}
|
||
|
||
&--gray {
|
||
background: #f1f5f9;
|
||
color: #64748b;
|
||
}
|
||
|
||
&--red {
|
||
background: #fef2f2;
|
||
color: #dc2626;
|
||
}
|
||
|
||
&--blue {
|
||
background: #eff6ff;
|
||
color: #2563eb;
|
||
}
|
||
}
|
||
|
||
.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 .data-grid {
|
||
padding: 14px 18px;
|
||
}
|
||
|
||
.preview-container {
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.kpi-strip {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.data-grid--3,
|
||
.data-grid--4 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.sub-cards {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|