@morningfast/create-ui 0.0.9 → 0.0.11

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.
@@ -9,6 +9,7 @@ declare module '*.vue' {
9
9
 
10
10
  interface ImportMetaEnv {
11
11
  readonly VITE_API_BASE_URL?: string
12
+ readonly VITE_USE_MOCK?: string
12
13
  readonly VITE_PROXY_TARGET?: string
13
14
  }
14
15
 
@@ -18,8 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@element-plus/icons-vue": "^2.3.2",
21
- "@morningfast/platform": "^0.0.9",
21
+ "@morningfast/platform": "^0.0.11",
22
22
  "axios": "^1.16.0",
23
+ "echarts": "^6.1.0",
23
24
  "element-plus": "^2.14.0",
24
25
  "localforage": "^1.10.0",
25
26
  "morningfast-plus": "^1.1.0",
@@ -1,79 +1,770 @@
1
1
  <!--
2
- 业务首页示例,可删除或改造成项目真实首页。
2
+ 主应用业务首页示例,可删除或改造成项目真实首页。
3
3
  这里演示页面如何调用 `src/stores/workplace.ts`,再由 store 调用当前页面目录的 `api.ts`。
4
4
  -->
5
5
  <template>
6
- <div class="page">
7
- <div class="page__header">
6
+ <div class="portal-page">
7
+ <header class="portal-header">
8
8
  <div>
9
- <h2 class="page__title">首页</h2>
10
- <p class="page__subtitle">这一页用于承接内部管理系统的首页概览、待办提醒和常用入口。</p>
9
+ <h2 class="portal-header__title">主应用运营门户</h2>
10
+ <p class="portal-header__subtitle">聚合待办、数据环比、和常用工作入口。</p>
11
11
  </div>
12
- </div>
12
+ <div class="portal-header__status">
13
+ <el-tag effect="light" type="success">主应用运行中</el-tag>
14
+ <el-tag effect="plain">3 个图表</el-tag>
15
+ </div>
16
+ </header>
13
17
 
14
- <el-row v-loading="workplaceStore.loading" :gutter="16" class="stat-grid">
15
- <el-col v-for="item in workplaceStore.metrics" :key="item.label" :xs="12" :sm="12" :md="6">
16
- <el-card shadow="hover" class="stat-card">
17
- <span class="stat-card__label">{{ item.label }}</span>
18
- <strong class="stat-card__value">{{ item.value }}</strong>
19
- </el-card>
20
- </el-col>
21
- </el-row>
22
-
23
- <el-row :gutter="16">
24
- <el-col :xs="24" :lg="16">
25
- <el-card shadow="never">
26
- <template #header>
27
- <div class="block-header">
28
- <strong>项目建议推进项</strong>
29
- <span>先做结构和能力层,后续直接替换视觉规范</span>
30
- </div>
31
- </template>
32
-
33
- <el-timeline>
34
- <el-timeline-item timestamp="阶段 1" type="primary">
35
- 路由、布局、登录态、权限占位、请求层
36
- </el-timeline-item>
37
- <el-timeline-item timestamp="阶段 2" type="success">
38
- 列表页模板、表单模板、字典管理和基础组件沉淀
39
- </el-timeline-item>
40
- <el-timeline-item timestamp="阶段 3" type="warning">
41
- 按项目设计规范覆盖样式,并逐步接入真实接口
42
- </el-timeline-item>
43
- </el-timeline>
44
- </el-card>
45
- </el-col>
46
-
47
- <el-col :xs="24" :lg="8">
48
- <el-card shadow="never">
49
- <template #header>
50
- <div class="block-header">
51
- <strong>常用入口</strong>
52
- <span>后续可以替换成卡片路由导航</span>
53
- </div>
54
- </template>
18
+ <div ref="widgetGridRef" class="dashboard-grid">
19
+ <section
20
+ v-for="widget in portalWidgets"
21
+ :key="widget.id"
22
+ class="portal-panel dashboard-widget"
23
+ :class="[
24
+ `dashboard-widget--${widget.id}`,
25
+ { 'dashboard-widget--wide': widget.size === 'wide' },
26
+ { 'dashboard-widget--full': widget.size === 'full' },
27
+ ]"
28
+ >
29
+ <div class="panel-header widget-drag-handle" title="拖拽调整模块位置">
30
+ <div>
31
+ <strong>{{ widget.title }}</strong>
32
+ <span>{{ widget.subtitle }}</span>
33
+ </div>
34
+ </div>
55
35
 
