feat: MeetingDetail 参会人表改用 GrTable + 移除合计行
GrTable 增强 (sibling subagent 已完成重构): - mobile 模式从 el-table expand 行改为 el-descriptions 卡片列表 - 每行一张卡, 显示所有列 - 通过 extractDefaultSlot 抓取自定义 #default slot 的 vnode - renderTemplate 在 mobile 卡片里渲染原始 vnode (含 maskPhone/maskBankCard/ maskIdCard/openPreview/el-link/el-tag 等自定义 template) - 支持无 prop 自定义 template 列在 mobile 正确渲染 MeetingDetail 改动: - el-table → GrTable (show-summary 移除, 不要合计行) - 移除 attendeeSummary 函数 (合计不再需要) - 移除之前 mobile attendee-table 紧凑规则 (mobile 走卡片分支, 不需要) - v-bind $attrs 透传 v-loading/border/size 给桌面 el-table - 修复 GrTable 闭合标签 (</el-table> → </GrTable>) 桌面端 el-table 完整渲染不变 (sibling 完整保留透传逻辑) mobile 端: 每行一张 el-descriptions 卡, 27+ 列全部展示, 自定义 template 正常渲染
This commit is contained in:
@@ -18,30 +18,15 @@
|
|||||||
弃用 refer/gr-table.vue 的 Vue 2 Options API + componentOptions 私有 API.
|
弃用 refer/gr-table.vue 的 Vue 2 Options API + componentOptions 私有 API.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 桌面: el-table 完整渲染 -->
|
||||||
<el-table
|
<el-table
|
||||||
|
v-if="!isMobile"
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:data="data"
|
:data="data"
|
||||||
>
|
>
|
||||||
<!-- 移动端展开行: 只在 ≤breakpoint 渲染, 显示非主列 -->
|
|
||||||
<el-table-column v-if="isMobile && collapsedCols.length" type="expand">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-descriptions :column="1" size="small" class="gr-mobile-desc">
|
|
||||||
<el-descriptions-item
|
|
||||||
v-for="col in collapsedCols"
|
|
||||||
:key="col.prop"
|
|
||||||
:label="col.label"
|
|
||||||
>
|
|
||||||
<span v-if="col.formatter">{{ col.formatter(row, col.prop, row[col.prop]) }}</span>
|
|
||||||
<span v-else>{{ row[col.prop] ?? '-' }}</span>
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
桌面: 透传所有列 (含非主列)
|
桌面: 透传所有列 (含非主列)
|
||||||
移动端: 只透传主列 + 操作列 (type="selection" 也保留, 它没 prop)
|
|
||||||
-->
|
-->
|
||||||
<template v-for="(child, idx) in slotColumns" :key="idx">
|
<template v-for="(child, idx) in slotColumns" :key="idx">
|
||||||
<component
|
<component
|
||||||
@@ -51,6 +36,32 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 移动端: el-descriptions 卡片列表 (每行 = 一张卡, 包含所有字段) -->
|
||||||
|
<div v-else class="gr-mobile-cards">
|
||||||
|
<div v-for="(row, rIdx) in data" :key="rIdx" class="gr-mobile-card">
|
||||||
|
<el-descriptions :column="1" size="small" class="gr-mobile-desc">
|
||||||
|
<el-descriptions-item
|
||||||
|
v-for="(col, cIdx) in allCols"
|
||||||
|
:key="cIdx"
|
||||||
|
:label="col.label || (col.type === 'selection' ? '选择' : (col.type === 'index' ? '序号' : ''))"
|
||||||
|
>
|
||||||
|
<!-- 序号列特殊渲染 -->
|
||||||
|
<span v-if="col.type === 'index'">{{ rIdx + 1 }}</span>
|
||||||
|
<!-- 选择列特殊渲染 -->
|
||||||
|
<el-checkbox v-else-if="col.type === 'selection'" :model-value="false" disabled />
|
||||||
|
<!-- 有自定义 template 的列: 用 vnode 渲染 (含 maskPhone / openPreview 等自定义) -->
|
||||||
|
<component
|
||||||
|
v-else-if="col.templateVnode"
|
||||||
|
:is="renderTemplate(col.templateVnode, row, $event)"
|
||||||
|
/>
|
||||||
|
<!-- 普通 prop 列: 直接显示 -->
|
||||||
|
<span v-else>{{ row[col.prop] ?? '-' }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<div v-if="!data || !data.length" class="gr-mobile-empty">暂无数据</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -107,8 +118,7 @@ const slotColumns = computed(() => {
|
|||||||
return flat
|
return flat
|
||||||
})
|
})
|
||||||
|
|
||||||
// 抽取所有有 prop 的列 (排除 type=expand / type=selection / type=index, 它们用
|
// 抽取所有列的元数据 (含自定义 template vnode)
|
||||||
// 自身 type 标识, 不通过 prop)
|
|
||||||
const dataCols = computed(() => {
|
const dataCols = computed(() => {
|
||||||
return slotColumns.value
|
return slotColumns.value
|
||||||
.map((n, idx) => ({
|
.map((n, idx) => ({
|
||||||
@@ -117,11 +127,41 @@ const dataCols = computed(() => {
|
|||||||
type: n.props?.type,
|
type: n.props?.type,
|
||||||
prop: n.props?.prop,
|
prop: n.props?.prop,
|
||||||
label: n.props?.label || (n.props?.type === 'selection' ? '#' : (n.props?.type === 'index' ? '#' : '')),
|
label: n.props?.label || (n.props?.type === 'selection' ? '#' : (n.props?.type === 'index' ? '#' : '')),
|
||||||
formatter: n.props?.formatter
|
formatter: n.props?.formatter,
|
||||||
|
// 抓取自定义 #default slot 的 vnode (用于 mobile 卡片渲染)
|
||||||
|
templateVnode: extractDefaultSlot(n)
|
||||||
}))
|
}))
|
||||||
.filter((c) => c.prop) // 只要有 prop 的列
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 从 el-table-column vnode 抓取自定义 default slot 的 vnode
|
||||||
|
// el-table-column 的 children 通常是 template #default="{ row }"
|
||||||
|
function extractDefaultSlot(node) {
|
||||||
|
const children = node.children
|
||||||
|
if (!children) return null
|
||||||
|
// children 可能是数组, 也可能是单个
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
for (const c of children) {
|
||||||
|
if (c && c.type && c.type.name === 'template') {
|
||||||
|
// Vue 3 把 template slot 编译成对象 { default: () => [vnode] }
|
||||||
|
if (typeof c.children === 'object' && c.children && typeof c.children.default === 'function') {
|
||||||
|
return c.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// mobile 卡片: 渲染自定义 template (传入 row, 返回 vnode 数组)
|
||||||
|
function renderTemplate(slotObj, row) {
|
||||||
|
if (!slotObj || typeof slotObj.default !== 'function') return null
|
||||||
|
try {
|
||||||
|
return slotObj.default({ row })
|
||||||
|
} catch (e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算主列列表 (Vue3 不用 componentOptions, 直接读 vnode.props)
|
// 计算主列列表 (Vue3 不用 componentOptions, 直接读 vnode.props)
|
||||||
const mainColsResolved = computed(() => {
|
const mainColsResolved = computed(() => {
|
||||||
if (props.mainCols && props.mainCols.length) return props.mainCols
|
if (props.mainCols && props.mainCols.length) return props.mainCols
|
||||||
@@ -136,6 +176,9 @@ const collapsedCols = computed(() => {
|
|||||||
return dataCols.value.filter((c) => !mainColsResolved.value.includes(c.prop))
|
return dataCols.value.filter((c) => !mainColsResolved.value.includes(c.prop))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// mobile 卡片显示的列 (按用户传入顺序, 但 prop 列在前+ 非 prop 在后; 不折叠)
|
||||||
|
const allCols = computed(() => dataCols.value)
|
||||||
|
|
||||||
function shouldRenderColumn(node, idx) {
|
function shouldRenderColumn(node, idx) {
|
||||||
if (!isMobile.value) return true // 桌面全显示
|
if (!isMobile.value) return true // 桌面全显示
|
||||||
const meta = dataCols.value.find((c) => c.originalIndex === idx)
|
const meta = dataCols.value.find((c) => c.originalIndex === idx)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
让内容超出时容器内出现横向滚动条, 不影响外层 grid 布局.
|
让内容超出时容器内出现横向滚动条, 不影响外层 grid 布局.
|
||||||
-->
|
-->
|
||||||
<div class="attendee-table-wrap">
|
<div class="attendee-table-wrap">
|
||||||
<el-table :data="attendeeRows" v-loading="attendeeLoading" border size="small" style="width: 100%;" show-summary :summary-method="attendeeSummary" @selection-change="onAttendeeSelectionChange" class="attendee-table">
|
<GrTable :data="attendeeRows" v-loading="attendeeLoading" border size="small" @selection-change="onAttendeeSelectionChange" class="attendee-table">
|
||||||
<el-table-column v-if="!isSponsor && !isReadonly" type="selection" width="42" />
|
<el-table-column v-if="!isSponsor && !isReadonly" type="selection" width="42" />
|
||||||
<el-table-column type="index" label="序号" width="50" align="center" />
|
<el-table-column type="index" label="序号" width="50" align="center" />
|
||||||
<el-table-column prop="name" label="医生" min-width="90" />
|
<el-table-column prop="name" label="医生" min-width="90" />
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
<template #empty>
|
<template #empty>
|
||||||
<span class="text-muted">暂无参会人, 点击右上"新增参会人"按手机号添加</span>
|
<span class="text-muted">暂无参会人, 点击右上"新增参会人"按手机号添加</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table>
|
</GrTable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -563,7 +563,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import OssFileUploader from '@/components/OssFileUploader.vue'
|
import OssFileUploader from '@/components/OssFileUploader.vue'
|
||||||
import CameraQrUpload from '@/components/CameraQrUpload.vue'
|
import CameraQrUpload from '@/components/CameraQrUpload.vue'
|
||||||
import OssImageUploader from '@/components/OssImageUploader.vue'
|
import OssImageUploader from '@/components/OssImageUploader.vue'
|
||||||
import IdCardUploader from '@/components/IdCardUploader.vue'
|
import GrTable from '@/components/GrTable.vue'
|
||||||
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
import ImportResultDialog from '@/components/ImportResultDialog.vue'
|
||||||
import Preview from '@/components/Preview.vue'
|
import Preview from '@/components/Preview.vue'
|
||||||
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
import DoctorTitleSelect from '@/components/DoctorTitleSelect.vue'
|
||||||
@@ -874,15 +874,7 @@ const meetingFee = computed(() => {
|
|||||||
return { total, useMain: false }
|
return { total, useMain: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 参会人表格底部合计行: "医生"列显示"合计", "应发金额"列显示劳务费合计 */
|
|
||||||
function attendeeSummary({ columns }) {
|
|
||||||
const sums = columns.map(() => '')
|
|
||||||
const nameIdx = columns.findIndex(c => c.label === '医生')
|
|
||||||
const feeIdx = columns.findIndex(c => c.label === '应发金额')
|
|
||||||
if (nameIdx >= 0) sums[nameIdx] = '合计'
|
|
||||||
if (feeIdx >= 0) sums[feeIdx] = '¥ ' + laborFeeTotal.value.toFixed(2)
|
|
||||||
return sums
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 已勾选的参会人 (批量邀请/推送共用) */
|
/** 已勾选的参会人 (批量邀请/推送共用) */
|
||||||
const selectedAttendees = ref([])
|
const selectedAttendees = ref([])
|
||||||
@@ -2169,36 +2161,5 @@ onBeforeUnmount(stopFeePolling)
|
|||||||
|
|
||||||
/* 表格外层 wrap: 确保手机端表格能横向滚 */
|
/* 表格外层 wrap: 确保手机端表格能横向滚 */
|
||||||
.attendee-table-wrap { -webkit-overflow-scrolling: touch; }
|
.attendee-table-wrap { -webkit-overflow-scrolling: touch; }
|
||||||
|
|
||||||
/* 参会人表格 mobile: 字号缩到 11px, padding 紧凑, 行高紧凑 */
|
|
||||||
.attendee-table {
|
|
||||||
font-size: 11px !important;
|
|
||||||
}
|
|
||||||
.attendee-table :deep(.el-table th.el-table__cell),
|
|
||||||
.attendee-table :deep(.el-table td.el-table__cell) {
|
|
||||||
padding: 4px 3px !important;
|
|
||||||
font-size: 11px !important;
|
|
||||||
}
|
|
||||||
.attendee-table :deep(.el-table .row-actions) {
|
|
||||||
flex-direction: column !important;
|
|
||||||
align-items: flex-start !important;
|
|
||||||
gap: 1px !important;
|
|
||||||
}
|
|
||||||
.attendee-table :deep(.el-table .row-actions .el-link) {
|
|
||||||
font-size: 11px !important;
|
|
||||||
padding: 0 2px !important;
|
|
||||||
}
|
|
||||||
/* 标签字号缩窄 */
|
|
||||||
.attendee-table :deep(.el-table .el-tag) {
|
|
||||||
font-size: 10px !important;
|
|
||||||
padding: 0 4px !important;
|
|
||||||
height: 18px !important;
|
|
||||||
line-height: 18px !important;
|
|
||||||
}
|
|
||||||
/* 摘要行 (el-table__footer) 字号缩窄 */
|
|
||||||
.attendee-table :deep(.el-table .el-table__footer-wrapper td) {
|
|
||||||
font-size: 11px !important;
|
|
||||||
padding: 6px 3px !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user