Forráskód Böngészése

操作日志、数据同步页面调试

skyline 1 hónapja
szülő
commit
54eb7c6dab

+ 1 - 1
haha-admin-web/src/router/modules/operation.ts

@@ -26,7 +26,7 @@ export default {
       }
     },
     {
-      path: "/distribution",
+      path: "/operation/distribution-map",
       name: "DistributionMap",
       component: () => import("@/views/distribution/index.vue"),
       meta: {

+ 437 - 131
haha-admin-web/src/views/sync/index.vue

@@ -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>

+ 138 - 31
haha-admin-web/src/views/sync/utils/hook.tsx

@@ -1,8 +1,15 @@
 import dayjs from "dayjs";
 import { message } from "@/utils/message";
-import { syncDevices, syncProducts, getSyncRecords, getSyncLogs, getSyncStatistics } from "@/api/sync";
+import {
+  syncDevices,
+  syncProducts,
+  getSyncRecords,
+  getSyncLogs,
+  getSyncStatistics
+} from "@/api/sync";
 import type { PaginationProps } from "@pureadmin/table";
-import { onMounted, reactive, ref, toRaw } from "vue";
+import type { SyncModuleItem, SearchFormProps } from "./types";
+import { onMounted, reactive, ref } from "vue";
 
 export function useSync() {
   const form = reactive<SearchFormProps>({
@@ -11,6 +18,7 @@ export function useSync() {
   });
   const loading = ref(true);
   const syncing = ref(false);
+  const currentSyncKey = ref<string>("");
   const dataList = ref([]);
   const statistics = ref({
     totalSync: 0,
@@ -25,12 +33,87 @@ export function useSync() {
     background: true
   });
 
-  const syncTypeMap = {
+  // ========== 同步模块配置 ==========
+  const syncModules = ref<SyncModuleItem[]>([
+    {
+      key: "device",
+      title: "设备同步",
+      description: "从哈哈平台拉取设备列表,同步设备基本信息与状态",
+      icon: "ri:computer-line",
+      color: "#4f46e5",
+      bgColor: "#eef2ff",
+      active: true,
+      lastSyncTime: "",
+      lastSyncStatus: undefined,
+      lastSyncCount: undefined,
+      lastSuccessTime: ""
+    },
+    {
+      key: "product",
+      title: "商品同步",
+      description: "从哈哈平台商家商品库拉取商品列表,同步商品信息与价格",
+      icon: "ri:shopping-bag-3-line",
+      color: "#059669",
+      bgColor: "#ecfdf5",
+      active: true,
+      lastSyncTime: "",
+      lastSyncStatus: undefined,
+      lastSyncCount: undefined,
+      lastSuccessTime: ""
+    },
+    // ========== 预留未来同步模块 ==========
+    {
+      key: "order",
+      title: "订单同步",
+      description: "从哈哈平台同步历史订单与交易流水数据",
+      icon: "ri:file-list-3-line",
+      color: "#d97706",
+      bgColor: "#fffbeb",
+      active: false,
+      upcomingText: "即将上线"
+    },
+    {
+      key: "shop",
+      title: "门店同步",
+      description: "从哈哈平台同步门店信息、营业时间与地理位置",
+      icon: "ri:store-2-line",
+      color: "#dc2626",
+      bgColor: "#fef2f2",
+      active: false,
+      upcomingText: "即将上线"
+    },
+    {
+      key: "coupon",
+      title: "优惠券同步",
+      description: "从哈哈平台同步优惠券模板与活动发放规则",
+      icon: "ri:ticket-2-line",
+      color: "#7c3aed",
+      bgColor: "#f5f3ff",
+      active: false,
+      upcomingText: "规划中"
+    },
+    {
+      key: "inventory",
+      title: "库存同步",
+      description: "从哈哈平台同步各设备实时库存与补货记录",
+      icon: "ri:archive-2-line",
+      color: "#0891b2",
+      bgColor: "#ecfeff",
+      active: false,
+      upcomingText: "规划中"
+    }
+  ]);
+
+  const syncTypeMap: Record<string, string> = {
     device: "设备同步",
-    product: "商品同步"
+    product: "商品同步",
+    order: "订单同步",
+    shop: "门店同步",
+    coupon: "优惠券同步",
+    inventory: "库存同步"
   };
 
-  const statusMap = {
+  const statusMap: Record<number, { text: string; type: string }> = {
     1: { text: "进行中", type: "primary" },
     2: { text: "成功", type: "success" },
     3: { text: "失败", type: "danger" }
@@ -40,7 +123,7 @@ export function useSync() {
     {
       label: "记录ID",
       prop: "id",
-      width: 80
+      width: 180
     },
     {
       label: "同步类型",
@@ -69,7 +152,7 @@ export function useSync() {
       minWidth: 100,
       cellRenderer: ({ row }) => {
         const item = statusMap[row.status] || { text: "未知", type: "info" };
-        return <el-tag type={item.type}>{item.text}</el-tag>;
+        return <el-tag type={item.type as any}>{item.text}</el-tag>;
       }
     },
     {
@@ -88,7 +171,8 @@ export function useSync() {
       label: "耗时(s)",
       prop: "duration",
       minWidth: 90,
-      formatter: ({ duration }) => duration ? (duration / 1000).toFixed(2) : "-"
+      formatter: ({ duration }) =>
+        duration ? (duration / 1000).toFixed(2) : "-"
     },
     {
       label: "操作",
@@ -98,6 +182,12 @@ export function useSync() {
     }
   ];
 
+  // ========== 同步执行映射 ==========
+  const syncActionMap: Record<string, () => Promise<any>> = {
+    device: syncDevices,
+    product: syncProducts
+  };
+
   async function onSearch() {
     loading.value = true;
     try {
@@ -105,21 +195,41 @@ export function useSync() {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
       };
-      
-      // 只添加非空的搜索条件
+
       if (form.syncType) searchParams.syncType = form.syncType;
       if (form.status !== undefined) searchParams.status = form.status;
-      
+
       const { data } = await getSyncRecords(searchParams);
       if (data) {
         dataList.value = data.list || [];
         pagination.total = data.total || 0;
+        // 更新模块卡片的最近同步信息
+        updateModuleLastSync(data.list);
       }
     } finally {
       loading.value = false;
     }
   }
 
+  /** 从记录列表更新模块卡片的最新状态 */
+  function updateModuleLastSync(records: any[]) {
+    if (!records || records.length === 0) return;
+    for (const mod of syncModules.value) {
+      if (!mod.active) continue;
+      // 找到该模块最近一条成功记录的时间
+      const successRecord = records.find((r: any) => r.syncType === mod.key && r.status === 2);
+      if (successRecord) {
+        mod.lastSuccessTime = successRecord.startTime;
+      }
+      const latestRecord = records.find((r: any) => r.syncType === mod.key);
+      if (latestRecord) {
+        mod.lastSyncTime = latestRecord.startTime;
+        mod.lastSyncStatus = latestRecord.status;
+        mod.lastSyncCount = latestRecord.totalCount;
+      }
+    }
+  }
+
   function resetForm(formEl) {
     if (!formEl) return;
     formEl.resetFields();
@@ -127,28 +237,21 @@ export function useSync() {
     onSearch();
   }
 
-  async function handleSyncDevices() {
-    syncing.value = true;
-    try {
-      const { code, message: msg } = await syncDevices();
-      if (code === 0) {
-        message("设备同步成功", { type: "success" });
-        onSearch();
-        getStatistics();
-      } else {
-        message(msg || "同步失败", { type: "error" });
-      }
-    } finally {
-      syncing.value = false;
+  /** 通用同步执行:根据模块 key 调用对应 API */
+  async function handleSync(mod: SyncModuleItem) {
+    if (!mod.active) return;
+    const action = syncActionMap[mod.key];
+    if (!action) {
+      message(`同步功能「${mod.title}」暂未实现`, { type: "warning" });
+      return;
     }
-  }
 
-  async function handleSyncProducts() {
+    currentSyncKey.value = mod.key;
     syncing.value = true;
     try {
-      const { code, message: msg } = await syncProducts();
-      if (code === 0) {
-        message("商品同步成功", { type: "success" });
+      const { code, message: msg } = await action();
+      if (code === 0 || code === 200) {
+        message(`${mod.title}成功`, { type: "success" });
         onSearch();
         getStatistics();
       } else {
@@ -156,6 +259,7 @@ export function useSync() {
       }
     } finally {
       syncing.value = false;
+      currentSyncKey.value = "";
     }
   }
 
@@ -190,14 +294,17 @@ export function useSync() {
     form,
     loading,
     syncing,
+    currentSyncKey,
     columns,
     dataList,
     statistics,
+    syncModules,
+    syncTypeMap,
+    statusMap,
     pagination,
     onSearch,
     resetForm,
-    handleSyncDevices,
-    handleSyncProducts,
+    handleSync,
     handleViewLogs,
     handleSizeChange,
     handleCurrentChange

+ 29 - 1
haha-admin-web/src/views/sync/utils/types.ts

@@ -21,4 +21,32 @@ interface SyncRecordProps {
   createTime: string;
 }
 
-export type { SearchFormProps, SyncRecordProps };
+/** 同步模块配置项 */
+interface SyncModuleItem {
+  /** 模块唯一标识,对应 API 的 syncType */
+  key: string;
+  /** 显示名称 */
+  title: string;
+  /** 模块描述 */
+  description: string;
+  /** 图标(Remix Icon 名称) */
+  icon: string;
+  /** 主题色,用于图标背景和强调色 */
+  color: string;
+  /** 图标背景色(淡色) */
+  bgColor: string;
+  /** 是否已上线 */
+  active: boolean;
+  /** 未上线时的提示文案 */
+  upcomingText?: string;
+  /** 最近一次同步时间 */
+  lastSyncTime?: string;
+  /** 最近一次同步状态: 1=进行中 2=成功 3=失败 */
+  lastSyncStatus?: number;
+  /** 最近一次同步数量 */
+  lastSyncCount?: number;
+  /** 上次成功同步时间 */
+  lastSuccessTime?: string;
+}
+
+export type { SearchFormProps, SyncRecordProps, SyncModuleItem };

+ 47 - 28
haha-admin-web/src/views/system/operation-log/index.vue

@@ -30,7 +30,10 @@ const {
   handleClearAll,
   handleViewDetail,
   handleSizeChange,
-  handleCurrentChange
+  handleCurrentChange,
+  detailVisible,
+  detailData,
+  detailLoading
 } = useOperationLog();
 </script>
 
@@ -101,33 +104,6 @@ const {
       </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.total }}</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.success }}</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-danger">{{ statistics.failed }}</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.today }}</div>
-          <div class="text-gray-500">今日日志</div>
-        </div>
-      </el-card>
-    </div>
-
     <PureTableBar
       title="操作日志"
       :columns="columns"
@@ -151,6 +127,7 @@ const {
       <template v-slot="{ size, dynamicColumns }">
         <pure-table
           ref="tableRef"
+          row-key="id"
           align-whole="center"
           showOverflowTooltip
           table-layout="auto"
@@ -184,6 +161,48 @@ const {
         </pure-table>
       </template>
     </PureTableBar>
+
+    <!-- 详情弹窗 -->
+    <el-dialog
+      v-model="detailVisible"
+      title="操作日志详情"
+      width="700px"
+      destroy-on-close
+    >
+      <div v-loading="detailLoading">
+        <template v-if="detailData">
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="日志ID">{{ detailData.id }}</el-descriptions-item>
+            <el-descriptions-item label="操作模块">{{ detailData.module }}</el-descriptions-item>
+            <el-descriptions-item label="操作类型">{{ detailData.operationType }}</el-descriptions-item>
+            <el-descriptions-item label="操作人">{{ detailData.adminName }}</el-descriptions-item>
+            <el-descriptions-item label="请求URL" :span="2">{{ detailData.requestUrl }}</el-descriptions-item>
+            <el-descriptions-item label="请求方法">{{ detailData.requestMethod }}</el-descriptions-item>
+            <el-descriptions-item label="耗时(ms)">{{ detailData.costTime }}</el-descriptions-item>
+            <el-descriptions-item label="IP地址">{{ detailData.ip }}</el-descriptions-item>
+            <el-descriptions-item label="操作地点">{{ detailData.address }}</el-descriptions-item>
+            <el-descriptions-item label="浏览器">{{ detailData.browser }}</el-descriptions-item>
+            <el-descriptions-item label="操作系统">{{ detailData.os }}</el-descriptions-item>
+            <el-descriptions-item label="状态">
+              <el-tag :type="detailData.status === 1 ? 'success' : 'danger'">
+                {{ detailData.status === 1 ? '成功' : '失败' }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="操作时间">{{ detailData.createTime }}</el-descriptions-item>
+            <el-descriptions-item v-if="detailData.summary" label="操作概要" :span="2">{{ detailData.summary }}</el-descriptions-item>
+            <el-descriptions-item v-if="detailData.errorMsg" label="错误信息" :span="2">
+              <span class="text-danger">{{ detailData.errorMsg }}</span>
+            </el-descriptions-item>
+            <el-descriptions-item v-if="detailData.requestParams" label="请求参数" :span="2">
+              <pre class="whitespace-pre-wrap break-all text-xs">{{ detailData.requestParams }}</pre>
+            </el-descriptions-item>
+            <el-descriptions-item v-if="detailData.responseResult" label="响应结果" :span="2">
+              <pre class="whitespace-pre-wrap break-all text-xs">{{ detailData.responseResult }}</pre>
+            </el-descriptions-item>
+          </el-descriptions>
+        </template>
+      </div>
+    </el-dialog>
   </div>
 </template>
 

+ 20 - 7
haha-admin-web/src/views/system/operation-log/utils/hook.tsx

@@ -43,7 +43,7 @@ export function useOperationLog() {
     {
       label: "日志ID",
       prop: "id",
-      width: 80
+      width: 180
     },
     {
       label: "操作模块",
@@ -52,12 +52,12 @@ export function useOperationLog() {
     },
     {
       label: "操作类型",
-      prop: "operation",
+      prop: "operationType",
       minWidth: 100
     },
     {
       label: "操作人",
-      prop: "operatorName",
+      prop: "adminName",
       minWidth: 100
     },
     {
@@ -73,7 +73,7 @@ export function useOperationLog() {
     },
     {
       label: "耗时(ms)",
-      prop: "duration",
+      prop: "costTime",
       minWidth: 90
     },
     {
@@ -159,9 +159,19 @@ export function useOperationLog() {
     }
   }
 
+  const detailVisible = ref(false);
+  const detailData = ref<any>(null);
+  const detailLoading = ref(false);
+
   async function handleViewDetail(row) {
-    const { data } = await getOperationLogDetail(row.id);
-    console.log("详情:", data);
+    detailVisible.value = true;
+    detailLoading.value = true;
+    try {
+      const { data } = await getOperationLogDetail(row.id);
+      detailData.value = data;
+    } finally {
+      detailLoading.value = false;
+    }
   }
 
   function handleSizeChange(val: number) {
@@ -204,6 +214,9 @@ export function useOperationLog() {
     handleClearAll,
     handleViewDetail,
     handleSizeChange,
-    handleCurrentChange
+    handleCurrentChange,
+    detailVisible,
+    detailData,
+    detailLoading
   };
 }

+ 6 - 6
haha-admin-web/src/views/system/operation-log/utils/types.ts

@@ -11,20 +11,20 @@ interface SearchFormProps {
 interface LogItemProps {
   id: number;
   module: string;
-  operation: string;
-  method: string;
+  operationType: string;
+  operationMethod: string;
   requestUrl: string;
   requestParams?: string;
   responseResult?: string;
   ip: string;
-  location?: string;
+  address?: string;
   browser?: string;
   os?: string;
   status: number;
   errorMsg?: string;
-  operatorId: number;
-  operatorName: string;
-  duration: number;
+  adminId: number;
+  adminName: string;
+  costTime: number;
   createTime: string;
 }