56
- <div class="shortcut-grid">
57
- <div v-for="item in workplaceStore.shortcuts" :key="item.title" class="shortcut-item">
58
- {{ item.title }}
36
+ <div v-if="widget.id === 'metrics'" v-loading="workplaceStore.loading" class="metric-grid">
37
+ <div
38
+ v-for="item in metricCards"
39
+ :key="item.label"
40
+ class="metric-card"
41
+ :style="{ '--accent-color': item.color }"
42
+ >
43
+ <div class="metric-card__icon">
44
+ <el-icon>
45
+ <component :is="item.icon" />
46
+ </el-icon>
59
47
  </div>
48
+ <span class="metric-card__label">{{ item.label }}</span>
49
+ <strong class="metric-card__value">{{ item.value }}</strong>
50
+ <span class="metric-card__hint">{{ item.hint }}</span>
60
51
  </div>
61
- </el-card>
62
- </el-col>
63
- </el-row>
52
+ </div>
53
+
54
+ <div v-else-if="widget.id === 'trend'" class="chart chart--trend chart--large" />
55
+
56
+ <div v-else-if="widget.id === 'apps'" class="chart chart--apps" />
57
+
58
+ <div v-else-if="widget.id === 'radar'" class="chart chart--radar" />
59
+
60
+ <el-timeline v-else-if="widget.id === 'timeline'" class="portal-timeline">
61
+ <el-timeline-item timestamp="阶段 1" type="primary">
62
+ 路由、布局、登录态、权限占位、请求层
63
+ </el-timeline-item>
64
+ <el-timeline-item timestamp="阶段 2" type="success">
65
+ 列表页模板、表单模板、字典管理和基础组件沉淀
66
+ </el-timeline-item>
67
+ <el-timeline-item timestamp="阶段 3" type="warning">
68
+ 按项目设计规范覆盖样式,并逐步接入真实接口
69
+ </el-timeline-item>
70
+ </el-timeline>
71
+
72
+ <div v-else-if="widget.id === 'shortcuts'" class="shortcut-grid">
73
+ <button
74
+ v-for="item in shortcutCards"
75
+ :key="item.title"
76
+ class="shortcut-item"
77
+ type="button"
78
+ >
79
+ <span class="shortcut-item__icon" :style="{ '--accent-color': item.color }">
80
+ <el-icon>
81
+ <component :is="item.icon" />
82
+ </el-icon>
83
+ </span>
84
+ <span>{{ item.title }}</span>
85
+ </button>
86
+ </div>
87
+ </section>
88
+ </div>
64
89
  </div>
65
90
  </template>
66
91
 
67
92
  <script setup lang="ts">
68
- import { onMounted } from 'vue';
93
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
94
+ import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts';
95
+ import { GridComponent, LegendComponent, RadarComponent, TooltipComponent } from 'echarts/components';
96
+ import { init, use, type EChartsType } from 'echarts/core';
97
+ import { CanvasRenderer } from 'echarts/renderers';
98
+ import {
99
+ Bell,
100
+ Collection,
101
+ Finished,
102
+ Menu,
103
+ Operation,
104
+ Setting,
105
+ User,
106
+ Warning,
107
+ } from '@element-plus/icons-vue';
108
+ import {
109
+ getPersistedValue,
110
+ setPersistedValue,
111
+ useSortable,
112
+ } from '@morningfast/platform/main';
69
113
 
