|
|
@@ -1,12 +1,12 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { ref } from "vue";
|
|
|
+import { ref, computed } from "vue";
|
|
|
import { useSync } from "./utils/hook";
|
|
|
import { PureTableBar } from "@/components/RePureTableBar";
|
|
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
|
|
+import dayjs from "dayjs";
|
|
|
|
|
|
import Refresh from "~icons/ep/refresh";
|
|
|
import View from "~icons/ep/view";
|
|
|
-import Sync from "~icons/ep/refresh-right";
|
|
|
|
|
|
defineOptions({
|
|
|
name: "DataSync"
|
|
|
@@ -19,162 +19,468 @@ const {
|
|
|
form,
|
|
|
loading,
|
|
|
syncing,
|
|
|
+ currentSyncKey,
|
|
|
columns,
|
|
|
dataList,
|
|
|
statistics,
|
|
|
+ syncModules,
|
|
|
+ syncTypeMap,
|
|
|
+ statusMap,
|
|
|
pagination,
|
|
|
onSearch,
|
|
|
resetForm,
|
|
|
- handleSyncDevices,
|
|
|
- handleSyncProducts,
|
|
|
+ handleSync,
|
|
|
handleViewLogs,
|
|
|
handleSizeChange,
|
|
|
handleCurrentChange
|
|
|
} = useSync();
|
|
|
+
|
|
|
+/** 已上线的模块 */
|
|
|
+const activeModules = computed(() => syncModules.value.filter(m => m.active));
|
|
|
+/** 未上线的模块 */
|
|
|
+const upcomingModules = computed(() => syncModules.value.filter(m => !m.active));
|
|
|
+
|
|
|
+/** 格式化最后同步时间 */
|
|
|
+function formatTime(time?: string) {
|
|
|
+ if (!time) return "-";
|
|
|
+ return dayjs(time).format("MM-DD HH:mm");
|
|
|
+}
|
|
|
+
|
|
|
+/** 获取最近同步状态标签类型 */
|
|
|
+function getStatusTagType(status?: number): string {
|
|
|
+ if (status === undefined || status === null) return "info";
|
|
|
+ return statusMap[status]?.type || "info";
|
|
|
+}
|
|
|
+
|
|
|
+/** 获取最近同步状态文本 */
|
|
|
+function getStatusText(status?: number): string {
|
|
|
+ if (status === undefined || status === null) return "未同步";
|
|
|
+ return statusMap[status]?.text || "未知";
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <div class="main">
|
|
|
- <el-form
|
|
|
- ref="formRef"
|
|
|
- :inline="true"
|
|
|
- :model="form"
|
|
|
- class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
|
|
|
- >
|
|
|
- <el-form-item label="同步类型:" prop="syncType">
|
|
|
- <el-select
|
|
|
- v-model="form.syncType"
|
|
|
- placeholder="请选择"
|
|
|
- clearable
|
|
|
- class="w-[180px]!"
|
|
|
- >
|
|
|
- <el-option label="设备同步" value="device" />
|
|
|
- <el-option label="商品同步" value="product" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="状态:" prop="status">
|
|
|
- <el-select
|
|
|
- v-model="form.status"
|
|
|
- placeholder="请选择"
|
|
|
- clearable
|
|
|
- class="w-[180px]!"
|
|
|
- >
|
|
|
- <el-option label="进行中" :value="1" />
|
|
|
- <el-option label="成功" :value="2" />
|
|
|
- <el-option label="失败" :value="3" />
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item>
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- :icon="useRenderIcon('ri/search-line')"
|
|
|
- :loading="loading"
|
|
|
- @click="onSearch"
|
|
|
- >
|
|
|
- 搜索
|
|
|
- </el-button>
|
|
|
- <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
|
|
- 重置
|
|
|
- </el-button>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
-
|
|
|
- <div class="flex gap-4 mb-4">
|
|
|
- <el-card class="flex-1">
|
|
|
- <div class="text-center">
|
|
|
- <div class="text-2xl font-bold text-primary">{{ statistics.totalSync }}</div>
|
|
|
- <div class="text-gray-500">总同步次数</div>
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
- <el-card class="flex-1">
|
|
|
- <div class="text-center">
|
|
|
- <div class="text-2xl font-bold text-success">{{ statistics.todaySync }}</div>
|
|
|
- <div class="text-gray-500">今日同步</div>
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
- <el-card class="flex-1">
|
|
|
- <div class="text-center">
|
|
|
- <div class="text-2xl font-bold text-warning">{{ statistics.successRate }}%</div>
|
|
|
- <div class="text-gray-500">成功率</div>
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
- <el-card class="flex-1">
|
|
|
- <div class="text-center">
|
|
|
- <div class="text-sm font-bold text-info">{{ statistics.lastSyncTime || '-' }}</div>
|
|
|
- <div class="text-gray-500">最后同步时间</div>
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
- </div>
|
|
|
+ <div class="sync-page">
|
|
|
|
|
|
- <PureTableBar
|
|
|
- title="数据同步"
|
|
|
- :columns="columns"
|
|
|
- @refresh="onSearch"
|
|
|
- >
|
|
|
- <template #buttons>
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- :icon="useRenderIcon(Sync)"
|
|
|
- :loading="syncing"
|
|
|
- @click="handleSyncDevices"
|
|
|
+ <!-- 已上线同步模块 -->
|
|
|
+ <div class="section">
|
|
|
+ <div class="section-header">
|
|
|
+ <span class="section-title">同步模块</span>
|
|
|
+ <span class="section-desc">点击同步按钮从哈哈平台拉取最新数据</span>
|
|
|
+ </div>
|
|
|
+ <div class="module-grid">
|
|
|
+ <div
|
|
|
+ v-for="mod in activeModules"
|
|
|
+ :key="mod.key"
|
|
|
+ class="sync-card active"
|
|
|
>
|
|
|
- 同步设备
|
|
|
- </el-button>
|
|
|
- <el-button
|
|
|
- type="primary"
|
|
|
- :icon="useRenderIcon(Sync)"
|
|
|
- :loading="syncing"
|
|
|
- @click="handleSyncProducts"
|
|
|
- >
|
|
|
- 同步商品
|
|
|
- </el-button>
|
|
|
- </template>
|
|
|
- <template v-slot="{ size, dynamicColumns }">
|
|
|
- <pure-table
|
|
|
- ref="tableRef"
|
|
|
- align-whole="center"
|
|
|
- showOverflowTooltip
|
|
|
- table-layout="auto"
|
|
|
- :loading="loading"
|
|
|
- :size="size"
|
|
|
- adaptive
|
|
|
- :adaptiveConfig="{ offsetBottom: 108 }"
|
|
|
- :data="dataList"
|
|
|
- :columns="dynamicColumns"
|
|
|
- :pagination="{ ...pagination, size }"
|
|
|
- :header-cell-style="{
|
|
|
- background: 'var(--el-fill-color-light)',
|
|
|
- color: 'var(--el-text-color-primary)'
|
|
|
- }"
|
|
|
- @page-size-change="handleSizeChange"
|
|
|
- @page-current-change="handleCurrentChange"
|
|
|
- >
|
|
|
- <template #operation="{ row }">
|
|
|
+ <div class="card-top">
|
|
|
+ <div class="card-icon" :style="{ backgroundColor: mod.bgColor, color: mod.color }">
|
|
|
+ <IconifyIconOffline :icon="mod.icon" width="26" />
|
|
|
+ </div>
|
|
|
+ <div class="card-meta">
|
|
|
+ <div class="card-title">{{ mod.title }}</div>
|
|
|
+ <div class="card-desc">{{ mod.description }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-bottom">
|
|
|
+ <div class="card-status">
|
|
|
+ <span class="status-label">上次成功同步</span>
|
|
|
+ <span class="last-time">
|
|
|
+ {{ mod.lastSuccessTime ? formatTime(mod.lastSuccessTime) : '暂无记录' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
<el-button
|
|
|
- class="reset-margin"
|
|
|
- link
|
|
|
- type="primary"
|
|
|
- :size="size"
|
|
|
- :icon="useRenderIcon(View)"
|
|
|
- @click="handleViewLogs(row)"
|
|
|
+ :type="syncing && currentSyncKey === mod.key ? 'primary' : 'default'"
|
|
|
+ :loading="syncing && currentSyncKey === mod.key"
|
|
|
+ :disabled="syncing && currentSyncKey !== mod.key"
|
|
|
+ size="small"
|
|
|
+ @click="handleSync(mod)"
|
|
|
>
|
|
|
- 查看日志
|
|
|
+ <el-icon v-if="!(syncing && currentSyncKey === mod.key)" style="margin-right: 4px">
|
|
|
+ <IconifyIconOffline icon="ri:refresh-line" width="14" />
|
|
|
+ </el-icon>
|
|
|
+ {{ syncing && currentSyncKey === mod.key ? '同步中' : '立即同步' }}
|
|
|
</el-button>
|
|
|
- </template>
|
|
|
- </pure-table>
|
|
|
- </template>
|
|
|
- </PureTableBar>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预留同步模块(暂不展示) -->
|
|
|
+ <!-- <div v-if="upcomingModules.length" class="section upcoming-section">
|
|
|
+ <div class="section-header">
|
|
|
+ <span class="section-title">更多同步</span>
|
|
|
+ <span class="section-desc">以下模块正在规划开发中</span>
|
|
|
+ </div>
|
|
|
+ <div class="module-grid upcoming-grid">
|
|
|
+ <div
|
|
|
+ v-for="mod in upcomingModules"
|
|
|
+ :key="mod.key"
|
|
|
+ class="sync-card upcoming"
|
|
|
+ >
|
|
|
+ <div class="card-top">
|
|
|
+ <div class="card-icon" :style="{ backgroundColor: mod.bgColor, color: mod.color }">
|
|
|
+ <IconifyIconOffline :icon="mod.icon" width="26" />
|
|
|
+ </div>
|
|
|
+ <div class="card-meta">
|
|
|
+ <div class="card-title">{{ mod.title }}</div>
|
|
|
+ <div class="card-desc">{{ mod.description }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="card-bottom">
|
|
|
+ <div class="card-status">
|
|
|
+ <el-tag type="info" size="small" effect="plain" round>
|
|
|
+ {{ mod.upcomingText || '即将上线' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <el-button size="small" disabled>敬请期待</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div> -->
|
|
|
+
|
|
|
+ <!-- 同步记录表格 -->
|
|
|
+ <div class="section table-section">
|
|
|
+ <el-form
|
|
|
+ ref="formRef"
|
|
|
+ :inline="true"
|
|
|
+ :model="form"
|
|
|
+ class="search-form"
|
|
|
+ >
|
|
|
+ <el-form-item label="同步类型:" prop="syncType">
|
|
|
+ <el-select
|
|
|
+ v-model="form.syncType"
|
|
|
+ placeholder="全部类型"
|
|
|
+ clearable
|
|
|
+ class="w-[160px]!"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="mod in activeModules"
|
|
|
+ :key="mod.key"
|
|
|
+ :label="mod.title"
|
|
|
+ :value="mod.key"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="状态:" prop="status">
|
|
|
+ <el-select
|
|
|
+ v-model="form.status"
|
|
|
+ placeholder="全部状态"
|
|
|
+ clearable
|
|
|
+ class="w-[140px]!"
|
|
|
+ >
|
|
|
+ <el-option label="进行中" :value="1" />
|
|
|
+ <el-option label="成功" :value="2" />
|
|
|
+ <el-option label="失败" :value="3" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ :icon="useRenderIcon('ri/search-line')"
|
|
|
+ :loading="loading"
|
|
|
+ @click="onSearch"
|
|
|
+ >
|
|
|
+ 搜索
|
|
|
+ </el-button>
|
|
|
+ <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
|
|
+ 重置
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <PureTableBar
|
|
|
+ title="同步记录"
|
|
|
+ :columns="columns"
|
|
|
+ @refresh="onSearch"
|
|
|
+ >
|
|
|
+ <template v-slot="{ size, dynamicColumns }">
|
|
|
+ <pure-table
|
|
|
+ ref="tableRef"
|
|
|
+ row-key="id"
|
|
|
+ align-whole="center"
|
|
|
+ showOverflowTooltip
|
|
|
+ table-layout="auto"
|
|
|
+ :loading="loading"
|
|
|
+ :size="size"
|
|
|
+ :height="560"
|
|
|
+ :data="dataList"
|
|
|
+ :columns="dynamicColumns"
|
|
|
+ :pagination="{ ...pagination, size }"
|
|
|
+ :header-cell-style="{
|
|
|
+ background: 'var(--el-fill-color-light)',
|
|
|
+ color: 'var(--el-text-color-primary)'
|
|
|
+ }"
|
|
|
+ @page-size-change="handleSizeChange"
|
|
|
+ @page-current-change="handleCurrentChange"
|
|
|
+ >
|
|
|
+ <template #operation="{ row }">
|
|
|
+ <el-button
|
|
|
+ class="reset-margin"
|
|
|
+ link
|
|
|
+ type="primary"
|
|
|
+ :size="size"
|
|
|
+ :icon="useRenderIcon(View)"
|
|
|
+ @click="handleViewLogs(row)"
|
|
|
+ >
|
|
|
+ 查看日志
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </pure-table>
|
|
|
+ </template>
|
|
|
+ </PureTableBar>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
-.main-content {
|
|
|
- margin: 24px 24px 0 !important;
|
|
|
+.sync-page {
|
|
|
+ padding: 20px;
|
|
|
+ background: var(--el-bg-color-page, #f5f7fa);
|
|
|
+ min-height: calc(100vh - 100px);
|
|
|
+}
|
|
|
+
|
|
|
+/* ========== 页面头部 ========== */
|
|
|
+.page-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-title {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ margin: 0 0 4px 0;
|
|
|
+ letter-spacing: -0.3px;
|
|
|
+}
|
|
|
+
|
|
|
+.page-subtitle {
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* ========== 统计概览 ========== */
|
|
|
+.stats-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 24px;
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 18px 28px;
|
|
|
+ margin-bottom: 24px;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
+}
|
|
|
+
|
|
|
+.stat-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ min-width: 80px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-value {
|
|
|
+ font-size: 22px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
+
|
|
|
+ &.success {
|
|
|
+ color: var(--el-color-success);
|
|
|
+ }
|
|
|
+ &.warning {
|
|
|
+ color: var(--el-color-warning);
|
|
|
+ }
|
|
|
+ &.info {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-regular);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.stat-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-divider {
|
|
|
+ width: 1px;
|
|
|
+ height: 32px;
|
|
|
+ background: var(--el-border-color-lighter);
|
|
|
+}
|
|
|
+
|
|
|
+/* ========== 区块通用 ========== */
|
|
|
+.section {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+}
|
|
|
+
|
|
|
+.section-desc {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-placeholder);
|
|
|
+}
|
|
|
+
|
|
|
+/* ========== 同步模块卡片 ========== */
|
|
|
+.module-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.upcoming-grid {
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
+}
|
|
|
+
|
|
|
+.sync-card {
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 20px;
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
|
+ transition: box-shadow 0.25s, border-color 0.25s, transform 0.2s;
|
|
|
+
|
|
|
+ &.active:hover {
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
|
+ border-color: var(--el-border-color);
|
|
|
+ transform: translateY(-1px);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.upcoming {
|
|
|
+ opacity: 0.7;
|
|
|
+ border-style: dashed;
|
|
|
+
|
|
|
+ .card-icon {
|
|
|
+ opacity: 0.6;
|
|
|
+ }
|
|
|
+ .card-desc {
|
|
|
+ color: var(--el-text-color-placeholder);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card-top {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 14px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-icon {
|
|
|
+ flex-shrink: 0;
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ border-radius: 12px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-meta {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.card-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--el-text-color-primary);
|
|
|
+ margin-bottom: 4px;
|
|
|
}
|
|
|
|
|
|
+.card-desc {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+ line-height: 1.5;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.card-bottom {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding-top: 14px;
|
|
|
+ border-top: 1px solid var(--el-border-color-extra-light, #f0f0f0);
|
|
|
+}
|
|
|
+
|
|
|
+.card-status {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--el-text-color-secondary);
|
|
|
+}
|
|
|
+
|
|
|
+.last-time {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--el-text-color-placeholder);
|
|
|
+}
|
|
|
+
|
|
|
+/* ========== 搜索表单 ========== */
|
|
|
.search-form {
|
|
|
+ background: var(--el-bg-color);
|
|
|
+ padding: 16px 20px 4px;
|
|
|
+ border-radius: 12px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ border: 1px solid var(--el-border-color-lighter);
|
|
|
+
|
|
|
:deep(.el-form-item) {
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/* ========== 更多同步区块 ========== */
|
|
|
+.upcoming-section {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ========== 响应式 ========== */
|
|
|
+@media screen and (max-width: 768px) {
|
|
|
+ .sync-page {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-bar {
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 14px 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-divider {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-item {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 60px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .module-grid,
|
|
|
+ .upcoming-grid {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|