@axiom-lattice/gateway 2.1.39 → 2.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +15 -0
- package/dist/index.js +464 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +415 -118
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/public/sdk/README.md +695 -0
- package/public/sdk/dashboard-engine-skill.md +1122 -0
- package/public/sdk/dashboard-single-file-spec.md +357 -0
- package/public/sdk/data-query-sdk-skill.md +307 -0
- package/public/sdk/data-query-sdk.d.ts +252 -0
- package/public/sdk/data-query-sdk.js +970 -0
- package/public/sdk/occupancy-dashboard.html +363 -0
- package/public/sdk/test-dashboard.html +690 -0
- package/src/__tests__/data-query.test.ts +77 -0
- package/src/controllers/data-query.ts +236 -0
- package/src/controllers/metrics-configs.ts +29 -25
- package/src/controllers/workspace.ts +95 -1
- package/src/index.ts +11 -0
- package/src/routes/index.ts +11 -0
- package/src/schemas/data-query.ts +69 -0
- package/src/schemas/index.ts +3 -0
- package/src/services/agent_task_consumer.ts +2 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>门店趋势监控仪表板</title>
|
|
8
|
+
<!-- Google Font: Inter -->
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
12
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
|
14
|
+
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
|
15
|
+
<script src="http://localhost:4001/sdk/data-query-sdk.js"></script>
|
|
16
|
+
<style>
|
|
17
|
+
body { font-family: 'Inter', ui-sans-serif, system-ui, sans-serif; }
|
|
18
|
+
.glass-card {
|
|
19
|
+
background: rgba(255, 255, 255, 0.9);
|
|
20
|
+
backdrop-filter: blur(10px);
|
|
21
|
+
-webkit-backdrop-filter: blur(10px);
|
|
22
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
23
|
+
}
|
|
24
|
+
.card {
|
|
25
|
+
background: white;
|
|
26
|
+
border-radius: 12px;
|
|
27
|
+
padding: 1.25rem 1.5rem;
|
|
28
|
+
box-shadow: 0 1px 3px rgb(0 0 0 / 0.06);
|
|
29
|
+
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
30
|
+
}
|
|
31
|
+
.chart-container { height: 300px; }
|
|
32
|
+
.sparkline { height: 60px; }
|
|
33
|
+
.skeleton {
|
|
34
|
+
background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
|
|
35
|
+
background-size: 200% 100%;
|
|
36
|
+
animation: skeleton 1.5s ease-in-out infinite;
|
|
37
|
+
border-radius: 6px;
|
|
38
|
+
}
|
|
39
|
+
@keyframes skeleton {
|
|
40
|
+
0% { background-position: 200% 0; }
|
|
41
|
+
100% { background-position: -200% 0; }
|
|
42
|
+
}
|
|
43
|
+
.input-shadcn {
|
|
44
|
+
border-radius: 8px;
|
|
45
|
+
border: 1px solid rgb(229 231 235);
|
|
46
|
+
padding: 0.5rem 0.75rem;
|
|
47
|
+
font-size: 0.875rem;
|
|
48
|
+
transition: box-shadow 0.15s, border-color 0.15s;
|
|
49
|
+
}
|
|
50
|
+
.input-shadcn:focus {
|
|
51
|
+
outline: none;
|
|
52
|
+
border-color: rgb(59 130 246);
|
|
53
|
+
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.15);
|
|
54
|
+
}
|
|
55
|
+
.btn-primary {
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
background: rgb(37 99 235);
|
|
58
|
+
color: white;
|
|
59
|
+
padding: 0.5rem 1rem;
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
transition: background 0.15s;
|
|
62
|
+
}
|
|
63
|
+
.btn-primary:hover { background: rgb(29 78 216); }
|
|
64
|
+
</style>
|
|
65
|
+
<script>window.__AI2APP_CONTEXT__ = { "tenantId": "fina_demo", "workspaceId": "fina-workspace", "projectId": "b0282da7-193c-4ca2-915b-362cee469806", "timestamp": 1773577987902 };</script>
|
|
66
|
+
</head>
|
|
67
|
+
|
|
68
|
+
<body class="bg-slate-50 min-h-screen">
|
|
69
|
+
<div class="container mx-auto px-4 py-8 max-w-7xl">
|
|
70
|
+
<!-- Header -->
|
|
71
|
+
<div class="mb-8">
|
|
72
|
+
<h1 class="text-3xl font-bold text-slate-800 mb-2 flex items-center gap-2">
|
|
73
|
+
<i data-lucide="layout-dashboard" class="w-8 h-8 text-blue-600"></i>
|
|
74
|
+
门店趋势监控仪表板
|
|
75
|
+
</h1>
|
|
76
|
+
<p class="text-slate-600">实时监控门店运营关键指标与趋势分析</p>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Filters: glass-card + Shadcn-style -->
|
|
80
|
+
<div class="glass-card rounded-xl shadow-sm p-6 mb-8">
|
|
81
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
82
|
+
<div>
|
|
83
|
+
<label class="block text-sm font-medium text-slate-700 mb-2 flex items-center gap-1.5">
|
|
84
|
+
<i data-lucide="calendar" class="w-4 h-4 text-slate-500"></i> 时间范围
|
|
85
|
+
</label>
|
|
86
|
+
<select id="dateRange" class="w-full input-shadcn">
|
|
87
|
+
<option value="ytd">本年度至今</option>
|
|
88
|
+
<option value="last30days">最近30天</option>
|
|
89
|
+
<option value="last90days">最近90天</option>
|
|
90
|
+
<option value="last6months">最近6个月</option>
|
|
91
|
+
</select>
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<label class="block text-sm font-medium text-slate-700 mb-2 flex items-center gap-1.5">
|
|
95
|
+
<i data-lucide="store" class="w-4 h-4 text-slate-500"></i> 门店
|
|
96
|
+
</label>
|
|
97
|
+
<select id="shopFilter" class="w-full input-shadcn">
|
|
98
|
+
<option value="all">全部门店</option>
|
|
99
|
+
<option value="漕宝路店">漕宝路店</option>
|
|
100
|
+
<option value="金桥店">金桥店</option>
|
|
101
|
+
<option value="宜山路店">宜山路店</option>
|
|
102
|
+
<option value="莘松路店">莘松路店</option>
|
|
103
|
+
<option value="静安诺富特">静安诺富特</option>
|
|
104
|
+
<option value="同济店">同济店</option>
|
|
105
|
+
<option value="龙漕路店">龙漕路店</option>
|
|
106
|
+
</select>
|
|
107
|
+
</div>
|
|
108
|
+
<div>
|
|
109
|
+
<label class="block text-sm font-medium text-slate-700 mb-2 flex items-center gap-1.5">
|
|
110
|
+
<i data-lucide="layout-grid" class="w-4 h-4 text-slate-500"></i> 房型
|
|
111
|
+
</label>
|
|
112
|
+
<select id="roomTypeFilter" class="w-full input-shadcn">
|
|
113
|
+
<option value="all">全部房型</option>
|
|
114
|
+
<option value="LOFT">LOFT</option>
|
|
115
|
+
<option value="平层">平层</option>
|
|
116
|
+
<option value="架子床">架子床</option>
|
|
117
|
+
<option value="天井LOFT">天井LOFT</option>
|
|
118
|
+
<option value="花园LOFT">花园LOFT</option>
|
|
119
|
+
</select>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="flex items-end">
|
|
122
|
+
<button id="refreshBtn" class="w-full btn-primary flex items-center justify-center gap-2">
|
|
123
|
+
<i data-lucide="refresh-cw" class="w-4 h-4"></i> 刷新数据
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Tier 1: Hero Metrics (glass-card style, Lucide icons, loading skeletons) -->
|
|
130
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
131
|
+
<!-- Occupancy Rate Card -->
|
|
132
|
+
<div class="card">
|
|
133
|
+
<div class="flex items-center justify-between mb-4">
|
|
134
|
+
<div class="min-w-0 flex-1">
|
|
135
|
+
<p class="text-sm font-medium text-slate-600 flex items-center gap-1.5">
|
|
136
|
+
<i data-lucide="percent" class="w-4 h-4 text-blue-600"></i> 出租率
|
|
137
|
+
</p>
|
|
138
|
+
<p id="occupancyRate" class="text-2xl font-bold text-slate-900 mt-1">--</p>
|
|
139
|
+
<p id="occupancyRateSkeleton" class="skeleton h-8 w-24 mt-1 hidden"></p>
|
|
140
|
+
<p id="occupancyChange" class="text-sm text-emerald-600 mt-0.5">↑ --%</p>
|
|
141
|
+
<p id="occupancyChangeSkeleton" class="skeleton h-4 w-16 mt-0.5 hidden"></p>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center shrink-0">
|
|
144
|
+
<i data-lucide="trending-up" class="w-6 h-6 text-blue-600"></i>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div id="occupancySparkline" class="sparkline"></div>
|
|
148
|
+
<div id="occupancySparklineSkeleton" class="sparkline skeleton hidden"></div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- Total Revenue Card -->
|
|
152
|
+
<div class="card">
|
|
153
|
+
<div class="flex items-center justify-between mb-4">
|
|
154
|
+
<div class="min-w-0 flex-1">
|
|
155
|
+
<p class="text-sm font-medium text-slate-600 flex items-center gap-1.5">
|
|
156
|
+
<i data-lucide="wallet" class="w-4 h-4 text-emerald-600"></i> 总租金收入
|
|
157
|
+
</p>
|
|
158
|
+
<p id="totalRevenue" class="text-2xl font-bold text-slate-900 mt-1">--</p>
|
|
159
|
+
<p id="totalRevenueSkeleton" class="skeleton h-8 w-28 mt-1 hidden"></p>
|
|
160
|
+
<p id="revenueChange" class="text-sm text-emerald-600 mt-0.5">↑ --%</p>
|
|
161
|
+
<p id="revenueChangeSkeleton" class="skeleton h-4 w-16 mt-0.5 hidden"></p>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="w-12 h-12 bg-emerald-100 rounded-xl flex items-center justify-center shrink-0">
|
|
164
|
+
<i data-lucide="dollar-sign" class="w-6 h-6 text-emerald-600"></i>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div id="revenueSparkline" class="sparkline"></div>
|
|
168
|
+
<div id="revenueSparklineSkeleton" class="sparkline skeleton hidden"></div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Average Rent Card -->
|
|
172
|
+
<div class="card">
|
|
173
|
+
<div class="flex items-center justify-between mb-4">
|
|
174
|
+
<div class="min-w-0 flex-1">
|
|
175
|
+
<p class="text-sm font-medium text-slate-600 flex items-center gap-1.5">
|
|
176
|
+
<i data-lucide="home" class="w-4 h-4 text-violet-600"></i> 平均租金
|
|
177
|
+
</p>
|
|
178
|
+
<p id="avgRent" class="text-2xl font-bold text-slate-900 mt-1">--</p>
|
|
179
|
+
<p id="avgRentSkeleton" class="skeleton h-8 w-24 mt-1 hidden"></p>
|
|
180
|
+
<p id="avgRentChange" class="text-sm text-emerald-600 mt-0.5">↑ --%</p>
|
|
181
|
+
<p id="avgRentChangeSkeleton" class="skeleton h-4 w-16 mt-0.5 hidden"></p>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="w-12 h-12 bg-violet-100 rounded-xl flex items-center justify-center shrink-0">
|
|
184
|
+
<i data-lucide="bar-chart-2" class="w-6 h-6 text-violet-600"></i>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div id="avgRentSparkline" class="sparkline"></div>
|
|
188
|
+
<div id="avgRentSparklineSkeleton" class="sparkline skeleton hidden"></div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<!-- Churn Rate Card -->
|
|
192
|
+
<div class="card">
|
|
193
|
+
<div class="flex items-center justify-between mb-4">
|
|
194
|
+
<div class="min-w-0 flex-1">
|
|
195
|
+
<p class="text-sm font-medium text-slate-600 flex items-center gap-1.5">
|
|
196
|
+
<i data-lucide="trending-down" class="w-4 h-4 text-rose-600"></i> 流失率
|
|
197
|
+
</p>
|
|
198
|
+
<p id="churnRate" class="text-2xl font-bold text-slate-900 mt-1">--</p>
|
|
199
|
+
<p id="churnRateSkeleton" class="skeleton h-8 w-20 mt-1 hidden"></p>
|
|
200
|
+
<p id="churnChange" class="text-sm text-rose-600 mt-0.5">↓ --%</p>
|
|
201
|
+
<p id="churnChangeSkeleton" class="skeleton h-4 w-16 mt-0.5 hidden"></p>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="w-12 h-12 bg-rose-100 rounded-xl flex items-center justify-center shrink-0">
|
|
204
|
+
<i data-lucide="activity" class="w-6 h-6 text-rose-600"></i>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div id="churnSparkline" class="sparkline"></div>
|
|
208
|
+
<div id="churnSparklineSkeleton" class="sparkline skeleton hidden"></div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Tier 2: Trend Analysis -->
|
|
213
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
214
|
+
<!-- Monthly Occupancy Trend -->
|
|
215
|
+
<div class="card">
|
|
216
|
+
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
|
217
|
+
<i data-lucide="line-chart" class="w-5 h-5 text-blue-600"></i> 月度出租率趋势
|
|
218
|
+
</h3>
|
|
219
|
+
<div id="occupancyTrend" class="chart-container"></div>
|
|
220
|
+
<div id="occupancyTrendSkeleton" class="chart-container skeleton hidden"></div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Monthly Revenue Trend -->
|
|
224
|
+
<div class="card">
|
|
225
|
+
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
|
226
|
+
<i data-lucide="area-chart" class="w-5 h-5 text-emerald-600"></i> 月度租金收入趋势
|
|
227
|
+
</h3>
|
|
228
|
+
<div id="revenueTrend" class="chart-container"></div>
|
|
229
|
+
<div id="revenueTrendSkeleton" class="chart-container skeleton hidden"></div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<!-- Store Performance Comparison -->
|
|
234
|
+
<div class="card mb-8">
|
|
235
|
+
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
|
236
|
+
<i data-lucide="bar-chart-3" class="w-5 h-5 text-violet-600"></i> 门店业绩对比
|
|
237
|
+
</h3>
|
|
238
|
+
<div id="storeComparison" class="chart-container"></div>
|
|
239
|
+
<div id="storeComparisonSkeleton" class="chart-container skeleton hidden"></div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Room Type Performance -->
|
|
243
|
+
<div class="card mb-8">
|
|
244
|
+
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
|
245
|
+
<i data-lucide="pie-chart" class="w-5 h-5 text-amber-600"></i> 房型业绩分析
|
|
246
|
+
</h3>
|
|
247
|
+
<div id="roomTypeAnalysis" class="chart-container"></div>
|
|
248
|
+
<div id="roomTypeAnalysisSkeleton" class="chart-container skeleton hidden"></div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<!-- Tier 3: Diagnostic Table -->
|
|
252
|
+
<div class="card">
|
|
253
|
+
<h3 class="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
|
254
|
+
<i data-lucide="table-2" class="w-5 h-5 text-slate-600"></i> 门店详细业绩表
|
|
255
|
+
</h3>
|
|
256
|
+
<div class="overflow-x-auto">
|
|
257
|
+
<table id="performanceTable" class="min-w-full divide-y divide-slate-200">
|
|
258
|
+
<thead class="bg-slate-50/80">
|
|
259
|
+
<tr>
|
|
260
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider rounded-tl-lg">门店</th>
|
|
261
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">出租率</th>
|
|
262
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">租金收入</th>
|
|
263
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">平均租金</th>
|
|
264
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider rounded-tr-lg">状态</th>
|
|
265
|
+
</tr>
|
|
266
|
+
</thead>
|
|
267
|
+
<tbody id="tableBody" class="bg-white divide-y divide-slate-200">
|
|
268
|
+
</tbody>
|
|
269
|
+
</table>
|
|
270
|
+
</div>
|
|
271
|
+
<div id="tableSkeleton" class="hidden space-y-2 py-4">
|
|
272
|
+
<div class="skeleton h-10 w-full"></div>
|
|
273
|
+
<div class="skeleton h-10 w-full"></div>
|
|
274
|
+
<div class="skeleton h-10 w-full"></div>
|
|
275
|
+
<div class="skeleton h-10 w-3/4"></div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<script>
|
|
281
|
+
// Initialize SDK
|
|
282
|
+
const sdk = new DataQuerySDK({
|
|
283
|
+
baseURL: 'http://localhost:4001',
|
|
284
|
+
serverKey: 'bjy_demo',
|
|
285
|
+
datasourceId: '1'
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Chart instances
|
|
289
|
+
let occupancyTrendChart, revenueTrendChart, storeComparisonChart, roomTypeChart;
|
|
290
|
+
|
|
291
|
+
// Show loading skeletons for a section (e.g. 'hero' | 'trend' | 'store' | 'room' | 'table')
|
|
292
|
+
function setSectionLoading(section, loading) {
|
|
293
|
+
const show = (id) => { const el = document.getElementById(id); if (el) el.classList.remove('hidden'); };
|
|
294
|
+
const hide = (id) => { const el = document.getElementById(id); if (el) el.classList.add('hidden'); };
|
|
295
|
+
if (section === 'hero') {
|
|
296
|
+
if (loading) {
|
|
297
|
+
['occupancyRateSkeleton', 'occupancyChangeSkeleton', 'occupancySparklineSkeleton',
|
|
298
|
+
'totalRevenueSkeleton', 'revenueChangeSkeleton', 'revenueSparklineSkeleton',
|
|
299
|
+
'avgRentSkeleton', 'avgRentChangeSkeleton', 'avgRentSparklineSkeleton',
|
|
300
|
+
'churnRateSkeleton', 'churnChangeSkeleton', 'churnSparklineSkeleton'].forEach(show);
|
|
301
|
+
['occupancyRate', 'occupancyChange', 'totalRevenue', 'revenueChange', 'avgRent', 'avgRentChange', 'churnRate', 'churnChange'].forEach(id => { const e = document.getElementById(id); if (e) e.style.visibility = 'hidden'; });
|
|
302
|
+
} else {
|
|
303
|
+
['occupancyRateSkeleton', 'occupancyChangeSkeleton', 'occupancySparklineSkeleton',
|
|
304
|
+
'totalRevenueSkeleton', 'revenueChangeSkeleton', 'revenueSparklineSkeleton',
|
|
305
|
+
'avgRentSkeleton', 'avgRentChangeSkeleton', 'avgRentSparklineSkeleton',
|
|
306
|
+
'churnRateSkeleton', 'churnChangeSkeleton', 'churnSparklineSkeleton'].forEach(hide);
|
|
307
|
+
['occupancyRate', 'occupancyChange', 'totalRevenue', 'revenueChange', 'avgRent', 'avgRentChange', 'churnRate', 'churnChange'].forEach(id => { const e = document.getElementById(id); if (e) e.style.visibility = ''; });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (section === 'trend') {
|
|
311
|
+
if (loading) { show('occupancyTrendSkeleton'); show('revenueTrendSkeleton'); document.getElementById('occupancyTrend').style.visibility = 'hidden'; document.getElementById('revenueTrend').style.visibility = 'hidden'; }
|
|
312
|
+
else { hide('occupancyTrendSkeleton'); hide('revenueTrendSkeleton'); document.getElementById('occupancyTrend').style.visibility = ''; document.getElementById('revenueTrend').style.visibility = ''; }
|
|
313
|
+
}
|
|
314
|
+
if (section === 'store') {
|
|
315
|
+
if (loading) { show('storeComparisonSkeleton'); document.getElementById('storeComparison').style.visibility = 'hidden'; }
|
|
316
|
+
else { hide('storeComparisonSkeleton'); document.getElementById('storeComparison').style.visibility = ''; }
|
|
317
|
+
}
|
|
318
|
+
if (section === 'room') {
|
|
319
|
+
if (loading) { show('roomTypeAnalysisSkeleton'); document.getElementById('roomTypeAnalysis').style.visibility = 'hidden'; }
|
|
320
|
+
else { hide('roomTypeAnalysisSkeleton'); document.getElementById('roomTypeAnalysis').style.visibility = ''; }
|
|
321
|
+
}
|
|
322
|
+
if (section === 'table') {
|
|
323
|
+
if (loading) { show('tableSkeleton'); document.getElementById('performanceTable').style.visibility = 'hidden'; }
|
|
324
|
+
else { hide('tableSkeleton'); document.getElementById('performanceTable').style.visibility = ''; }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Data adaptation function
|
|
329
|
+
function adaptToECharts(sdkResponse) {
|
|
330
|
+
if (!sdkResponse || !sdkResponse.data || !sdkResponse.data.columns) {
|
|
331
|
+
return [['date', 'value'], ['2024-01-01', 0]];
|
|
332
|
+
}
|
|
333
|
+
const header = sdkResponse.data.columns.map(c => c.name);
|
|
334
|
+
return [header, ...sdkResponse.data.rows];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Format currency
|
|
338
|
+
function formatCurrency(value) {
|
|
339
|
+
return new Intl.NumberFormat('zh-CN', {
|
|
340
|
+
style: 'currency',
|
|
341
|
+
currency: 'CNY',
|
|
342
|
+
minimumFractionDigits: 0
|
|
343
|
+
}).format(value);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Format percentage
|
|
347
|
+
function formatPercentage(value) {
|
|
348
|
+
return (value * 100).toFixed(1) + '%';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Initialize hero metrics
|
|
352
|
+
async function loadHeroMetrics() {
|
|
353
|
+
setSectionLoading('hero', true);
|
|
354
|
+
try {
|
|
355
|
+
// Occupancy Rate
|
|
356
|
+
const occupancyData = await sdk.query({
|
|
357
|
+
metrics: ['occupancy_rate'],
|
|
358
|
+
groupBy: ['date__month'],
|
|
359
|
+
filters: [{ dimension: 'date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
360
|
+
orderBy: [{ field: 'date__month', direction: 'ASC' }]
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
if (occupancyData.data && occupancyData.data.rows.length > 0) {
|
|
364
|
+
const latestRate = occupancyData.data.rows[occupancyData.data.rows.length - 1][1];
|
|
365
|
+
const previousRate = occupancyData.data.rows[occupancyData.data.rows.length - 2][1];
|
|
366
|
+
const change = ((latestRate - previousRate) / previousRate * 100).toFixed(1);
|
|
367
|
+
|
|
368
|
+
document.getElementById('occupancyRate').textContent = formatPercentage(latestRate);
|
|
369
|
+
document.getElementById('occupancyChange').textContent = `${change > 0 ? '↑' : '↓'} ${Math.abs(change)}%`;
|
|
370
|
+
document.getElementById('occupancyChange').className = change > 0 ? 'text-sm text-green-600' : 'text-sm text-red-600';
|
|
371
|
+
|
|
372
|
+
// Sparkline
|
|
373
|
+
const sparklineChart = echarts.init(document.getElementById('occupancySparkline'));
|
|
374
|
+
sparklineChart.setOption({
|
|
375
|
+
dataset: { source: adaptToECharts(occupancyData) },
|
|
376
|
+
xAxis: { type: 'category', show: false },
|
|
377
|
+
yAxis: { type: 'value', show: false },
|
|
378
|
+
series: [{
|
|
379
|
+
type: 'line',
|
|
380
|
+
smooth: true,
|
|
381
|
+
areaStyle: { opacity: 0.3, color: '#3B82F6' },
|
|
382
|
+
symbol: 'none',
|
|
383
|
+
lineStyle: { color: '#3B82F6', width: 2 }
|
|
384
|
+
}],
|
|
385
|
+
grid: { left: 0, right: 0, top: 0, bottom: 0 }
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Total Revenue
|
|
390
|
+
const revenueData = await sdk.query({
|
|
391
|
+
metrics: ['total_rent_amount'],
|
|
392
|
+
groupBy: ['date__month'],
|
|
393
|
+
filters: [{ dimension: 'date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
394
|
+
orderBy: [{ field: 'date__month', direction: 'ASC' }]
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (revenueData.data && revenueData.data.rows.length > 0) {
|
|
398
|
+
const latestRevenue = revenueData.data.rows[revenueData.data.rows.length - 1][1];
|
|
399
|
+
const previousRevenue = revenueData.data.rows[revenueData.data.rows.length - 2][1];
|
|
400
|
+
const change = ((latestRevenue - previousRevenue) / previousRevenue * 100).toFixed(1);
|
|
401
|
+
|
|
402
|
+
document.getElementById('totalRevenue').textContent = formatCurrency(latestRevenue);
|
|
403
|
+
document.getElementById('revenueChange').textContent = `${change > 0 ? '↑' : '↓'} ${Math.abs(change)}%`;
|
|
404
|
+
document.getElementById('revenueChange').className = change > 0 ? 'text-sm text-green-600' : 'text-sm text-red-600';
|
|
405
|
+
|
|
406
|
+
// Sparkline
|
|
407
|
+
const sparklineChart = echarts.init(document.getElementById('revenueSparkline'));
|
|
408
|
+
sparklineChart.setOption({
|
|
409
|
+
dataset: { source: adaptToECharts(revenueData) },
|
|
410
|
+
xAxis: { type: 'category', show: false },
|
|
411
|
+
yAxis: { type: 'value', show: false },
|
|
412
|
+
series: [{
|
|
413
|
+
type: 'line',
|
|
414
|
+
smooth: true,
|
|
415
|
+
areaStyle: { opacity: 0.3, color: '#10B981' },
|
|
416
|
+
symbol: 'none',
|
|
417
|
+
lineStyle: { color: '#10B981', width: 2 }
|
|
418
|
+
}],
|
|
419
|
+
grid: { left: 0, right: 0, top: 0, bottom: 0 }
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Average Rent
|
|
424
|
+
const avgRentData = await sdk.query({
|
|
425
|
+
metrics: ['avg_rent'],
|
|
426
|
+
groupBy: ['actual_start_date__month'],
|
|
427
|
+
filters: [{ dimension: 'actual_start_date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
428
|
+
orderBy: [{ field: 'actual_start_date__month', direction: 'ASC' }]
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
if (avgRentData.data && avgRentData.data.rows.length > 0) {
|
|
432
|
+
const latestAvgRent = avgRentData.data.rows[avgRentData.data.rows.length - 1][1];
|
|
433
|
+
const previousAvgRent = avgRentData.data.rows[avgRentData.data.rows.length - 2][1];
|
|
434
|
+
const change = ((latestAvgRent - previousAvgRent) / previousAvgRent * 100).toFixed(1);
|
|
435
|
+
|
|
436
|
+
document.getElementById('avgRent').textContent = formatCurrency(latestAvgRent);
|
|
437
|
+
document.getElementById('avgRentChange').textContent = `${change > 0 ? '↑' : '↓'} ${Math.abs(change)}%`;
|
|
438
|
+
document.getElementById('avgRentChange').className = change > 0 ? 'text-sm text-green-600' : 'text-sm text-red-600';
|
|
439
|
+
|
|
440
|
+
// Sparkline
|
|
441
|
+
const sparklineChart = echarts.init(document.getElementById('avgRentSparkline'));
|
|
442
|
+
sparklineChart.setOption({
|
|
443
|
+
dataset: { source: adaptToECharts(avgRentData) },
|
|
444
|
+
xAxis: { type: 'category', show: false },
|
|
445
|
+
yAxis: { type: 'value', show: false },
|
|
446
|
+
series: [{
|
|
447
|
+
type: 'line',
|
|
448
|
+
smooth: true,
|
|
449
|
+
areaStyle: { opacity: 0.3, color: '#8B5CF6' },
|
|
450
|
+
symbol: 'none',
|
|
451
|
+
lineStyle: { color: '#8B5CF6', width: 2 }
|
|
452
|
+
}],
|
|
453
|
+
grid: { left: 0, right: 0, top: 0, bottom: 0 }
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
setSectionLoading('hero', false);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error('Error loading hero metrics:', error);
|
|
459
|
+
setSectionLoading('hero', false);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Initialize trend charts
|
|
464
|
+
async function loadTrendCharts() {
|
|
465
|
+
setSectionLoading('trend', true);
|
|
466
|
+
try {
|
|
467
|
+
// Occupancy Trend
|
|
468
|
+
const occupancyTrendData = await sdk.query({
|
|
469
|
+
metrics: ['occupancy_rate'],
|
|
470
|
+
groupBy: ['date__month'],
|
|
471
|
+
filters: [{ dimension: 'date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
472
|
+
orderBy: [{ field: 'date__month', direction: 'ASC' }]
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (occupancyTrendData.data) {
|
|
476
|
+
occupancyTrendChart = echarts.init(document.getElementById('occupancyTrend'));
|
|
477
|
+
occupancyTrendChart.setOption({
|
|
478
|
+
dataset: { source: adaptToECharts(occupancyTrendData) },
|
|
479
|
+
tooltip: { trigger: 'axis', formatter: '{b}: {c}%' },
|
|
480
|
+
xAxis: {
|
|
481
|
+
type: 'category',
|
|
482
|
+
axisLabel: { formatter: '{MMM}' }
|
|
483
|
+
},
|
|
484
|
+
yAxis: {
|
|
485
|
+
type: 'value',
|
|
486
|
+
axisLabel: { formatter: '{value}%' }
|
|
487
|
+
},
|
|
488
|
+
series: [{
|
|
489
|
+
type: 'line',
|
|
490
|
+
smooth: true,
|
|
491
|
+
areaStyle: { opacity: 0.3 },
|
|
492
|
+
lineStyle: { width: 3 }
|
|
493
|
+
}],
|
|
494
|
+
color: ['#3B82F6']
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Revenue Trend
|
|
499
|
+
const revenueTrendData = await sdk.query({
|
|
500
|
+
metrics: ['total_rent_amount'],
|
|
501
|
+
groupBy: ['date__month'],
|
|
502
|
+
filters: [{ dimension: 'date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
503
|
+
orderBy: [{ field: 'date__month', direction: 'ASC' }]
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
if (revenueTrendData.data) {
|
|
507
|
+
revenueTrendChart = echarts.init(document.getElementById('revenueTrend'));
|
|
508
|
+
revenueTrendChart.setOption({
|
|
509
|
+
dataset: { source: adaptToECharts(revenueTrendData) },
|
|
510
|
+
tooltip: { trigger: 'axis', formatter: '{b}: {c}' },
|
|
511
|
+
xAxis: {
|
|
512
|
+
type: 'category',
|
|
513
|
+
axisLabel: { formatter: '{MMM}' }
|
|
514
|
+
},
|
|
515
|
+
yAxis: {
|
|
516
|
+
type: 'value',
|
|
517
|
+
axisLabel: { formatter: '{value}' }
|
|
518
|
+
},
|
|
519
|
+
series: [{
|
|
520
|
+
type: 'line',
|
|
521
|
+
smooth: true,
|
|
522
|
+
areaStyle: { opacity: 0.3 },
|
|
523
|
+
lineStyle: { width: 3 }
|
|
524
|
+
}],
|
|
525
|
+
color: ['#10B981']
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
setSectionLoading('trend', false);
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error('Error loading trend charts:', error);
|
|
531
|
+
setSectionLoading('trend', false);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Load store comparison
|
|
536
|
+
async function loadStoreComparison() {
|
|
537
|
+
setSectionLoading('store', true);
|
|
538
|
+
try {
|
|
539
|
+
const storeData = await sdk.query({
|
|
540
|
+
metrics: ['occupancy_rate', 'total_rent_amount'],
|
|
541
|
+
groupBy: ['shop'],
|
|
542
|
+
filters: [{ dimension: 'date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
543
|
+
orderBy: [{ field: 'total_rent_amount', direction: 'DESC' }],
|
|
544
|
+
limit: 10
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
if (storeData.data) {
|
|
548
|
+
storeComparisonChart = echarts.init(document.getElementById('storeComparison'));
|
|
549
|
+
storeComparisonChart.setOption({
|
|
550
|
+
dataset: { source: adaptToECharts(storeData) },
|
|
551
|
+
tooltip: { trigger: 'axis' },
|
|
552
|
+
legend: {},
|
|
553
|
+
xAxis: { type: 'category' },
|
|
554
|
+
yAxis: [
|
|
555
|
+
{ type: 'value', name: '出租率(%)', axisLabel: { formatter: '{value}%' } },
|
|
556
|
+
{ type: 'value', name: '租金收入', axisLabel: { formatter: '{value}' } }
|
|
557
|
+
],
|
|
558
|
+
series: [
|
|
559
|
+
{
|
|
560
|
+
type: 'bar',
|
|
561
|
+
name: '出租率',
|
|
562
|
+
yAxisIndex: 0,
|
|
563
|
+
encode: { x: 'shop', y: 'occupancy_rate' }
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
type: 'line',
|
|
567
|
+
name: '租金收入',
|
|
568
|
+
yAxisIndex: 1,
|
|
569
|
+
encode: { x: 'shop', y: 'total_rent_amount' }
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
setSectionLoading('store', false);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.error('Error loading store comparison:', error);
|
|
577
|
+
setSectionLoading('store', false);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Load room type analysis
|
|
582
|
+
async function loadRoomTypeAnalysis() {
|
|
583
|
+
setSectionLoading('room', true);
|
|
584
|
+
try {
|
|
585
|
+
const roomTypeData = await sdk.query({
|
|
586
|
+
metrics: ['occupancy_rate', 'total_rent_amount'],
|
|
587
|
+
groupBy: ['room_type'],
|
|
588
|
+
filters: [{ dimension: 'date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
589
|
+
orderBy: [{ field: 'total_rent_amount', direction: 'DESC' }]
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (roomTypeData.data) {
|
|
593
|
+
roomTypeChart = echarts.init(document.getElementById('roomTypeAnalysis'));
|
|
594
|
+
roomTypeChart.setOption({
|
|
595
|
+
dataset: { source: adaptToECharts(roomTypeData) },
|
|
596
|
+
tooltip: { trigger: 'item' },
|
|
597
|
+
legend: { orient: 'vertical', left: 'left' },
|
|
598
|
+
series: [
|
|
599
|
+
{
|
|
600
|
+
type: 'pie',
|
|
601
|
+
radius: '50%',
|
|
602
|
+
encode: { itemName: 'room_type', value: 'total_rent_amount' },
|
|
603
|
+
label: {
|
|
604
|
+
formatter: '{b}: {d}%'
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
setSectionLoading('room', false);
|
|
611
|
+
} catch (error) {
|
|
612
|
+
console.error('Error loading room type analysis:', error);
|
|
613
|
+
setSectionLoading('room', false);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Load performance table
|
|
618
|
+
async function loadPerformanceTable() {
|
|
619
|
+
setSectionLoading('table', true);
|
|
620
|
+
try {
|
|
621
|
+
const tableData = await sdk.query({
|
|
622
|
+
metrics: ['occupancy_rate', 'total_rent_amount', 'avg_rent'],
|
|
623
|
+
groupBy: ['shop'],
|
|
624
|
+
filters: [{ dimension: 'date', operator: 'BETWEEN', values: ['2024-01-01', '2024-12-31'] }],
|
|
625
|
+
orderBy: [{ field: 'total_rent_amount', direction: 'DESC' }],
|
|
626
|
+
limit: 20
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
if (tableData.data) {
|
|
630
|
+
const tbody = document.getElementById('tableBody');
|
|
631
|
+
tbody.innerHTML = '';
|
|
632
|
+
|
|
633
|
+
tableData.data.rows.forEach(row => {
|
|
634
|
+
const [shop, occupancyRate, totalRevenue, avgRent] = row;
|
|
635
|
+
const status = occupancyRate > 0.9 ? '优秀' : occupancyRate > 0.8 ? '良好' : '需改进';
|
|
636
|
+
const badgeClass = occupancyRate > 0.9 ? 'bg-emerald-100 text-emerald-800' : occupancyRate > 0.8 ? 'bg-amber-100 text-amber-800' : 'bg-rose-100 text-rose-800';
|
|
637
|
+
|
|
638
|
+
tbody.innerHTML += `
|
|
639
|
+
<tr>
|
|
640
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">${shop}</td>
|
|
641
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-700">${formatPercentage(occupancyRate)}</td>
|
|
642
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-700">${formatCurrency(totalRevenue)}</td>
|
|
643
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-700">${formatCurrency(avgRent)}</td>
|
|
644
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
645
|
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${badgeClass}">${status}</span>
|
|
646
|
+
</td>
|
|
647
|
+
</tr>
|
|
648
|
+
`;
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
setSectionLoading('table', false);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
console.error('Error loading performance table:', error);
|
|
654
|
+
setSectionLoading('table', false);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Refresh all data
|
|
659
|
+
async function refreshAll() {
|
|
660
|
+
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
661
|
+
await Promise.all([
|
|
662
|
+
loadHeroMetrics(),
|
|
663
|
+
loadTrendCharts(),
|
|
664
|
+
loadStoreComparison(),
|
|
665
|
+
loadRoomTypeAnalysis(),
|
|
666
|
+
loadPerformanceTable()
|
|
667
|
+
]);
|
|
668
|
+
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Event listeners
|
|
672
|
+
document.getElementById('refreshBtn').addEventListener('click', refreshAll);
|
|
673
|
+
|
|
674
|
+
// Handle window resize
|
|
675
|
+
window.addEventListener('resize', () => {
|
|
676
|
+
occupancyTrendChart && occupancyTrendChart.resize();
|
|
677
|
+
revenueTrendChart && revenueTrendChart.resize();
|
|
678
|
+
storeComparisonChart && storeComparisonChart.resize();
|
|
679
|
+
roomTypeChart && roomTypeChart.resize();
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Initialize dashboard
|
|
683
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
684
|
+
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
685
|
+
refreshAll();
|
|
686
|
+
});
|
|
687
|
+
</script>
|
|
688
|
+
</body>
|
|
689
|
+
|
|
690
|
+
</html>
|