70
114
  import { useWorkplaceStore } from '@/stores';
71
115
 
116
+ use([
117
+ BarChart,
118
+ CanvasRenderer,
119
+ GridComponent,
120
+ LegendComponent,
121
+ LineChart,
122
+ PieChart,
123
+ RadarChart,
124
+ RadarComponent,
125
+ TooltipComponent,
126
+ ]);
127
+
72
128
  const workplaceStore = useWorkplaceStore();
73
129
 
74
- onMounted(() => {
130
+ const widgetGridRef = ref<HTMLDivElement>();
131
+
132
+ let trendChart: EChartsType | undefined;
133
+ let appChart: EChartsType | undefined;
134
+ let radarChart: EChartsType | undefined;
135
+ let resizeObserver: ResizeObserver | undefined;
136
+
137
+ type WidgetId = 'metrics' | 'trend' | 'apps' | 'radar' | 'timeline' | 'shortcuts';
138
+
139
+ interface PortalWidget {
140
+ id: WidgetId;
141
+ size?: 'full' | 'wide';
142
+ subtitle: string;
143
+ title: string;
144
+ }
145
+
146
+ const WIDGET_ORDER_STORAGE_KEY = 'dashboard:portal-widget-order';
147
+ const METRIC_ORDER_STORAGE_KEY = 'dashboard:portal-metric-order';
148
+
149
+ const defaultWidgets: PortalWidget[] = [
150
+ {
151
+ id: 'metrics',
152
+ size: 'full',
153
+ subtitle: '按个人关注顺序展示核心待办',
154
+ title: '关键指标',
155
+ },
156
+ {
157
+ id: 'trend',
158
+ size: 'wide',
159
+ subtitle: '主应用视角下的访问、任务和告警走势',
160
+ title: '业务活跃趋势',
161
+ },
162
+ {
163
+ id: 'apps',
164
+ subtitle: '按当前处理阶段查看工作量占比',
165
+ title: '待办状态分布',
166
+ },
167
+ {
168
+ id: 'radar',
169
+ subtitle: '模板当前能力成熟度',
170
+ title: '平台能力雷达',
171
+ },
172
+ {
173
+ id: 'timeline',
174
+ subtitle: '先做结构和能力层,再接业务规范',
175
+ title: '项目建议推进项',
176
+ },
177
+ {
178
+ id: 'shortcuts',
179
+ subtitle: '后续替换成真实菜单路由导航',
180
+ title: '常用入口',
181
+ },
182
+ ];
183
+
184
+ const portalWidgets = ref(sortByStoredOrder(defaultWidgets, [], (item) => item.id));
185
+ const metricOrder = ref<string[]>([]);
186
+
187
+ const { init: initWidgetSortable } = useSortable({
188
+ animation: 180,
189
+ chosenClass: 'dashboard-widget--chosen',
190
+ direction: 'horizontal',
191
+ dragClass: 'dashboard-widget--dragging',
192
+ draggable: '.dashboard-widget',
193
+ ghostClass: 'dashboard-widget--ghost',
194
+ handle: '.widget-drag-handle',
195
+ onEnd: (oldIndex, newIndex) => {
196
+ moveItem(portalWidgets.value, oldIndex, newIndex);
197
+ persistWidgetOrder();
198
+ void resetCharts();
199
+ },
200
+ swapThreshold: 1,
201
+ });
202
+
203
+ const { init: initMetricSortable } = useSortable({
204
+ animation: 160,
205
+ chosenClass: 'metric-card--chosen',
206
+ direction: 'horizontal',
207
+ dragClass: 'metric-card--dragging',
208
+ draggable: '.metric-card',
209
+ ghostClass: 'metric-card--ghost',
210
+ onEnd: (oldIndex, newIndex) => {
211
+ moveItem(metricOrder.value, oldIndex, newIndex);
212
+ persistMetricOrder();
213
+ },
214
+ swapThreshold: 1,
215
+ });
216
+
217
+ const metricMeta = [
218
+ { color: '#2f6fed', hint: '较昨日 +18%', icon: Bell },
219
+ { color: '#16a085', hint: '已收敛 7 项', icon: Warning },
220
+ { color: '#8e44ad', hint: '今日完成 11 项', icon: Finished },
221
+ { color: '#d35400', hint: '2 项等待确认', icon: Operation },
222
+ ];
223
+
224
+ const shortcutMeta = [
225
+ { color: '#2f6fed', icon: User },
226
+ { color: '#16a085', icon: Menu },
227
+ { color: '#8e44ad', icon: Collection },
228
+ { color: '#d35400', icon: Setting },
229
+ ];
230
+
231
+ const resolvedMetrics = computed(() =>
232
+ workplaceStore.metrics.map((item, index) => ({
233
+ ...item,
234
+ ...metricMeta[index % metricMeta.length],
235
+ })),
236
+ );
237
+
238
+ const metricCards = computed(() =>
239
+ sortByStoredOrder(resolvedMetrics.value, metricOrder.value, (item) => item.label),
240
+ );
241
+
242
+ const shortcutCards = computed(() =>
243
+ workplaceStore.shortcuts.map((item, index) => ({
244
+ ...item,
245
+ ...shortcutMeta[index % shortcutMeta.length],
246
+ })),
247
+ );
248
+
249
+ function sortByStoredOrder<T>(items: T[], storedOrder: string[], getKey: (item: T) => string) {
250
+ const orderedSet = new Set(storedOrder);
251
+ const itemMap = new Map(items.map((item) => [getKey(item), item]));
252
+ const sorted = storedOrder.flatMap((id) => {
253
+ const item = itemMap.get(id);
254
+ return item ? [item] : [];
255
+ });
256
+ const rest = items.filter((item) => !orderedSet.has(getKey(item)));
257
+
258
+ return [...sorted, ...rest];
259
+ }
260
+
261
+ function sortStringOrder(items: string[], storedOrder: string[]) {
262
+ const itemSet = new Set(items);
263
+ const sorted = storedOrder.filter((item) => itemSet.has(item));
264
+ const orderedSet = new Set(storedOrder);
265
+ const rest = items.filter((item) => !orderedSet.has(item));
266
+
267
+ return [...sorted, ...rest];
268
+ }
269
+
270
+ function moveItem<T>(items: T[], oldIndex: number, newIndex: number) {
271
+ if (oldIndex === newIndex) {
272
+ return;
273
+ }
274
+
275
+ const [item] = items.splice(oldIndex, 1);
276
+ if (!item) {
277
+ return;
278
+ }
279
+
280
+ items.splice(newIndex, 0, item);
281
+ }
282
+
283
+ async function loadWidgetOrder() {
284
+ const storedOrder = await getPersistedValue<string[]>(WIDGET_ORDER_STORAGE_KEY, []);
285
+ portalWidgets.value = sortByStoredOrder(defaultWidgets, normalizeStringArray(storedOrder), (item) => item.id);
286
+ }
287
+
288
+ async function loadMetricOrder() {
289
+ const storedOrder = await getPersistedValue<string[]>(METRIC_ORDER_STORAGE_KEY, []);
290
+ metricOrder.value = normalizeStringArray(storedOrder);
291
+ }
292
+
293
+ function normalizeStringArray(value: unknown) {
294
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
295
+ }
296
+
297
+ function persistWidgetOrder() {
298
+ void setPersistedValue(
299
+ WIDGET_ORDER_STORAGE_KEY,
300
+ portalWidgets.value.map((item) => item.id),
301
+ );
302
+ }
303
+
304
+ function persistMetricOrder() {
305
+ void setPersistedValue(METRIC_ORDER_STORAGE_KEY, metricOrder.value);
306
+ }
307
+
308
+ function getPortalElement(selector: string) {
309
+ return widgetGridRef.value?.querySelector<HTMLElement>(selector) ?? null;
310
+ }
311
+
312
+ function syncMetricOrder() {
313
+ const currentLabels = workplaceStore.metrics.map((item) => item.label);
314
+ metricOrder.value = sortStringOrder(currentLabels, metricOrder.value);
315
+ persistMetricOrder();
316
+ }
317
+
318
+ async function resetCharts() {
319
+ disposeCharts();
320
+ await nextTick();
321
+ setupCharts();
322
+ }
323
+
324
+ function setupCharts() {
325
+ const trendChartElement = getPortalElement('.chart--trend');
326
+ const appChartElement = getPortalElement('.chart--apps');
327
+ const radarChartElement = getPortalElement('.chart--radar');
328
+
329
+ if (!trendChartElement || !appChartElement || !radarChartElement) {
330
+ return;
331
+ }
332
+
333
+ trendChart = init(trendChartElement);
334
+ appChart = init(appChartElement);
335
+ radarChart = init(radarChartElement);
336
+
337
+ trendChart.setOption({
338
+ color: ['#2f6fed', '#16a085', '#f39c12'],
339
+ grid: { bottom: 28, left: 36, right: 20, top: 36 },
340
+ legend: { right: 12, top: 0, textStyle: { color: '#667085' } },
341
+ tooltip: { trigger: 'axis' },
342
+ xAxis: {
343
+ boundaryGap: false,
344
+ data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
345
+ type: 'category',
346
+ axisLine: { lineStyle: { color: '#d0d5dd' } },
347
+ axisLabel: { color: '#667085' },
348
+ },
349
+ yAxis: {
350
+ type: 'value',
351
+ splitLine: { lineStyle: { color: '#eef2f6' } },
352
+ axisLabel: { color: '#667085' },
353
+ },
354
+ series: [
355
+ {
356
+ areaStyle: { opacity: 0.12 },
357
+ data: [132, 154, 181, 176, 224, 238, 265],
358
+ name: '访问量',
359
+ smooth: true,
360
+ symbolSize: 7,
361
+ type: 'line',
362
+ },
363
+ {
364
+ data: [28, 36, 42, 39, 51, 56, 64],
365
+ name: '任务数',
366
+ smooth: true,
367
+ symbolSize: 7,
368
+ type: 'line',
369
+ },
370
+ {
371
+ barWidth: 10,
372
+ data: [6, 5, 9, 4, 7, 3, 5],
373
+ name: '告警',
374
+ type: 'bar',
375
+ },
376
+ ],
377
+ });
378
+
379
+ appChart.setOption({
380
+ color: ['#2f6fed', '#16a085', '#8e44ad', '#f39c12'],
381
+ tooltip: { trigger: 'item' },
382
+ legend: { bottom: 0, textStyle: { color: '#667085' } },
383
+ series: [
384
+ {
385
+ center: ['50%', '44%'],
386
+ data: [
387
+ { name: '待审核', value: 32 },
388
+ { name: '处理中', value: 28 },
389
+ { name: '待确认', value: 22 },
390
+ { name: '已逾期', value: 8 },
391
+ ],
392
+ emphasis: {
393
+ itemStyle: {
394
+ shadowBlur: 16,
395
+ shadowColor: 'rgba(16, 24, 40, 0.18)',
396
+ },
397
+ },
398
+ radius: ['48%', '72%'],
399
+ type: 'pie',
400
+ },
401
+ ],
402
+ });
403
+
404
+ radarChart.setOption({
405
+ color: ['#2f6fed'],
406
+ radar: {
407
+ indicator: [
408
+ { max: 100, name: '路由' },
409
+ { max: 100, name: '权限' },
410
+ { max: 100, name: '请求' },
411
+ { max: 100, name: '微前端' },
412
+ { max: 100, name: '组件' },
413
+ ],
414
+ radius: '66%',
415
+ splitArea: {
416
+ areaStyle: {
417
+ color: ['rgba(47, 111, 237, 0.04)', 'rgba(22, 160, 133, 0.04)'],
418
+ },
419
+ },
420
+ axisName: { color: '#667085' },
421
+ axisLine: { lineStyle: { color: '#d0d5dd' } },
422
+ splitLine: { lineStyle: { color: '#e4e7ec' } },
423
+ },
424
+ series: [
425
+ {
426
+ areaStyle: { opacity: 0.16 },
427
+ data: [{ name: '当前模板', value: [88, 76, 84, 82, 72] }],
428
+ symbolSize: 6,
429
+ type: 'radar',
430
+ },
431
+ ],
432
+ tooltip: {},
433
+ });
434
+
435
+ resizeObserver = new ResizeObserver(() => {
436
+ trendChart?.resize();
437
+ appChart?.resize();
438
+ radarChart?.resize();
439
+ });
440
+ resizeObserver.observe(trendChartElement);
441
+ resizeObserver.observe(appChartElement);
442
+ resizeObserver.observe(radarChartElement);
443
+ }
444
+
445
+ function disposeCharts() {
446
+ resizeObserver?.disconnect();
447
+ trendChart?.dispose();
448
+ appChart?.dispose();
449
+ radarChart?.dispose();
450
+
451
+ resizeObserver = undefined;
452
+ trendChart = undefined;
453
+ appChart = undefined;
454
+ radarChart = undefined;
455
+ }
456
+
457
+ onMounted(async () => {
75
458
  if (!workplaceStore.hasOverview) {
76
459
  void workplaceStore.loadOverview();
77
460
  }
461
+
462
+ await Promise.all([loadWidgetOrder(), loadMetricOrder()]);
463
+ await nextTick();
464
+ syncMetricOrder();
465
+ initWidgetSortable(widgetGridRef.value ?? null);
466
+ initMetricSortable(getPortalElement('.metric-grid'));
467
+ setupCharts();
468
+ });
469
+
470
+ watch(
471
+ () => workplaceStore.loading,
472
+ async (loading) => {
473
+ if (!loading) {
474
+ await nextTick();
475
+ syncMetricOrder();
476
+ initMetricSortable(getPortalElement('.metric-grid'));
477
+ trendChart?.resize();
478
+ appChart?.resize();
479
+ radarChart?.resize();
480
+ }
481
+ },
482
+ );
483
+
484
+ onBeforeUnmount(() => {
485
+ disposeCharts();
78
486
  });
79
487
  </script>
488
+
489
+ <style scoped>
490
+ .portal-page {
491
+ display: flex;
492
+ flex-direction: column;
493
+ gap: 16px;
494
+ height: 100%;
495
+ min-height: 0;
496
+ padding: 20px;
497
+ overflow-y: auto;
498
+ }
499
+
500
+ .portal-header {
501
+ display: flex;
502
+ align-items: center;
503
+ justify-content: space-between;
504
+ gap: 16px;
505
+ }
506
+
507
+ .portal-header__title {
508
+ margin: 0;
509
+ font-size: 24px;
510
+ line-height: 1.3;
511
+ color: #101828;
512
+ }
513
+
514
+ .portal-header__subtitle {
515
+ margin: 6px 0 0;
516
+ color: #667085;
517
+ font-size: 14px;
518
+ }
519
+
520
+ .portal-header__status {
521
+ display: inline-flex;
522
+ flex-shrink: 0;
523
+ align-items: center;
524
+ gap: 8px;
525
+ }
526
+
527
+ .dashboard-grid {
528
+ display: flex;
529
+ align-items: stretch;
530
+ flex-wrap: wrap;
531
+ gap: 16px;
532
+ }
533
+
534
+ .dashboard-widget {
535
+ flex: 1 1 calc((100% - 32px) / 3);
536
+ min-height: 330px;
537
+ min-width: 320px;
538
+ }
539
+
540
+ .dashboard-widget--wide {
541
+ flex-basis: calc(((100% - 32px) / 3) * 2 + 16px);
542
+ }
543
+
544
+ .dashboard-widget--full {
545
+ flex-basis: 100%;
546
+ min-height: auto;
547
+ }
548
+
549
+ .dashboard-widget--metrics {
550
+ min-height: 220px;
551
+ }
552
+
553
+ .dashboard-widget--chosen,
554
+ .metric-card--chosen {
555
+ cursor: grabbing;
556
+ }
557
+
558
+ .dashboard-widget--ghost,
559
+ .metric-card--ghost {
560
+ border-style: dashed;
561
+ background: #eef4ff;
562
+ box-shadow: none;
563
+ opacity: 0.64;
564
+ }
565
+
566
+ .dashboard-widget--dragging,
567
+ .metric-card--dragging {
568
+ box-shadow: 0 18px 46px rgba(16, 24, 40, 0.18);
569
+ opacity: 0.96;
570
+ }
571
+
572
+ .metric-card,
573
+ .portal-panel {
574
+ border: 1px solid #e4e7ec;
575
+ border-radius: 8px;
576
+ background: #fff;
577
+ box-shadow: 0 12px 30px rgba(16, 24, 40, 0.06);
578
+ }
579
+
580
+ .metric-card {
581
+ position: relative;
582
+ display: grid;
583
+ min-height: 142px;
584
+ padding: 18px;
585
+ overflow: hidden;
586
+ cursor: grab;
587
+ }
588
+
589
+ .metric-card::after {
590
+ position: absolute;
591
+ top: -40px;
592
+ right: -38px;
593
+ width: 118px;
594
+ height: 118px;
595
+ border-radius: 50%;
596
+ background: color-mix(in srgb, var(--accent-color) 16%, transparent);
597
+ content: '';
598
+ }
599
+
600
+ .metric-card__icon {
601
+ display: inline-flex;
602
+ align-items: center;
603
+ justify-content: center;
604
+ width: 38px;
605
+ height: 38px;
606
+ margin-bottom: 14px;
607
+ border-radius: 8px;
608
+ background: color-mix(in srgb, var(--accent-color) 12%, #fff);
609
+ color: var(--accent-color);
610
+ font-size: 20px;
611
+ }
612
+
613
+ .metric-card__label,
614
+ .metric-card__hint,
615
+ .panel-header span {
616
+ color: #667085;
617
+ }
618
+
619
+ .metric-card__label {
620
+ font-size: 13px;
621
+ }
622
+
623
+ .metric-card__value {
624
+ margin-top: 4px;
625
+ font-size: 30px;
626
+ line-height: 1.15;
627
+ color: #101828;
628
+ }
629
+
630
+ .metric-card__hint {
631
+ margin-top: 8px;
632
+ font-size: 12px;
633
+ }
634
+
635
+ .portal-panel {
636
+ padding: 18px;
637
+ }
638
+
639
+ .panel-header {
640
+ display: flex;
641
+ align-items: flex-start;
642
+ justify-content: space-between;
643
+ gap: 16px;
644
+ margin-bottom: 12px;
645
+ }
646
+
647
+ .panel-header strong {
648
+ display: block;
649
+ font-size: 16px;
650
+ color: #101828;
651
+ }
652
+
653
+ .panel-header span {
654
+ display: block;
655
+ margin-top: 4px;
656
+ font-size: 12px;
657
+ }
658
+
659
+ .widget-drag-handle {
660
+ cursor: grab;
661
+ user-select: none;
662
+ }
663
+
664
+ .widget-drag-handle:active {
665
+ cursor: grabbing;
666
+ }
667
+
668
+ .chart {
669
+ width: 100%;
670
+ height: 258px;
671
+ }
672
+
673
+ .chart--large {
674
+ height: 300px;
675
+ }
676
+
677
+ .chart--radar {
678
+ height: 252px;
679
+ }
680
+
681
+ .portal-timeline {
682
+ padding-top: 8px;
683
+ }
684
+
685
+ .metric-grid {
686
+ display: grid;
687
+ grid-template-columns: repeat(4, minmax(0, 1fr));
688
+ gap: 12px;
689
+ }
690
+
691
+ .shortcut-grid {
692
+ display: grid;
693
+ grid-template-columns: repeat(2, minmax(0, 1fr));
694
+ gap: 12px;
695
+ }
696
+
697
+ .shortcut-item {
698
+ display: flex;
699
+ align-items: center;
700
+ gap: 10px;
701
+ min-width: 0;
702
+ height: 66px;
703
+ padding: 0 12px;
704
+ border: 1px solid #e4e7ec;
705
+ border-radius: 8px;
706
+ background: #f8fafc;
707
+ color: #344054;
708
+ cursor: pointer;
709
+ font: inherit;
710
+ font-weight: 700;
711
+ text-align: left;
712
+ transition:
713
+ border-color 0.2s ease,
714
+ box-shadow 0.2s ease,
715
+ transform 0.2s ease;
716
+ }
717
+
718
+ .shortcut-item:hover {
719
+ border-color: color-mix(in srgb, var(--accent-color) 34%, #d0d5dd);
720
+ box-shadow: 0 12px 24px rgba(16, 24, 40, 0.08);
721
+ transform: translateY(-2px);
722
+ }
723
+
724
+ .shortcut-item__icon {
725
+ display: inline-flex;
726
+ flex: 0 0 auto;
727
+ align-items: center;
728
+ justify-content: center;
729
+ width: 34px;
730
+ height: 34px;
731
+ border-radius: 8px;
732
+ background: color-mix(in srgb, var(--accent-color) 12%, #fff);
733
+ color: var(--accent-color);
734
+ font-size: 18px;
735
+ }
736
+
737
+ @media (max-width: 640px) {
738
+ .portal-page {
739
+ padding: 16px;
740
+ }
741
+
742
+ .portal-header {
743
+ align-items: flex-start;
744
+ flex-direction: column;
745
+ }
746
+
747
+ .portal-header__status {
748
+ flex-wrap: wrap;
749
+ }
750
+
751
+ .shortcut-grid {
752
+ grid-template-columns: 1fr;
753
+ }
754
+ }
755
+
756
+ @media (max-width: 1200px) {
757
+ .metric-grid {
758
+ grid-template-columns: repeat(2, minmax(0, 1fr));
759
+ }
760
+ }
761
+
762
+ @media (max-width: 760px) {
763
+ .dashboard-widget,
764
+ .dashboard-widget--wide,
765
+ .dashboard-widget--full {
766
+ flex-basis: 100%;
767
+ min-width: 0;
768
+ }
769
+ }
770
+ </style>
@@ -34,7 +34,7 @@ const mockWorkplaceOverview: WorkplaceOverview = {
34
34
  };
35
35
 
36
36
  export async function fetchWorkplaceOverview(): Promise<WorkplaceOverview> {
37
- if (import.meta.env.VITE_USE_MOCK === 'true') {
37
+ if (import.meta.env.VITE_USE_MOCK === 'true' || !import.meta.env.VITE_PROXY_TARGET) {
38
38
  return mockWorkplaceOverview;
39
39
  }
40
40
 
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@element-plus/icons-vue": "^2.3.2",
17
- "@morningfast/platform": "^0.0.9",
17
+ "@morningfast/platform": "^0.0.11",
18
18
  "@types/sortablejs": "^1.15.9",
19
19
  "@vitejs/plugin-vue": "^6.0.6",
20
20
  "element-plus": "^2.14.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morningfast/create-ui",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Morningfast UI internal system scaffold CLI.",
5
5
  "bin": {
6
6
  "create-morningfast-ui": "dist/index.js"