<template>
<div class="table-summary-demo">
<!-- 页面标题 -->
<div class="demo-header">
<h1>📊 Element Plus 表格合计功能</h1>
<p>基于 Vue3 + TypeScript,支持接口返回合计数据的表格实现</p>
</div>
<!-- 操作按钮 -->
<div class="demo-actions">
<el-button
type="primary"
:loading="loading"
@click="fetchTableData"
icon="Refresh"
>
刷新数据
</el-button>
<el-button
:type="showSummary ? 'success' : 'info'"
@click="toggleSummary"
:icon="showSummary ? 'View' : 'Hide'"
>
{{ showSummary ? '隐藏合计' : '显示合计' }}
</el-button>
</div>
<!-- 表格组件 -->
<el-table
v-loading="loading"
:data="tableData"
:show-summary="showSummary"
:summary-method="getSummaries"
stripe
border
style="width: 100%"
class="summary-table"
>
<el-table-column
prop="productName"
label="产品名称"
width="200"
show-overflow-tooltip
/>
<el-table-column
prop="category"
label="产品分类"
width="150"
/>
<el-table-column
prop="price"
label="单价 (元)"
width="120"
align="right"
>
<template #default="{ row }">
<span class="price-text">¥{{ formatNumber(row.price) }}</span>
</template>
</el-table-column>
<el-table-column
prop="quantity"
label="数量"
width="100"
align="right"
/>
<el-table-column
prop="amount"
label="金额 (元)"
width="150"
align="right"
>
<template #default="{ row }">
<span class="amount-text">¥{{ formatNumber(row.amount) }}</span>
</template>
</el-table-column>
<el-table-column
prop="discount"
label="折扣金额 (元)"
width="150"
align="right"
>
<template #default="{ row }">
<span class="discount-text">-¥{{ formatNumber(row.discount) }}</span>
</template>
</el-table-column>
<el-table-column
prop="finalAmount"
label="实付金额 (元)"
width="150"
align="right"
>
<template #default="{ row }">
<span class="final-amount-text">¥{{ formatNumber(row.finalAmount) }}</span>
</template>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="120"
align="center"
>
<template #default="{ row }">
<el-tag
:type="getStatusType(row.status)"
size="small"
>
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
width="180"
/>
</el-table>
<!-- 合计数据信息 -->
<div v-if="summaryData" class="summary-info">
<h3>📈 合计数据详情</h3>
<div class="summary-grid">
<div class="summary-item">
<div class="summary-label">总记录数</div>
<div class="summary-value">{{ summaryData.totalRecords }} 条</div>
</div>
<div class="summary-item">
<div class="summary-label">总数量</div>
<div class="summary-value">{{ formatNumber(summaryData.totalQuantity) }}</div>
</div>
<div class="summary-item">
<div class="summary-label">总金额</div>
<div class="summary-value price-highlight">¥{{ formatNumber(summaryData.totalAmount) }}</div>
</div>
<div class="summary-item">
<div class="summary-label">总折扣</div>
<div class="summary-value discount-highlight">-¥{{ formatNumber(summaryData.totalDiscount) }}</div>
</div>
<div class="summary-item">
<div class="summary-label">实付总额</div>
<div class="summary-value final-highlight">¥{{ formatNumber(summaryData.totalFinalAmount) }}</div>
</div>
</div>
</div>
<!-- API 响应示例 -->
<div class="api-example">
<h3>🔧 接口返回数据结构示例</h3>
<el-collapse v-model="activeCollapse">
<el-collapse-item title="查看 API 响应数据" name="api-response">
<pre><code>{{ JSON.stringify(mockApiResponse, null, 2) }}</code></pre>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import type { TableColumnCtx } from 'element-plus'
interface TableRowData {
id: number
productName: string
category: string
price: number
quantity: number
amount: number
discount: number
finalAmount: number
status: 'active' | 'inactive' | 'pending'
createTime: string
}
interface SummaryData {
totalRecords: number
totalQuantity: number
totalAmount: number
totalDiscount: number
totalFinalAmount: number
}
interface ApiResponse {
data: TableRowData[]
summary: SummaryData
success: boolean
message: string
}
const loading = ref<boolean>(false)
const showSummary = ref<boolean>(true)
const activeCollapse = ref<string>('')
const tableData = ref<TableRowData[]>([])
const summaryData = ref<SummaryData | null>(null)
const mockApiResponse = reactive<ApiResponse>({
data: [
{
id: 1,
productName: 'iPhone 15 Pro Max',
category: '智能手机',
price: 9999.00,
quantity: 2,
amount: 19998.00,
discount: 1000.00,
finalAmount: 18998.00,
status: 'active',
createTime: '2024-01-15 10:30:00'
},
{
id: 2,
productName: 'MacBook Pro 16"',
category: '笔记本电脑',
price: 25999.00,
quantity: 1,
amount: 25999.00,
discount: 2000.00,
finalAmount: 23999.00,
status: 'active',
createTime: '2024-01-16 14:20:00'
},
{
id: 3,
productName: 'iPad Air',
category: '平板电脑',
price: 4999.00,
quantity: 3,
amount: 14997.00,
discount: 500.00,
finalAmount: 14497.00,
status: 'pending',
createTime: '2024-01-17 09:15:00'
},
{
id: 4,
productName: 'AirPods Pro',
category: '音频设备',
price: 1999.00,
quantity: 5,
amount: 9995.00,
discount: 200.00,
finalAmount: 9795.00,
status: 'active',
createTime: '2024-01-18 16:45:00'
},
{
id: 5,
productName: 'Apple Watch Ultra',
category: '智能手表',
price: 6299.00,
quantity: 1,
amount: 6299.00,
discount: 300.00,
finalAmount: 5999.00,
status: 'inactive',
createTime: '2024-01-19 11:00:00'
}
],
summary: {
totalRecords: 5,
totalQuantity: 12,
totalAmount: 77288.00,
totalDiscount: 4000.00,
totalFinalAmount: 73288.00
},
success: true,
message: '数据获取成功'
})
const fetchTableData = async (): Promise<void> => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 1000))
const response: ApiResponse = {
...mockApiResponse,
data: mockApiResponse.data.map(item => ({
...item,
quantity: item.quantity + Math.floor(Math.random() * 2),
}))
}
response.data.forEach(item => {
item.amount = item.price * item.quantity
item.finalAmount = item.amount - item.discount
})
response.summary = {
totalRecords: response.data.length,
totalQuantity: response.data.reduce((sum, item) => sum + item.quantity, 0),
totalAmount: response.data.reduce((sum, item) => sum + item.amount, 0),
totalDiscount: response.data.reduce((sum, item) => sum + item.discount, 0),
totalFinalAmount: response.data.reduce((sum, item) => sum + item.finalAmount, 0)
}
tableData.value = response.data
summaryData.value = response.summary
} catch (error) {
console.error('获取数据失败:', error)
} finally {
loading.value = false
}
}
const getSummaries = (param: {
columns: TableColumnCtx<TableRowData>[]
data: TableRowData[]
}): string[] => {
const { columns } = param
const sums: string[] = []
if (!summaryData.value) {
return columns.map(() => '')
}
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '合计'
} else if (column.property === 'quantity') {
sums[index] = formatNumber(summaryData.value!.totalQuantity)
} else if (column.property === 'amount') {
sums[index] = `¥${formatNumber(summaryData.value!.totalAmount)}`
} else if (column.property === 'discount') {
sums[index] = `-¥${formatNumber(summaryData.value!.totalDiscount)}`
} else if (column.property === 'finalAmount') {
sums[index] = `¥${formatNumber(summaryData.value!.totalFinalAmount)}`
} else {
sums[index] = '-'
}
})
return sums
}
const toggleSummary = (): void => {
showSummary.value = !showSummary.value
}
const formatNumber = (num: number): string => {
return num.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
const getStatusType = (status: string): string => {
const statusMap: Record<string, string> = {
active: 'success',
inactive: 'danger',
pending: 'warning'
}
return statusMap[status] || 'info'
}
const getStatusText = (status: string): string => {
const statusMap: Record<string, string> = {
active: '已激活',
inactive: '已停用',
pending: '待处理'
}
return statusMap[status] || '未知'
}
onMounted(() => {
fetchTableData()
})
</script>
<style scoped>
.table-summary-demo {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.demo-header {
text-align: center;
margin-bottom: 30px;
padding: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
color: white;
}
.demo-header h1 {
font-size: 2rem;
margin: 0 0 10px 0;
font-weight: 700;
}
.demo-header p {
font-size: 1.1rem;
margin: 0;
opacity: 0.9;
}
.demo-actions {
margin-bottom: 20px;
display: flex;
gap: 12px;
}
.summary-table {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.price-text {
color: #409EFF;
font-weight: 600;
}
.amount-text {
color: #67C23A;
font-weight: 600;
}
.discount-text {
color: #F56C6C;
font-weight: 600;
}
.final-amount-text {
color: #E6A23C;
font-weight: 700;
font-size: 14px;
}
.summary-info {
margin-top: 30px;
padding: 25px;
background: #f8fafc;
border-radius: 12px;
border: 2px solid #e2e8f0;
}
.summary-info h3 {
margin: 0 0 20px 0;
color: #374151;
font-size: 1.3rem;
font-weight: 700;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.summary-item {
background: white;
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.summary-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.summary-label {
font-size: 14px;
color: #6b7280;
margin-bottom: 8px;
font-weight: 500;
}
.summary-value {
font-size: 18px;
font-weight: 700;
color: #374151;
}
.price-highlight {
color: #10b981;
}
.discount-highlight {
color: #ef4444;
}
.final-highlight {
color: #f59e0b;
font-size: 20px;
}
.api-example {
margin-top: 30px;
padding: 25px;
background: white;
border-radius: 12px;
border: 2px solid #e5e7eb;
}
.api-example h3 {
margin: 0 0 20px 0;
color: #374151;
font-size: 1.3rem;
font-weight: 700;
}
.api-example pre {
background: #1f2937;
color: #e5e7eb;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
font-size: 13px;
line-height: 1.5;
}
.api-example code {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
@media (max-width: 768px) {
.table-summary-demo {
padding: 10px;
}
.demo-header {
padding: 20px;
}
.demo-header h1 {
font-size: 1.5rem;
}
.summary-grid {
grid-template-columns: 1fr;
}
.demo-actions {
flex-direction: column;
}
}
:deep(.el-table__footer) {
font-weight: 700;
}
:deep(.el-table__footer-wrapper tbody td) {
background-color: #f8fafc !important;
border-top: 2px solid #667eea !important;
font-size: 14px;
color: #374151;
}
:deep(.el-table__footer-wrapper tbody td:first-child) {
color: #667eea;
font-weight: 800;
font-size: 15px;
}
</style>