Просмотр исходного кода

feat: add dimension selector, tag selector, knowledge table and dialog components

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sisyphus 3 месяцев назад
Родитель
Сommit
468ee54c53

+ 61 - 0
cfc-web/src/components/DimensionSelector/index.vue

@@ -0,0 +1,61 @@
+<template>
+  <el-select
+    v-model="selectedValue"
+    :multiple="multiple"
+    :placeholder="placeholder"
+    :clearable="clearable"
+    :disabled="disabled"
+    style="width: 100%"
+    @change="$emit('change', selectedValue)"
+  >
+    <el-option
+      v-for="dim in filteredDimensions"
+      :key="dim.id"
+      :label="dim.dimensionName"
+      :value="dim.id"
+    >
+      <span>{{ dim.dimensionName }}</span>
+      <span class="dimension-code">[{{ dim.dimensionCode }}]</span>
+    </el-option>
+  </el-select>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+
+export default {
+  name: 'DimensionSelector',
+  props: {
+    value: [Number, Array],
+    multiple: { type: Boolean, default: false },
+    placeholder: { type: String, default: '请选择维度' },
+    clearable: { type: Boolean, default: true },
+    disabled: { type: Boolean, default: false },
+    dimensionType: { type: String, default: '' }
+  },
+  computed: {
+    ...mapState('dimension', ['dimensions']),
+    selectedValue: {
+      get() { return this.value },
+      set(val) { this.$emit('input', val) }
+    },
+    filteredDimensions() {
+      if (!this.dimensionType) return this.dimensions
+      return this.dimensions.filter(d => d.dimensionType === this.dimensionType)
+    }
+  },
+  created() {
+    if (this.dimensions.length === 0) {
+      this.$store.dispatch('dimension/fetchDimensions', { page: 1, size: 200 })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dimension-code {
+  color: #999;
+  font-size: 12px;
+  margin-left: 6px;
+}
+</style>

+ 129 - 0
cfc-web/src/components/KnowledgeDialog/index.vue

@@ -0,0 +1,129 @@
+<template>
+  <el-dialog
+    :title="isEdit ? '编辑知识' : '新增知识'"
+    :visible="visible"
+    width="700px"
+    @close="handleClose"
+  >
+    <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+      <el-form-item label="标题" prop="title">
+        <el-input v-model="form.title" placeholder="知识标题" maxlength="200" />
+      </el-form-item>
+      <el-form-item label="内容" prop="content">
+        <el-input
+          v-model="form.content"
+          type="textarea"
+          :rows="6"
+          placeholder="知识内容"
+          maxlength="5000"
+          show-word-limit
+        />
+      </el-form-item>
+      <el-form-item label="维度">
+        <dimension-selector v-model="form.dimensionIds" :multiple="true" />
+      </el-form-item>
+      <el-form-item label="标签">
+        <tag-selector v-model="form.tagIds" />
+      </el-form-item>
+      <el-form-item label="排序">
+        <el-input-number v-model="form.sort" :min="0" :max="9999" />
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-radio-group v-model="form.status">
+          <el-radio :label="1">启用</el-radio>
+          <el-radio :label="0">禁用</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注">
+        <el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button @click="handleClose">取消</el-button>
+      <el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import DimensionSelector from '@/components/DimensionSelector'
+import TagSelector from '@/components/TagSelector'
+
+export default {
+  name: 'KnowledgeDialog',
+  components: { DimensionSelector, TagSelector },
+  props: {
+    visible: { type: Boolean, default: false },
+    knowledge: { type: Object, default: () => null }
+  },
+  data() {
+    return {
+      submitting: false,
+      form: this.initForm(),
+      rules: {
+        title: [{ required: true, message: '请输入知识标题', trigger: 'blur' }],
+        content: [{ required: true, message: '请输入知识内容', trigger: 'blur' }]
+      }
+    }
+  },
+  computed: {
+    isEdit() {
+      return !!this.knowledge
+    }
+  },
+  watch: {
+    knowledge(val) {
+      if (val) {
+        this.form = {
+          id: val.id,
+          title: val.title,
+          content: val.content,
+          dimensionIds: val.dimensionIds || [],
+          tagIds: val.tagIds || [],
+          sort: val.sort || 0,
+          status: val.status !== undefined ? val.status : 1,
+          remark: val.remark || ''
+        }
+      } else {
+        this.form = this.initForm()
+      }
+    }
+  },
+  methods: {
+    initForm() {
+      return {
+        id: null,
+        title: '',
+        content: '',
+        dimensionIds: [],
+        tagIds: [],
+        sort: 0,
+        status: 1,
+        remark: ''
+      }
+    },
+    handleClose() {
+      this.$refs.form.resetFields()
+      this.$emit('update:visible', false)
+    },
+    async handleSubmit() {
+      try {
+        await this.$refs.form.validate()
+      } catch {
+        return
+      }
+      this.submitting = true
+      try {
+        await this.$store.dispatch('dimension/saveKnowledge', this.form)
+        this.$message.success('保存成功')
+        this.handleClose()
+        this.$emit('saved')
+      } catch (e) {
+        this.$message.error('保存失败: ' + (e.message || '未知错误'))
+      } finally {
+        this.submitting = false
+      }
+    }
+  }
+}
+</script>

+ 50 - 0
cfc-web/src/components/KnowledgeTable/index.vue

@@ -0,0 +1,50 @@
+<template>
+  <el-table
+    :data="list"
+    v-loading="loading"
+    stripe
+    border
+    style="width: 100%"
+    @selection-change="$emit('selection-change', $event)"
+  >
+    <el-table-column type="selection" width="55" />
+    <el-table-column prop="title" label="知识标题" min-width="200" show-overflow-tooltip />
+    <el-table-column prop="content" label="内容" min-width="300" show-overflow-tooltip>
+      <template slot-scope="scope">
+        <span>{{ scope.row.content | truncate(100) }}</span>
+      </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">
+        <el-tag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
+          {{ scope.row.status === 1 ? '启用' : '禁用' }}
+        </el-tag>
+      </template>
+    </el-table-column>
+    <el-table-column label="操作" width="200" 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" style="color: #f56c6c" @click="$emit('delete', scope.row)">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</template>
+
+<script>
+export default {
+  name: 'KnowledgeTable',
+  props: {
+    list: { type: Array, default: () => [] },
+    loading: { type: Boolean, default: false }
+  },
+  filters: {
+    truncate(text, len) {
+      if (!text) return ''
+      return text.length > len ? text.substring(0, len) + '...' : text
+    }
+  }
+}
+</script>

+ 70 - 0
cfc-web/src/components/TagSelector/index.vue

@@ -0,0 +1,70 @@
+<template>
+  <div>
+    <el-select
+      v-model="selectedTags"
+      multiple
+      filterable
+      allow-create
+      default-first-option
+      :placeholder="placeholder"
+      :clearable="clearable"
+      style="width: 100%"
+      @change="handleChange"
+      @remove-tag="handleRemove"
+    >
+      <el-option
+        v-for="tag in tagList"
+        :key="tag.id"
+        :label="tag.tagName"
+        :value="tag.id"
+      >
+        <span>{{ tag.tagName }}</span>
+        <span class="tag-type">[{{ tag.tagType }}]</span>
+      </el-option>
+    </el-select>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+
+export default {
+  name: 'TagSelector',
+  props: {
+    value: { type: Array, default: () => [] },
+    placeholder: { type: String, default: '请选择标签' },
+    clearable: { type: Boolean, default: true }
+  },
+  computed: {
+    ...mapState('dimension', ['tags']),
+    tagList() {
+      return this.tags || []
+    },
+    selectedTags: {
+      get() { return this.value || [] },
+      set(val) { this.$emit('input', val) }
+    }
+  },
+  created() {
+    if (this.tags.length === 0) {
+      this.$store.dispatch('dimension/fetchTags')
+    }
+  },
+  methods: {
+    handleChange(val) {
+      this.$emit('change', val)
+    },
+    handleRemove(tagId) {
+      this.$emit('remove-tag', tagId)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.tag-type {
+  color: #999;
+  font-size: 12px;
+  margin-left: 6px;
+}
+</style>