Jelajahi Sumber

feat(admin): 新增报告解析进度页面(查看/重试/回填)

- 新增 ReportParseStatus.vue:解析状态筛选、草稿列表、手动重试
- 新增 api/reportParse.js;注册路由 report-parse-status 与侧边菜单
- npm run build 产物版本 1.0.1588
Xiaogang Liao 11 jam lalu
induk
melakukan
e5323d509b

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-bb57c6e0fa99d876573bbdcd94a965e91684dc01
+770a6ac4a2dcad36f7675b5cd08a4ebafcc01bb0

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1587",
+  "version": "1.0.1588",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 23 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,29 @@
 
 ---
 
+## v1.0.1588 (2026-10-12)
+
+### 重构
+- DimensionSubDims 组件下沉到分包
+
+### 其他
+-    - pages/wealth/components/DimensionSubDims.vue
+-    - pages/action-detail/components/DimensionSubDims.vue
+-    原因:两者均为独立 subpackage root,微信禁止跨分包 import
+- 2. 更新 import 路径为本地相对路径('./components/DimensionSubDims.vue')
+-    - pages/wealth/index.vue
+-    - pages/action-detail/index.vue(同时去重 DimensionIntroCard 重复注册)
+- 3. pages/reminder-config/index.vue: 修复 :key 表达式警告
+-    - ch.type + '_' + index → getChannelKey(ch, index)
+-    - 'p-' + pIndex → getPraiseKey(pIndex)
+-    符合 uni-app 非 H5 平台 :key 规范(需使用方法调用而非表达式)
+- 
+
+### Bug 修复
+- 打破 ProductService↔NutritionProductService 循环依赖
+- 补注入 NutritionProductService 并修正 syncFromProduct 调用位置
+
+
 ## v1.0.1587 (2026-10-12)
 
 ### Bug 修复

+ 24 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1587
+> 当前版本: v1.0.1588
 
 ## 历史版本
 
@@ -8,6 +8,29 @@
 
 ---
 
+## v1.0.1588 (2026-10-12)
+
+### 重构
+- DimensionSubDims 组件下沉到分包
+
+### 其他
+-    - pages/wealth/components/DimensionSubDims.vue
+-    - pages/action-detail/components/DimensionSubDims.vue
+-    原因:两者均为独立 subpackage root,微信禁止跨分包 import
+- 2. 更新 import 路径为本地相对路径('./components/DimensionSubDims.vue')
+-    - pages/wealth/index.vue
+-    - pages/action-detail/index.vue(同时去重 DimensionIntroCard 重复注册)
+- 3. pages/reminder-config/index.vue: 修复 :key 表达式警告
+-    - ch.type + '_' + index → getChannelKey(ch, index)
+-    - 'p-' + pIndex → getPraiseKey(pIndex)
+-    符合 uni-app 非 H5 平台 :key 规范(需使用方法调用而非表达式)
+- 
+
+### Bug 修复
+- 打破 ProductService↔NutritionProductService 循环依赖
+- 补注入 NutritionProductService 并修正 syncFromProduct 调用位置
+
+
 ## v1.0.1587 (2026-10-12)
 
 ### Bug 修复

+ 26 - 0
cfc-web/src/api/reportParse.js

@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// ========== 报告解析进度 ==========
+export function listReportParseDrafts(params) {
+  return request({
+    url: '/api/admin/report-parse/list',
+    method: 'post',
+    data: params
+  })
+}
+
+export function retryReportParseDraft(draftId) {
+  return request({
+    url: '/api/admin/report-parse/retry',
+    method: 'post',
+    data: { draftId }
+  })
+}
+
+export function backfillReportFood(reportId) {
+  return request({
+    url: '/api/admin/report-parse/backfill-food',
+    method: 'post',
+    data: { reportId }
+  })
+}

+ 6 - 0
cfc-web/src/router/index.js

@@ -654,6 +654,12 @@ const routes = [
         component: () => import('@/views/health/HealthReportAudit.vue'),
         meta: { title: '报告审核', perm: 'report:audit' }
       },
