Files

276 lines
6.0 KiB
Vue
Raw Permalink 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>
import { ref, computed } from 'vue'
const activeOp = ref('groupBy')
const rawOrders = [
{ userId: 'U001', orderId: 'ORD001', amount: 100, date: '2024-01-01' },
{ userId: 'U001', orderId: 'ORD002', amount: 200, date: '2024-01-02' },
{ userId: 'U002', orderId: 'ORD003', amount: 150, date: '2024-01-01' },
{ userId: 'U002', orderId: 'ORD004', amount: 300, date: '2024-01-03' },
{ userId: 'U003', orderId: 'ORD005', amount: 250, date: '2024-01-02' },
{ userId: 'U001', orderId: 'ORD006', amount: 180, date: '2024-01-04' }
]
const ops = {
groupBy: {
name: '按用户分组',
sql: `SELECT user_id, COUNT(*) as order_count, SUM(amount) as total
FROM orders GROUP BY user_id;`,
columns: ['用户 ID', '订单数', '总金额'],
data: [
{ '用户 ID': 'U001', 订单数: 3, 总金额: 480 },
{ '用户 ID': 'U002', 订单数: 2, 总金额: 450 },
{ '用户 ID': 'U003', 订单数: 1, 总金额: 250 }
]
},
sum: {
name: '总销售额',
sql: `SELECT SUM(amount) as total_sales FROM orders;`,
columns: ['总销售额'],
data: [{ 总销售额: 1180 }]
},
avg: {
name: '平均订单额',
sql: `SELECT AVG(amount) as avg_amount FROM orders;`,
columns: ['平均订单额'],
data: [{ 平均订单额: 196.67 }]
},
max: {
name: '最大订单额',
sql: `SELECT MAX(amount) as max_amount FROM orders;`,
columns: ['最大订单额'],
data: [{ 最大订单额: 300 }]
}
}
const opKeys = Object.keys(ops)
const currentOp = computed(() => ops[activeOp.value])
</script>
<template>
<div class="agg-demo">
<div class="demo-header">
<span class="icon">🧮</span>
<span class="title">数据聚合演示</span>
<span class="subtitle">拆分-计算-组合</span>
</div>
<div class="intro-text">
"所有用户平均转化率 5%" 往往毫无意义通过
<span class="hl">分组聚合</span>
把数据"切开"才能发现不同用户之间的真实差异点击下方操作观察同一份原始数据如何产生不同的
<span class="hl">聚合视角</span>
</div>
<!-- 原始数据表 -->
<div class="section">
<div class="section-label">原始订单数据</div>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th>用户 ID</th>
<th>订单号</th>
<th>金额</th>
<th>日期</th>
</tr>
</thead>
<tbody>
<tr v-for="r in rawOrders" :key="r.orderId">
<td>{{ r.userId }}</td>
<td>{{ r.orderId }}</td>
<td>{{ r.amount }}</td>
<td>{{ r.date }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 操作按钮 -->
<div class="ops-row">
<button
v-for="k in opKeys"
:key="k"
:class="['op-btn', { active: activeOp === k }]"
@click="activeOp = k"
>
{{ ops[k].name }}
</button>
</div>
<!-- 聚合结果 -->
<div class="section result-section">
<div class="section-label">{{ currentOp.name }} 结果</div>
<div class="table-wrap">
<table class="data-table">
<thead>
<tr>
<th v-for="col in currentOp.columns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in currentOp.data" :key="i">
<td v-for="col in currentOp.columns" :key="col">
{{ row[col] }}
</td>
</tr>
</tbody>
</table>
</div>
<div class="sql-block">
<div class="sql-label">SQL 示例</div>
<pre class="sql-code">{{ currentOp.sql }}</pre>
</div>
</div>
</div>
</template>
<style scoped>
.agg-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.demo-header {
padding: 14px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 8px;
}
.icon {
font-size: 18px;
}
.title {
font-weight: 600;
font-size: 15px;
}
.subtitle {
font-size: 12px;
color: var(--vp-c-text-3);
margin-left: auto;
}
.intro-text {
padding: 16px 20px;
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.7;
border-bottom: 1px solid var(--vp-c-divider);
}
.hl {
color: var(--vp-c-brand);
font-weight: 600;
}
.section {
padding: 16px 20px;
}
.section-label {
font-weight: 600;
font-size: 13px;
margin-bottom: 10px;
}
.table-wrap {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.data-table th,
.data-table td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
}
.data-table th {
background: var(--vp-c-bg-alt);
font-weight: 600;
}
.data-table tbody tr:hover {
background: var(--vp-c-bg-soft);
}
.ops-row {
display: flex;
gap: 8px;
padding: 0 20px 16px;
flex-wrap: wrap;
}
.op-btn {
padding: 8px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.op-btn:hover {
border-color: var(--vp-c-brand);
}
.op-btn.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.result-section {
border-top: 1px solid var(--vp-c-divider);
}
.sql-block {
margin-top: 12px;
}
.sql-label {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.sql-code {
margin: 0;
padding: 10px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 11px;
line-height: 1.6;
overflow-x: auto;
}
@media (max-width: 768px) {
.ops-row {
flex-direction: column;
}
}
</style>