Procházet zdrojové kódy

feat(knowledge): 知识库管理页统一展示案例报告与菌群文章(sourceType 筛选/标识+全文详情),删除健康知识库页与路由

Xiaogang Liao před 5 dny
rodič
revize
50494b9f80

+ 9 - 1
cfc-web/src/components/KnowledgeTable/index.vue

@@ -14,6 +14,13 @@
         <span>{{ scope.row.content | truncate(100) }}</span>
       </template>
     </el-table-column>
+    <el-table-column label="来源方式" width="110" align="center">
+      <template slot-scope="{ row }">
+        <el-tag size="mini" :type="row.sourceType === 'article' ? 'success' : 'info'">
+          {{ row.sourceType === 'article' ? '菌群文章' : '案例报告' }}
+        </el-tag>
+      </template>
+    </el-table-column>
     <el-table-column prop="sort" label="排序" width="80" align="center" />
     <el-table-column prop="status" label="状态" width="80" align="center">
       <template slot-scope="scope">
@@ -22,11 +29,12 @@
         </el-tag>
       </template>
     </el-table-column>
-    <el-table-column label="操作" width="200" fixed="right">
+    <el-table-column label="操作" width="220" fixed="right">
       <template slot-scope="scope">
         <el-button size="small" type="text" @click="$emit('edit', scope.row)">编辑</el-button>
         <el-button size="small" type="text" @click="$emit('bind-dimension', scope.row)">维度</el-button>
         <el-button size="small" type="text" @click="$emit('bind-tag', scope.row)">标签</el-button>
+        <el-button size="small" type="text" @click="$emit('view-full', scope.row)">查看全文</el-button>
         <el-button size="small" type="text" style="color: #f56c6c" @click="$emit('delete', scope.row)">删除</el-button>
       </template>
     </el-table-column>

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

@@ -767,12 +767,6 @@ const routes = [
         component: () => import('@/views/admin/knowledge'),
         meta: { title: '知识库管理', perm: 'knowledge:base' }
       },
-      {
-        path: 'health-knowledge',
-        name: 'HealthKnowledge',
-        component: () => import('@/views/admin/health-knowledge'),
-        meta: { title: '健康知识库管理', perm: 'knowledge:base' }
-      },
       {
         path: 'knowledge-base/unified',
         name: 'UnifiedKnowledgeBase',

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

@@ -235,7 +235,6 @@ export default {
             { path: '/knowledge-tags', label: '知识标签', icon: 'el-icon-price-tag', perm: 'articles:categories' },
             { path: '/knowledge-base', label: '知识库', icon: 'el-icon-reading', perm: 'knowledge:base' },
             { path: '/knowledge-base/unified', label: '统一知识库', icon: 'el-icon-notebook-2', perm: 'knowledge:base' },
-            { path: '/health-knowledge', label: '健康知识库', icon: 'el-icon-first-aid-kit', perm: 'knowledge:base' },
             { path: '/vector-knowledge', label: '向量知识库', icon: 'el-icon-data-line', perm: 'knowledge:base' },
           ]},
 

+ 0 - 232
cfc-web/src/views/admin/health-knowledge.vue