+      {
+        path: 'report-parse-status',
+        name: 'ReportParseStatus',
+        component: () => import('@/views/admin/ReportParseStatus.vue'),
+        meta: { title: '解析进度', perm: 'health:reports' }
+      },
       {
         path: 'seasonal-foods',
         name: 'SeasonalFoods',

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -243,6 +243,7 @@ export default {
             { path: '/health-reports', label: '健康报告', icon: 'el-icon-document', perm: 'health:reports' },
             { path: '/report-management', label: '报告管理', icon: 'el-icon-document', perm: 'health:reports' },
             { path: '/report-audit', label: '报告审核', icon: 'el-icon-document-checked', perm: 'report:audit' },
+            { path: '/report-parse-status', label: '解析进度', icon: 'el-icon-data-line', perm: 'health:reports' },
             { path: '/health-indicators', label: '健康指标', icon: 'el-icon-data-line', perm: 'health:indicators' },
             { path: '/health-checkins', label: '健康打卡', icon: 'el-icon-s-order', perm: 'health:checkins' },
             { path: '/emotion-alert', label: '情绪告警', icon: 'el-icon-warning', perm: 'health:checkins' },

+ 157 - 0
cfc-web/src/views/admin/ReportParseStatus.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="report-parse-status admin-page">
+    <div class="header admin-page-header">
+      <h2 class="admin-page-title">解析进度</h2>
+    </div>
+
+    <div class="filter-bar">
+      <el-select v-model="query.parseStatus" placeholder="解析状态" clearable style="width: 140px">
+        <el-option label="全部" value="" />
+        <el-option label="采集中" value="collecting" />
+        <el-option label="成功" value="completed" />
+        <el-option label="失败" value="failed" />
+      </el-select>
+      <el-input
+        v-model="query.keyword"
+        placeholder="关键词(文件名或用户ID)"
+        style="width: 220px"
+        clearable
+        @keyup.enter.native="handleSearch"
+      />
+      <el-button type="primary" @click="handleSearch">查询</el-button>
+      <el-button @click="handleReset">重置</el-button>
+    </div>
+
+    <div class="adm-table-wrap">
+      <el-table :data="list" v-loading="loading" border stripe height="100%">
+        <el-table-column prop="draftId" label="草稿ID" width="90" />
+        <el-table-column prop="userId" label="用户ID" width="90" />
+        <el-table-column prop="reportType" label="报告类型" width="130" />
+        <el-table-column prop="originalFilename" label="文件名" min-width="200" show-overflow-tooltip />
+        <el-table-column label="解析状态" width="100">
+          <template slot-scope="{ row }">
+            <el-tag v-if="row.parseStatus === 'collecting'" type="info" size="small">采集中</el-tag>
+            <el-tag v-else-if="row.parseStatus === 'completed'" type="success" size="small">成功</el-tag>
+            <el-tag v-else-if="row.parseStatus === 'failed'" type="danger" size="small">失败</el-tag>
+            <el-tag v-else size="small">未触发</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="parseMethod" label="解析方式" width="110" />
+        <el-table-column prop="parseAttempt" label="尝试次数" width="90" />
+        <el-table-column label="下次重试" width="160">
+          <template slot-scope="{ row }">{{ formatTime(row.nextRetryAt) }}</template>
+        </el-table-column>
+        <el-table-column prop="parseError" label="错误信息" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="confirmedReportId" label="入库报告ID" width="110" />
+        <el-table-column label="更新时间" width="160">
+          <template slot-scope="{ row }">{{ formatTime(row.updatedAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="100" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button
+              size="mini"
+              type="primary"
+              :disabled="row.parseStatus === 'completed'"
+              @click="handleRetry(row)"
+            >重试</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <el-pagination
+      v-if="total > size"
+      @current-change="onPageChange"
+      :current-page="page"
+      :page-size="size"
+      :total="total"
+      layout="total, prev, pager, next"
+      class="pagination-wrap"
+    />
+  </div>
+</template>
+
+<script>
+import { listReportParseDrafts, retryReportParseDraft } from '@/api/reportParse'
+
+export default {
+  name: 'ReportParseStatus',
+  data() {
+    return {
+      list: [],
+      loading: false,
+      page: 1,
+      size: 20,
+      total: 0,
+      query: { parseStatus: '', keyword: '', reportType: '' }
+    }
+  },
+  created() {
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      this.loading = true
+      try {
+        const params = {
+          page: this.page,
+          size: this.size,
+          parseStatus: this.query.parseStatus,
+          reportType: this.query.reportType,
+          keyword: this.query.keyword
+        }
+        const res = await listReportParseDrafts(params)
+        const data = res.data || {}
+        this.list = data.records || []
+        this.total = data.total || 0
+      } catch (e) {
+        this.$message.error('加载解析进度失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSearch() {
+      this.page = 1
+      this.loadList()
+    },
+    handleReset() {
+      this.query.parseStatus = ''
+      this.query.keyword = ''
+      this.page = 1
+      this.loadList()
+    },
+    onPageChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    async handleRetry(row) {
+      try {
+        await this.$confirm(`确认重试草稿 #${row.draftId} 的解析?`, '提示', { type: 'warning' })
+      } catch (e) {
+        return
+      }
+      try {
+        await retryReportParseDraft(row.draftId)
+        this.$message.success('已触发重试')
+        this.loadList()
+      } catch (e) {
+        this.$message.error('重试失败')
+      }
+    },
+    formatTime(t) {
+      if (!t) return '—'
+      return t.replace ? t.replace('T', ' ').substring(0, 19) : t
+    }
+  }
+}
+</script>
+
+<style scoped>
+.report-parse-status {
+  padding: 20px;
+}
+.pagination-wrap {
+  margin-top: 16px;
+  text-align: right;
+}
+</style>