Przeglądaj źródła

feat: add dimension config and knowledge base admin pages

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sisyphus 3 miesięcy temu
rodzic
commit
9779016f47

+ 181 - 0
cfc-web/src/views/admin/dimension/index.vue

@@ -0,0 +1,181 @@
+<template>
+  <div class="dimension-manage">
+    <el-card>
+      <div slot="header">
+        <span>维度配置管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 新增维度</el-button>
+      </div>
+
+      <el-table :data="dimensions" stripe v-loading="loading">
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="dimensionCode" label="代码" width="120" />
+        <el-table-column prop="dimensionName" label="名称" width="160" />
+        <el-table-column prop="dimensionType" label="类型" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.dimensionType === 'five' ? 'primary' : 'success'" size="small">
+              {{ scope.row.dimensionType }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="sort" label="排序" width="80" align="center" />
+        <el-table-column prop="enabled" label="状态" width="80" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.enabled === 1 ? 'success' : 'info'" size="small">
+              {{ scope.row.enabled === 1 ? '启用' : '禁用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="difyDatasetId" label="Dify Dataset ID" min-width="180" show-overflow-tooltip />
+        <el-table-column label="操作" width="220" fixed="right">
+          <template slot-scope="scope">
+            <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑维度' : '新增维度'" width="500px">
+      <el-form :model="form" label-width="120px">
+        <el-form-item label="维度代码" required>
+          <el-input v-model="form.dimensionCode" placeholder="如: body/mind/wisdom/action/wealth" />
+        </el-form-item>
+        <el-form-item label="维度名称" required>
+          <el-input v-model="form.dimensionName" placeholder="如: 身体健康" />
+        </el-form-item>
+        <el-form-item label="类型">
+          <el-select v-model="form.dimensionType" placeholder="选择类型">
+            <el-option label="五维" value="five" />
+            <el-option label="行动" value="action" />
+            <el-option label="其他" value="other" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="父级维度">
+          <dimension-selector v-model="form.parentId" />
+        </el-form-item>
+        <el-form-item label="排序">
+          <el-input-number v-model="form.sort" :min="0" :max="999" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.enabled">
+            <el-radio :label="1">启用</el-radio>
+            <el-radio :label="0">禁用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="Dify Dataset ID">
+          <el-input v-model="form.difyDatasetId" placeholder="Dify 知识库ID" />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import { getDimensionTree, saveDimension, deleteDimension } from '@/api/dimension'
+import DimensionSelector from '@/components/DimensionSelector'
+
+export default {
+  name: 'DimensionManage',
+  components: { DimensionSelector },
+  data() {
+    return {
+      loading: false,
+      dialogVisible: false,
+      submitting: false,
+      isEdit: false,
+      form: this.initForm(),
+      dimensions: []
+    }
+  },
+  computed: {
+    ...mapState('dimension', ['dimensionTree'])
+  },
+  created() {
+    this.fetchList()
+  },
+  methods: {
+    initForm() {
+      return {
+        id: null,
+        dimensionCode: '',
+        dimensionName: '',
+        dimensionType: 'five',
+        parentId: null,
+        sort: 0,
+        enabled: 1,
+        difyDatasetId: '',
+        remark: ''
+      }
+    },
+    async fetchList() {
+      this.loading = true
+      try {
+        const res = await getDimensionTree()
+        if (res.code === 200) {
+          this.dimensions = res.data || []
+        }
+      } finally {
+        this.loading = false
+      }
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = this.initForm()
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = {
+        id: row.id,
+        dimensionCode: row.dimensionCode,
+        dimensionName: row.dimensionName,
+        dimensionType: row.dimensionType || 'five',
+        parentId: row.parentId || null,
+        sort: row.sort || 0,
+        enabled: row.enabled !== undefined ? row.enabled : 1,
+        difyDatasetId: row.difyDatasetId || '',
+        remark: row.remark || ''
+      }
+      this.dialogVisible = true
+    },
+    async handleSubmit() {
+      if (!this.form.dimensionCode || !this.form.dimensionName) {
+        this.$message.warning('请填写代码和名称')
+        return
+      }
+      this.submitting = true
+      try {
+        const res = await saveDimension(this.form)
+        if (res.code === 200) {
+          this.$message.success('保存成功')
+          this.dialogVisible = false
+          await this.fetchList()
+        } else {
+          this.$message.error(res.message || '保存失败')
+        }
+      } finally {
+        this.submitting = false
+      }
+    },
+    handleDelete(row) {
+      this.$confirm('确定删除该维度配置吗?', '提示', { type: 'warning' }).then(async () => {
+        const res = await deleteDimension(row.id)
+        if (res.code === 200) {
+          this.$message.success('删除成功')
+          await this.fetchList()
+        } else {
+          this.$message.error(res.message || '删除失败')
+        }
+      }).catch(() => {})
+    }
+  }
+}
+</script>

+ 257 - 0
cfc-web/src/views/admin/knowledge/index.vue

@@ -0,0 +1,257 @@
+<template>
+  <div class="knowledge-manage">
+    <el-row :gutter="20">
+      <!-- 左侧筛选面板 -->
+      <el-col :span="5">
+        <el-card>
+          <div slot="header"><span>维度筛选</span></div>
+          <el-tree
+            :data="dimensionTree"
+            :props="{ label: 'dimensionName', children: 'children' }"
+            node-key="id"
+            default-expand-all
+            highlight-current
+            @node-click="handleDimensionClick"
+          />
+        </el-card>
+        <el-card style="margin-top: 12px">
+          <div slot="header"><span>标签筛选</span></div>
+          <div style="padding: 4px 0">
+            <el-tag
+              v-for="tag in tags"
+              :key="tag.id"
+              :type="selectedTagId === tag.id ? 'primary' : 'info'"
+              style="cursor: pointer; margin: 2px"
+              @click="handleTagClick(tag)"
+            >
+              {{ tag.tagName }}
+            </el-tag>
+          </div>
+          <el-button v-if="selectedTagId" type="text" size="small" @click="clearTagFilter">清除筛选</el-button>
+        </el-card>
+      </el-col>
+
+      <!-- 右侧知识库列表 -->
+      <el-col :span="19">
+        <el-card>
+          <div slot="header">
+            <span>知识库管理</span>
+            <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 新增知识</el-button>
+          </div>
+
+          <!-- 搜索栏 -->
+          <div style="margin-bottom: 16px; display: flex; gap: 12px">
+            <el-input v-model="searchKeyword" placeholder="搜索标题/内容" style="width: 300px" clearable @keyup.enter.native="handleSearch" />
+            <el-button type="primary" @click="handleSearch">搜索</el-button>
+            <el-button @click="resetSearch">重置</el-button>
+          </div>
+
+          <knowledge-table
+            :list="knowledgeList"
+            :loading="knowledgeLoading"
+            @edit="handleEdit"
+            @delete="handleDelete"
+            @bind-dimension="handleBindDimension"
+            @bind-tag="handleBindTag"
+          />
+
+          <el-pagination
+            style="margin-top: 16px; text-align: right"
+            :current-page="page"
+            :page-size="size"
+            :total="knowledgeTotal"
+            layout="total, prev, pager, next"
+            @current-change="handlePageChange"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 新增/编辑知识弹窗 -->
+    <knowledge-dialog
+      :visible.sync="dialogVisible"
+      :knowledge="editingKnowledge"
+      @saved="handleDialogSaved"
+    />
+
+    <!-- 绑定维度弹窗 -->
+    <el-dialog :visible.sync="dimensionDialogVisible" title="绑定维度" width="500px">
+      <dimension-selector v-model="bindingDimensionIds" :multiple="true" />
+      <span slot="footer">
+        <el-button @click="dimensionDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="bindingDimLoading" @click="submitBindDimensions">保存</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 绑定标签弹窗 -->
+    <el-dialog :visible.sync="tagDialogVisible" title="绑定标签" width="500px">
+      <tag-selector v-model="bindingTagIds" />
+      <span slot="footer">
+        <el-button @click="tagDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="bindingTagLoading" @click="submitBindTags">保存</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex'
+import { getDimensionTree } from '@/api/dimension'
+import { getKnowledge, deleteKnowledge, bindKnowledgeDimensions, bindKnowledgeTags } from '@/api/dimension'
+import KnowledgeTable from '@/components/KnowledgeTable'
+import KnowledgeDialog from '@/components/KnowledgeDialog'
+import DimensionSelector from '@/components/DimensionSelector'
+import TagSelector from '@/components/TagSelector'
+
+export default {
+  name: 'KnowledgeManage',
+  components: { KnowledgeTable, KnowledgeDialog, DimensionSelector, TagSelector },
+  data() {
+    return {
+      dimensionTree: [],
+      searchKeyword: '',
+      page: 1,
+      size: 20,
+      // 筛选
+      selectedDimensionId: null,
+      selectedTagId: null,
+      // 编辑
+      dialogVisible: false,
+      editingKnowledge: null,
+      // 绑定维度
+      dimensionDialogVisible: false,
+      bindingKnowledgeId: null,
+      bindingDimensionIds: [],
+      bindingDimLoading: false,
+      // 绑定标签
+      tagDialogVisible: false,
+      bindingKnowledgeIdForTag: null,
+      bindingTagIds: [],
+      bindingTagLoading: false
+    }
+  },
+  computed: {
+    ...mapState('dimension', ['knowledgeList', 'knowledgeTotal', 'knowledgeLoading', 'tags'])
+  },
+  created() {
+    this.fetchDimensionTree()
+    this.$store.dispatch('dimension/fetchTags')
+    this.fetchList()
+  },
+  methods: {
+    ...mapActions('dimension', ['fetchKnowledgeList']),
+    async fetchDimensionTree() {
+      const res = await getDimensionTree()
+      if (res.code === 200) {
+        this.dimensionTree = res.data || []
+      }
+    },
+    fetchList() {
+      const params = { page: this.page, size: this.size }
+      if (this.searchKeyword) params.keyword = this.searchKeyword
+      if (this.selectedDimensionId) params.dimensionId = this.selectedDimensionId
+      if (this.selectedTagId) params.tagId = this.selectedTagId
+      this.fetchKnowledgeList(params)
+    },
+    handleDimensionClick(node) {
+      this.selectedDimensionId = node.id
+      this.page = 1
+      this.fetchList()
+    },
+    handleTagClick(tag) {
+      this.selectedTagId = this.selectedTagId === tag.id ? null : tag.id
+      this.page = 1
+      this.fetchList()
+    },
+    clearTagFilter() {
+      this.selectedTagId = null
+      this.page = 1
+      this.fetchList()
+    },
+    handleSearch() {
+      this.page = 1
+      this.fetchList()
+    },
+    resetSearch() {
+      this.searchKeyword = ''
+      this.selectedDimensionId = null
+      this.selectedTagId = null
+      this.page = 1
+      this.fetchList()
+    },
+    handlePageChange(p) {
+      this.page = p
+      this.fetchList()
+    },
+    handleCreate() {
+      this.editingKnowledge = null
+      this.dialogVisible = true
+    },
+    async handleEdit(row) {
+      const res = await getKnowledge(row.id)
+      if (res.code === 200) {
+        this.editingKnowledge = res.data
+        this.dialogVisible = true
+      }
+    },
+    async handleDelete(row) {
+      this.$confirm('确定删除该知识吗?', '提示', { type: 'warning' }).then(async () => {
+        const res = await deleteKnowledge(row.id)
+        if (res.code === 200) {
+          this.$message.success('删除成功')
+          this.fetchList()
+        } else {
+          this.$message.error(res.message || '删除失败')
+        }
+      }).catch(() => {})
+    },
+    handleDialogSaved() {
+      this.fetchList()
+    },
+    async handleBindDimension(row) {
+      this.bindingKnowledgeId = row.id
+      this.bindingDimensionIds = []
+      this.dimensionDialogVisible = true
+    },
+    async submitBindDimensions() {
+      this.bindingDimLoading = true
+      try {
+        const res = await bindKnowledgeDimensions({
+          knowledgeId: this.bindingKnowledgeId,
+          dimensionIds: this.bindingDimensionIds
+        })
+        if (res.code === 200) {
+          this.$message.success('绑定成功')
+          this.dimensionDialogVisible = false
+        } else {
+          this.$message.error(res.message || '绑定失败')
+        }
+      } finally {
+        this.bindingDimLoading = false
+      }
+    },
+    async handleBindTag(row) {
+      this.bindingKnowledgeIdForTag = row.id
+      this.bindingTagIds = []
+      this.tagDialogVisible = true
+    },
+    async submitBindTags() {
+      this.bindingTagLoading = true
+      try {
+        const res = await bindKnowledgeTags({
+          knowledgeId: this.bindingKnowledgeIdForTag,
+          tagIds: this.bindingTagIds
+        })
+        if (res.code === 200) {
+          this.$message.success('绑定成功')
+          this.tagDialogVisible = false
+        } else {
+          this.$message.error(res.message || '绑定失败')
+        }
+      } finally {
+        this.bindingTagLoading = false
+      }
+    }
+  }
+}
+</script>