@@ -1,232 +0,0 @@
-<template>
-  <div class="knowledge-page">
-    <el-card>
-      <div slot="header" class="clearfix">
-        <span>健康知识库管理</span>
-        <div style="float:right; display:flex; gap:10px;">
-          <el-button type="warning" size="small" @click="importData">导入参考数据</el-button>
-          <el-button type="primary" size="small" @click="showAddDialog">新增条目</el-button>
-        </div>
-      </div>
-
-      <!-- 搜索 -->
-      <el-form :model="filters" inline size="small" style="margin-bottom:16px;">
-        <el-form-item label="名称">
-          <el-input v-model="filters.name" placeholder="搜索指标名称" clearable @input="fetchList" />
-        </el-form-item>
-        <el-form-item label="分类">
-          <el-select v-model="filters.category" placeholder="全部分类" clearable @change="fetchList" style="width:160px;">
-            <el-option v-for="c in categories" :key="c" :label="c" :value="c" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-
-            <div class="adm-table-wrap">
-<el-table :data="list" border stripe v-loading="loading" empty-text="暂无数据" style="width:100%" height="100%">
-        <el-table-column prop="id" label="ID" width="60">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('id', $event)">ID{{ sortIcon('id') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="itemName" label="指标名称" min-width="140">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('itemName', $event)">指标名称{{ sortIcon('itemName') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="category" label="分类" width="140">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('category', $event)">分类<span v-if="sortIndex('category') !== null" class="sort-index" :class="sortClass('category') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('category') }}</span>{{ sortIcon('category') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="normalRange" label="正常范围" width="140">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('normalRange', $event)">正常范围<span v-if="sortIndex('normalRange') !== null" class="sort-index" :class="sortClass('normalRange') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('normalRange') }}</span>{{ sortIcon('normalRange') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="description" label="说明" min-width="260">
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('description', $event)">说明<span v-if="sortIndex('description') !== null" class="sort-index" :class="sortClass('description') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('description') }}</span>{{ sortIcon('description') }}</span>
-          </template>
-          <template slot-scope="{ row }">
-            <span class="desc-ellipsis">{{ (row.description || '').slice(0, 120) }}{{ (row.description || '').length > 120 ? '…' : '' }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="source" label="来源" width="120" show-overflow-tooltip>
-          <template slot="header">
-            <span class="sort-header" @click.stop="onSortClick('source', $event)">来源<span v-if="sortIndex('source') !== null" class="sort-index" :class="sortClass('source') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('source') }}</span>{{ sortIcon('source') }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="200" fixed="right">
-          <template slot-scope="{ row }">
-            <el-button type="text" @click="showDetail(row)">详情</el-button>
-            <el-button type="text" @click="editRow(row)">编辑</el-button>
-            <el-button type="text" style="color:#f56c6c" @click="deleteRow(row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      </div>
-    </el-card>
-
-    <el-dialog :visible.sync="dialog.visible" :title="dialog.isEdit ? '编辑知识库条目' : '新增知识库条目'" width="560px" :close-on-click-modal="false">
-      <el-form :model="dialog.form" label-width="100px" size="small">
-        <el-form-item label="指标名称" required>
-          <el-input v-model="dialog.form.itemName" placeholder="如:维生素D" />
-        </el-form-item>
-        <el-form-item label="分类" required>
-          <el-select v-model="dialog.form.category" placeholder="请选择分类" style="width:100%">
-            <el-option v-for="c in categories" :key="c" :label="c" :value="c" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="正常范围">
-          <el-input v-model="dialog.form.normalRange" placeholder="如:30-100 ng/mL" />
-        </el-form-item>
-        <el-form-item label="说明">
-          <el-input type="textarea" :rows="4" v-model="dialog.form.description" placeholder="指标说明..." />
-        </el-form-item>
-        <el-form-item label="来源">
-          <el-input v-model="dialog.form.source" placeholder="数据来源" />
-        </el-form-item>
-      </el-form>
-      <div slot="footer">
-        <el-button @click="dialog.visible = false" size="small">取消</el-button>
-        <el-button type="primary" @click="saveDialog" size="small" :loading="dialog.saving">保存</el-button>
-      </div>
-    </el-dialog>
-
-    <!-- 详情弹窗:完整展示文章/条目全文(菌群文章最长 71K 字符) -->
-    <el-dialog :visible.sync="detail.visible" :title="detail.row ? detail.row.itemName : '详情'" width="820px" :close-on-click-modal="true">
-      <div v-if="detail.row">
-        <div class="detail-meta">
-          <el-tag v-if="detail.row.category" size="small">{{ detail.row.category }}</el-tag>
-          <span class="detail-meta-item">来源:{{ detail.row.source || '-' }}</span>
-          <span class="detail-meta-item">ID:{{ detail.row.id }}</span>
-          <span class="detail-meta-item">正文 {{ (detail.row.description || '').length }} 字符</span>
-        </div>
-        <div class="detail-content">{{ detail.row.description || '(无正文)' }}</div>
-      </div>
-      <div slot="footer">
-        <el-button @click="detail.visible = false" size="small">关闭</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import SortMixin from '@/mixins/SortMixin'
-import request from '@/utils/request'
-
-export default {
-  mixins: [SortMixin],
-  computed: {
-    tableHeight() {
-      return window.innerHeight - 300
-    }
-  },
-  data() {
-    return {
-      loading: false,
-      list: [],
-      sortableColumns: { id: 'ID', itemName: '指标名称', category: '分类', normalRange: '正常范围', description: '说明', source: '来源' },
-      filters: { name: '', category: '' },
-      categories: ['维生素', '氨基酸', '微量元素', '短链脂肪酸', '神经递质与激素', '肠道屏障与代谢物', '抗生素风险评估'],
-      detail: { visible: false, row: null },
-      dialog: { visible: false, isEdit: false, saving: false, form: {} }
-    }
-  },
-  mounted() { this.fetchList() },
-  methods: {
-    loadList() {
-      this.fetchList()
-    },
-    fetchList() {
-      this.loading = true
-      var params = {}
-      if (this.filters.name) params.name = this.filters.name
-      if (this.filters.category) params.category = this.filters.category
-      if (this.sortState.length > 0) params.sort = this.sortState
-      request.post('/api/health/knowledge/list', params).then(res => {
-        if (res.code === 200) {
-          var rows = res.data || []
-          if (this.filters.name) {
-            rows = rows.filter(function(r) { return r.itemName && r.itemName.indexOf(this.filters.name) >= 0 }.bind(this))
-          }
-          if (this.filters.category) {
-            rows = rows.filter(function(r) { return r.category === this.filters.category }.bind(this))
-          }
-          // 动态合并数据中的分类(菌群文章等外部来源的分类可能不在硬编码列表内)
-          var cats = this.categories.slice()
-          rows.forEach(function(r) {
-            if (r.category && cats.indexOf(r.category) < 0) cats.push(r.category)
-          })
-          this.categories = cats
-          this.list = rows
-        } else {
-          this.$message.error(res.message || '加载失败')
-        }
-      }).finally(() => { this.loading = false })
-    },
-    showAddDialog() {
-      this.dialog = { visible: true, isEdit: false, saving: false, form: { itemName: '', category: '', normalRange: '', description: '', source: '' } }
-    },
-    editRow(row) {
-      this.dialog = { visible: true, isEdit: true, saving: false, form: { id: row.id, itemName: row.itemName, category: row.category, normalRange: row.normalRange, description: row.description, source: row.source } }
-    },
-    showDetail(row) {
-      this.detail = { visible: true, row: row }
-    },
-    saveDialog() {
-      if (!this.dialog.form.itemName || !this.dialog.form.category) {
-        this.$message.warning('指标名称和分类不能为空'); return
-      }
-      this.dialog.saving = true
-      var url = this.dialog.isEdit ? '/api/health/knowledge/update' : '/api/health/knowledge/add'
-      request.post(url, this.dialog.form).then(res => {
-        if (res.code === 200) {
-          this.$message.success('已保存')
-          this.dialog.visible = false
-          this.fetchList()
-        } else {
-          this.$message.error(res.message || '保存失败')
-        }
-      }).finally(() => { this.dialog.saving = false })
-    },
-    deleteRow(row) {
-      this.$confirm('确认删除"' + row.itemName + '"?', '提示', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' }).then(function() {
-        request.post('/api/health/knowledge/delete', { id: row.id }).then(function(res) {
-          if (res.code === 200) {
-            this.$message.success('已删除')
-            this.fetchList()
-          }
-        }.bind(this))
-      }.bind(this)).catch(function() {})
-    },
-    importData() {
-      this.$confirm('将从参考JSON导入知识库数据,是否继续?', '提示', { confirmButtonText: '导入', cancelButtonText: '取消' }).then(function() {
-        request.post('/api/health/knowledge/import', {}).then(function(res) {
-          if (res.code === 200) {
-            this.$message.success(res.data || '导入完成')
-            this.fetchList()
-          }
-        }.bind(this))
-      }.bind(this)).catch(function() {})
-    }
-  }
-}
-</script>
-
-<style scoped>
-.knowledge-page { padding: 20px; }
-.desc-ellipsis { display: inline; }
-.detail-meta { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #ebeef5; font-size: 12px; color: #606266; }
-.detail-meta-item { color: #909399; }
-.detail-content { max-height: 62vh; overflow-y: auto; padding: 10px 12px; background: #fafafa; border-radius: 4px; white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.75; color: #303133; }
-.sort-header {
-  cursor: pointer;
-  user-select: none;
-  font-weight: 500;
-  font-size: 12px;
-}
-.sort-header:hover {
-  color: #409eff;
-}
-</style>

+ 48 - 1
cfc-web/src/views/admin/knowledge/index.vue

@@ -39,6 +39,10 @@
             <PageHeader slot="header" title="知识库管理">
               
               <div class="admin-page-actions">
+                <el-select v-model="filterSourceType" placeholder="全部来源" clearable size="mini" style="width:140px" @change="handleSearch">
+                  <el-option label="案例报告" value="file" />
+                  <el-option label="菌群文章" value="article" />
+                </el-select>
                 <el-select v-model="filterStatus" placeholder="状态" size="mini" clearable @change="handleSearch">
                   <el-option label="全部" value="" />
                   <el-option label="启用" value="1" />
@@ -59,6 +63,7 @@
               @delete="handleDelete"
               @bind-dimension="handleBindDimension"
               @bind-tag="handleBindTag"
+              @view-full="handleViewFull"
             />
 
             <el-pagination
@@ -98,6 +103,18 @@
         <el-button type="primary" :loading="bindingTagLoading" @click="submitBindTags">保存</el-button>
       </span>
     </el-dialog>
+
+    <!-- 全文详情弹窗 -->
+    <el-dialog :visible.sync="fullDetailVisible" title="知识全文" width="700px" append-to-body>
+      <div v-if="currentDetail" class="full-detail-content">
+        <p><strong>标题:</strong>{{ currentDetail.title }}</p>
+        <p><strong>来源方式:</strong>{{ currentDetail.sourceType === 'article' ? '菌群文章' : '案例报告' }}</p>
+        <pre class="full-detail-text">{{ currentDetail.content }}</pre>
+      </div>
+      <span slot="footer">
+        <el-button @click="fullDetailVisible = false">关闭</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
@@ -118,6 +135,7 @@ export default {
       dimensionTree: [],
       searchKeyword: '',
       filterStatus: '',
+      filterSourceType: '',
       page: 1,
       size: 20,
       // 筛选
@@ -135,7 +153,10 @@ export default {
       tagDialogVisible: false,
       bindingKnowledgeIdForTag: null,
       bindingTagIds: [],
-      bindingTagLoading: false
+      bindingTagLoading: false,
+      // 全文详情
+      fullDetailVisible: false,
+      currentDetail: null
     }
   },
   computed: {
@@ -160,6 +181,7 @@ export default {
       if (this.selectedDimensionId) params.dimensionId = this.selectedDimensionId
       if (this.selectedTagId) params.tagId = this.selectedTagId
       if (this.filterStatus) params.status = this.filterStatus
+      if (this.filterSourceType) params.sourceType = this.filterSourceType
       this.fetchKnowledgeList(params)
     },
     handleDimensionClick(node) {
@@ -184,6 +206,7 @@ export default {
     resetSearch() {
       this.searchKeyword = ''
       this.filterStatus = ''
+      this.filterSourceType = ''
       this.selectedDimensionId = null
       this.selectedTagId = null
       this.page = 1
@@ -218,6 +241,10 @@ export default {
     handleDialogSaved() {
       this.fetchList()
     },
+    handleViewFull(row) {
+      this.currentDetail = row
+      this.fullDetailVisible = true
+    },
     async handleBindDimension(row) {
       this.bindingKnowledgeId = row.id
       this.bindingDimensionIds = []
@@ -279,4 +306,24 @@ export default {
 .knowledge-body .el-row {
   width: 100%;
 }
+.full-detail-content {
+  padding: 8px 4px;
+}
+.full-detail-content p {
+  margin: 4px 0;
+  line-height: 1.6;
+}
+.full-detail-text {
+  white-space: pre-wrap;
+  word-break: break-word;
+  background: #f5f7fa;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  padding: 12px;
+  max-height: 60vh;
+  overflow-y: auto;
+  font-size: 13px;
+  line-height: 1.7;
+  margin-top: 8px;
+}
 </style>