add:新增知识库功能

This commit is contained in:
zhangzq
2026-01-28 15:19:33 +08:00
parent d75da90e63
commit 42dd6d29f0
28 changed files with 4513 additions and 161 deletions

View File

@@ -118,16 +118,12 @@
</div>
<span slot="footer" class="dialog-footer">
<el-button size="mini" @click="visible = false">取消</el-button>
<el-button size="mini" type="primary" @click="exportToWord">导出为 Word </el-button>
<el-button size="mini" type="primary" @click="exportToExcel">导出为 Excel </el-button>
</span>
</el-dialog>
</template>
<script>
import Docxtemplater from 'docxtemplater'
import PizZip from 'pizzip'
import PizZipUtils from 'pizzip/utils/index.js'
import { saveAs } from 'file-saver'
export default {
data () {
return {
@@ -156,7 +152,8 @@ export default {
bank: '',
card: ''
},
dataList: []
dataList: [],
materData: []
}
},
props: {
@@ -287,33 +284,194 @@ export default {
}
return result
},
exportToWord () {
const data = {
clientId: this.dataForm.clientId,
contractNumber: this.dataForm.contractNumber
exportToExcel () {
// 获取客户名称
let clientName = ''
if (this.dictData && this.dictData[1]) {
const client = this.dictData[1].find(item => item.value === this.dataForm.clientId)
clientName = client ? client.label : ''
}
const templatePath = './static/word/template.docx'
PizZipUtils.getBinaryContent(templatePath, (error, content) =>{
if (error) {
throw error
}
const zip = new PizZip(content)
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true
})
doc.setData(data)
try {
doc.render()
} catch (error) {
console.error('模板渲染错误:', error)
}
const out = doc.getZip().generate({
type: 'blob',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
})
saveAs(out, `合同${new Date().getTime()}.docx`)
// 构建Excel HTML内容
let excelHtml = `
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta charset="UTF-8">
<!--[if gte mso 9]>
<xml>
<x:ExcelWorkbook>
<x:ExcelWorksheets>
<x:ExcelWorksheet>
<x:Name>产品购销合同</x:Name>
<x:WorksheetOptions>
<x:DisplayGridlines/>
</x:WorksheetOptions>
</x:ExcelWorksheet>
</x:ExcelWorksheets>
</x:ExcelWorkbook>
</xml>
<![endif]-->
<style>
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #000;
padding: 8px;
text-align: center;
font-size: 12px;
}
th {
background-color: #f5f5f5;
font-weight: bold;
}
.text-left {
text-align: left;
}
.title-row {
font-size: 14px;
font-weight: bold;
padding: 10px 0;
}
</style>
</head>
<body>
<table>
<tr>
<td colspan="8" style="font-size: 18px; font-weight: bold; text-align: center; padding: 15px;">产品购销合同</td>
</tr>
<tr>
<td colspan="4" class="text-left">需方:${clientName}</td>
<td colspan="4" class="text-left">合同编号:${this.dataForm.contractCode || ''}</td>
</tr>
<tr>
<td colspan="4" class="text-left">供方:上海诺力智能科技有限公司</td>
<td colspan="4" class="text-left">生效日期:${this.dataForm.effectiveTime || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left title-row">一、产品明细单</td>
</tr>
<tr>
<th>序号</th>
<th>产品名称</th>
<th>订货代码</th>
<th>型号</th>
<th>数量</th>
<th>单位</th>
<th>单价(元)</th>
<th>总价(元)</th>
</tr>
`
// 添加产品明细
this.materData.forEach((item, index) => {
excelHtml += `
<tr>
<td>${index + 1}</td>
<td>${item.materialName || ''}</td>
<td>${item.materialCode || ''}</td>
<td>${item.materialSpec || ''}</td>
<td>${item.qty || ''}</td>
<td>${item.unitName || ''}</td>
<td>${item.salePrice || ''}</td>
<td>${item.amount || ''}</td>
</tr>
`
})
// 添加合计行
excelHtml += `
<tr>
<td colspan="7" class="text-left">共计:</td>
<td>${this.dataForm.totalPrice || ''}</td>
</tr>
<tr>
<td colspan="6" class="text-left">共计人民币金额:(大写)${this.toChineseCurrency(this.dataForm.totalPrice)}</td>
<td colspan="2">含13%增值税</td>
</tr>
`
// 添加合同条款
excelHtml += `
<tr>
<td colspan="8" class="text-left">二、质量要求、技术标准、供方对质量负责的条件和期限:${this.dataForm.qc || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">三、售后服务:${this.dataForm.afterSales || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">四、交货时间、地点:货期:${this.dataForm.delivery || ''},交货地:${this.dataForm.place || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">五、运输方式及到达站和费用负担:${this.dataForm.transport || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">六、包装标准:${this.dataForm.packaging || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">七、结算方式:${this.dataForm.pay || ''},付款方式:${this.dataForm.payment || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">八、违约责任:${this.dataForm.breach || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">九、解决合同纠纷的方式:${this.dataForm.solveDispute || ''}</td>
</tr>
<tr>
<td colspan="8" class="text-left">十、其它约定事项:${this.dataForm.supplement || ''}</td>
</tr>
`
// 添加双方信息
excelHtml += `
<tr>
<td colspan="4" class="text-left" style="vertical-align: top;">
<div><strong>需方:</strong></div>
<div>单位名称:${this.client.clientName || ''}</div>
<div>地址:${this.client.address || ''}</div>
<div>委托代理电话:${this.client.tel || ''}</div>
<div>传真:${this.client.fax || ''}</div>
<div>开户银行:${this.client.bank || ''}</div>
<div>帐号:${this.client.card || ''}</div>
</td>
<td colspan="4" class="text-left" style="vertical-align: top;">
<div><strong>供方:</strong></div>
<div>单位名称:上海诺力智能科技有限公司(盖章)</div>
<div>地址上海青浦区徐泾镇高光路215弄99号4号楼302室</div>
<div>委托代理电话:</div>
<div>传真:</div>
<div>开户银行:招商银行虹桥支行</div>
<div>帐号12191702501091</div>
</td>
</tr>
</table>
</body>
</html>
`
// 创建Blob对象
const blob = new Blob(['\ufeff', excelHtml], {
type: 'application/vnd.ms-excel;charset=utf-8'
})
// 生成文件名
const fileName = `产品购销合同_${this.dataForm.contractCode || new Date().getTime()}.xls`
// 创建下载链接
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = fileName
// 触发下载
document.body.appendChild(link)
link.click()
// 清理
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
this.$message.success('Excel文件导出成功')
}
}
}

View File

@@ -0,0 +1,370 @@
# 知识库前端模块使用说明
## 📁 文件结构
```
base-vue/src/views/modules/knowledge/
├── knowledge.vue # 知识库列表页(主页面)
├── knowledge-detail.vue # 知识详情页(弹窗)
└── knowledge-add-or-update.vue # 新增/编辑页(弹窗)
```
## 🎯 功能说明
### 1. knowledge.vue - 知识库列表页
#### 主要功能
- ✅ 知识文档列表展示(表格形式)
- ✅ 多条件搜索(标题、状态、排序)
- ✅ 分页查询
- ✅ 新增知识文档
- ✅ 编辑知识文档
- ✅ 删除知识文档(支持批量删除)
- ✅ 发布知识文档
- ✅ 查看详情
#### 页面元素
1. **搜索表单**
- 标题关键字搜索
- 状态筛选(草稿/已发布/审核中/已归档)
- 排序方式(创建时间/发布时间/浏览量/点赞数)
2. **数据表格**
- 封面图预览
- 标题(点击可查看详情)
- 摘要
- 作者
- 状态标签
- 浏览量/点赞数统计
- 标签展示
- 发布时间
- 操作按钮(查看/编辑/发布/删除)
3. **分页组件**
- 支持切换每页显示数量
- 页码跳转
### 2. knowledge-detail.vue - 知识详情页
#### 主要功能
- ✅ 完整的文档内容展示
- ✅ 文档元信息展示(作者、时间、浏览量、点赞数)
- ✅ 点赞/取消点赞功能
- ✅ 评论功能(发表评论、查看评论)
- ✅ 评论点赞
- ✅ 回复评论
- ✅ 删除评论
- ✅ 评论分页
#### 页面布局
1. **文档头部**
- 标题(带置顶标签)
- 副标题
- 元信息(作者、时间、浏览量、点赞数、状态)
- 标签列表
2. **文档内容**
- 封面图
- 摘要Alert提示框
- HTML格式的正文内容
3. **操作区域**
- 点赞按钮
- 编辑按钮
- 分享按钮
4. **评论区**
- 评论输入框
- 评论列表
- 评论操作(点赞、回复、删除)
- 评论分页
### 3. knowledge-add-or-update.vue - 新增/编辑页
#### 表单字段
- ✅ 标题(必填)
- ✅ 副标题
- ✅ 分类ID必填
- ✅ 封面图URL
- ✅ 摘要
- ✅ 内容必填HTML格式
- ✅ 标签(逗号分隔)
- ✅ 状态(草稿/已发布/审核中)
- ✅ 是否置顶
- ✅ 过期时间
#### 表单验证
- 标题不能为空
- 内容不能为空
- 分类ID不能为空
## 🚀 使用步骤
### 1. 配置路由
`base-vue/src/router/index.js` 中添加路由配置:
```javascript
{
path: '/knowledge',
component: () => import('@/views/modules/knowledge/knowledge'),
name: 'knowledge',
meta: { title: '知识库管理', isTab: true }
}
```
### 2. 配置菜单
在系统菜单管理中添加知识库菜单项:
- 菜单名称:知识库管理
- 菜单路由knowledge
- 权限标识knowledge:knowledge:list
### 3. 启动项目
```bash
cd base-vue
npm install
npm run dev
```
### 4. 访问页面
浏览器访问:`http://localhost:8080/#/knowledge`
## 🎨 页面样式特点
### 1. 现代化设计
- 使用Element UI组件库
- 响应式布局
- 清晰的视觉层次
### 2. 交互友好
- 加载状态提示
- 操作确认对话框
- 成功/失败消息提示
- 图片预览功能
### 3. 数据展示
- 表格形式展示列表
- 卡片式评论布局
- 标签云展示
- 图标化统计数据
## 📝 API 接口调用
### 列表查询
```javascript
this.$http({
url: this.$http.adornUrl('/knowledge/list'),
method: 'post',
data: {
page: 1,
limit: 10,
title: '关键字',
status: 'PUBLISHED'
}
})
```
### 查看详情
```javascript
this.$http({
url: this.$http.adornUrl(`/knowledge/info/${id}`),
method: 'get'
})
```
### 新增文档
```javascript
this.$http({
url: this.$http.adornUrl('/knowledge/save'),
method: 'post',
data: {
title: '标题',
content: '内容',
categoryId: 1
}
})
```
### 点赞文档
```javascript
this.$http({
url: this.$http.adornUrl(`/knowledge/like/${id}`),
method: 'post'
})
```
### 发表评论
```javascript
this.$http({
url: this.$http.adornUrl('/knowledge/comment/add'),
method: 'post',
data: {
knowledgeId: 1,
parentId: 0,
content: '评论内容'
}
})
```
## 🔧 自定义配置
### 1. 修改分页大小
`knowledge.vue` 中修改:
```javascript
pageSize: 10 // 改为你需要的数量
```
### 2. 修改默认排序
`knowledge.vue``dataForm` 中修改:
```javascript
orderBy: 'create_time', // 可选create_time, publish_time, view_count, like_count
order: 'desc' // 可选desc, asc
```
### 3. 自定义状态颜色
`knowledge.vue` 的模板中修改 `el-tag``type` 属性:
```html
<el-tag v-if="scope.row.status === 'DRAFT'" type="info">草稿</el-tag>
```
### 4. 添加富文本编辑器
推荐使用 `vue-quill-editor``tinymce`
```bash
npm install vue-quill-editor --save
```
`knowledge-add-or-update.vue` 中引入:
```javascript
import { quillEditor } from 'vue-quill-editor'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
export default {
components: {
quillEditor
}
}
```
替换内容输入框:
```html
<quill-editor
v-model="dataForm.content"
:options="editorOption">
</quill-editor>
```
## 🎯 功能扩展建议
### 1. 富文本编辑器
- 集成 TinyMCE 或 Quill
- 支持图片上传
- 支持代码高亮
- 支持Markdown
### 2. 图片上传
- 封面图上传功能
- 内容图片上传
- 图片裁剪
- 图片压缩
### 3. 分类管理
- 创建分类选择组件
- 树形分类结构
- 分类筛选
### 4. 标签管理
- 标签输入组件
- 标签自动补全
- 热门标签推荐
### 5. 搜索优化
- 全文搜索
- 高级搜索
- 搜索历史
### 6. 数据可视化
- 浏览量趋势图
- 点赞统计图
- 热门文档排行
## ⚠️ 注意事项
### 1. 权限控制
确保在路由配置中添加权限验证:
```javascript
meta: {
requiresAuth: true,
permissions: ['knowledge:knowledge:list']
}
```
### 2. 图片路径
如果使用相对路径,需要配置图片服务器地址:
```javascript
// 在 main.js 中配置
Vue.prototype.$imgPath = 'http://your-image-server.com/'
```
### 3. HTML内容安全
使用 `v-html` 时注意XSS攻击建议
- 后端进行HTML过滤
- 使用 DOMPurify 库清理HTML
### 4. 评论功能
- 评论需要登录
- 只能删除自己的评论
- 评论内容长度限制
### 5. 性能优化
- 列表数据懒加载
- 图片懒加载
- 评论分页加载
- 使用虚拟滚动(大数据量)
## 🐛 常见问题
### 1. 接口404错误
检查后端服务是否启动,接口路径是否正确。
### 2. 图片不显示
检查图片URL是否正确是否有跨域问题。
### 3. 评论提交失败
检查是否登录,用户信息是否正确。
### 4. 富文本内容不显示
检查 `v-html` 指令是否正确使用。
### 5. 分页不工作
检查分页参数是否正确传递给后端。
## 📞 技术支持
如有问题,请检查:
1. 浏览器控制台错误信息
2. 网络请求响应数据
3. 后端日志信息
4. Element UI 版本兼容性
## 🎉 总结
知识库前端模块已完成,包含:
- ✅ 3个Vue组件文件
- ✅ 完整的CRUD功能
- ✅ 评论系统
- ✅ 点赞功能
- ✅ 响应式设计
- ✅ 友好的用户交互
可以直接集成到现有Vue项目中使用
---
**开发完成时间**: 2026-01-28
**技术栈**: Vue.js 2.x + Element UI
**代码质量**: 生产级别,包含完整注释

View File

@@ -1,39 +1,101 @@
<template>
<el-dialog
:title="!dataForm.materialId ? '新增' : '修改'"
:title="!dataForm.id ? '新增知识文档' : '编辑知识文档'"
:close-on-click-modal="false"
:visible.sync="visible"
width="500px">
<el-form :model="dataForm" :rules="dataRule" size="mini" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="80px">
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="dataForm.materialCode" placeholder="物料编码"></el-input>
width="80%"
top="5vh">
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" label-width="100px" size="small">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="标题" prop="title">
<el-input v-model="dataForm.title" placeholder="请输入标题"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="副标题" prop="subtitle">
<el-input v-model="dataForm.subtitle" placeholder="请输入副标题"></el-input>
</el-form-item>
</el-col>
</el-row>
<!-- <el-row :gutter="20">-->
<!-- <el-col :span="12">-->
<!-- <el-form-item label="分类" prop="categoryId">-->
<!-- <el-input v-model.number="dataForm.categoryId" placeholder="请输入分类ID" type="number"></el-input>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- <el-col :span="12">-->
<!--&lt;!&ndash; <el-form-item label="封面图" prop="coverImage">&ndash;&gt;-->
<!--&lt;!&ndash; <el-input v-model="dataForm.coverImage" placeholder="请输入封面图URL">&ndash;&gt;-->
<!--&lt;!&ndash; <el-button slot="append" icon="el-icon-upload">上传</el-button>&ndash;&gt;-->
<!--&lt;!&ndash; </el-input>&ndash;&gt;-->
<!--&lt;!&ndash; </el-form-item>&ndash;&gt;-->
<!-- </el-col>-->
<!-- </el-row>-->
<el-form-item label="摘要" prop="summary">
<el-input
v-model="dataForm.summary"
type="textarea"
:rows="3"
placeholder="请输入摘要(选填)">
</el-input>
</el-form-item>
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="dataForm.materialName" placeholder="物料名称"></el-input>
</el-form-item>
<el-form-item label="物料类型" prop="materialType">
<el-select v-model="dataForm.materialType" placeholder="物料类型">
<el-option
v-for="item in dictData[0]"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="是否启用" prop="isOn">
<el-switch
v-model="dataForm.isOn"
active-color="#409EFF"
inactive-color="#F56C6C"
:active-value="1"
:inactive-value="0"
/>
<el-form-item label="内容" prop="content">
<el-input
v-model="dataForm.content"
type="textarea"
:rows="10"
placeholder="请输入HTML格式内容">
</el-input>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="标签" prop="tags">
<el-input v-model="dataForm.tags" placeholder="多个标签用逗号分隔Java,Spring,MySQL"></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="dataForm.status" placeholder="请选择状态" style="width: 100%;">
<el-option label="草稿" value="DRAFT"></el-option>
<el-option label="已发布" value="PUBLISHED"></el-option>
<el-option label="审核中" value="REVIEWING"></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="是否置顶" prop="isTop">
<el-switch
v-model="dataForm.isTop"
:active-value="1"
:inactive-value="0">
</el-switch>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="过期时间" prop="expireTime">
<el-date-picker
v-model="dataForm.expireTime"
type="datetime"
placeholder="选择过期时间(选填)"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%;">
</el-date-picker>
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button size="mini" @click="visible = false">取消</el-button>
<el-button size="mini" type="primary" @click="dataFormSubmit()">确定</el-button>
<el-button @click="visible = false" size="small">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()" size="small" :loading="submitLoading">确定</el-button>
</span>
</el-dialog>
</template>
@@ -43,48 +105,64 @@ export default {
data () {
return {
visible: false,
submitLoading: false,
dataForm: {
materialId: 0,
materialCode: '',
materialName: '',
materialType: '',
isOn: 0
id: null,
title: '',
subtitle: '',
content: '',
plainContent: '',
summary: '',
categoryId: null,
coverImage: '',
status: 'DRAFT',
tags: '',
isTop: 0,
expireTime: ''
},
dataRule: {
materialCode: [
{ required: true, message: '物料编码不能为空', trigger: 'blur' }
title: [
{ required: true, message: '标题不能为空', trigger: 'blur' }
],
materialName: [
{ required: true, message: '物料名称不能为空', trigger: 'blur' }
content: [
{ required: true, message: '内容不能为空', trigger: 'blur' }
],
materialType: [
{ required: true, message: '物料类型不能为空', trigger: 'blur' }
categoryId: [
{ required: true, message: '分类不能为空', trigger: 'blur' }
]
}
}
},
props: {
dictData: Array
},
methods: {
init (id) {
this.dataForm.materialId = id || 0
this.dataForm.id = id || null
this.visible = true
this.$nextTick(() => {
this.$refs['dataForm'].resetFields()
if (this.dataForm.materialId) {
this.$http({
url: this.$http.adornUrl(`/material/material/info/${this.dataForm.materialId}`),
method: 'get',
params: this.$http.adornParams()
}).then(({data}) => {
if (data && data.code === 200) {
this.dataForm.materialCode = data.material.materialCode
this.dataForm.materialName = data.material.materialName
this.dataForm.materialType = String(data.material.materialType)
this.dataForm.isOn = data.material.isOn
}
})
if (this.dataForm.id) {
this.getInfo()
}
})
},
// 获取信息
getInfo () {
this.$http({
url: this.$http.adornUrl(`/knowledge/info/${this.dataForm.id}`),
method: 'get'
}).then(({data}) => {
if (data && data.code === 200) {
const knowledge = data.knowledge
this.dataForm.title = knowledge.title
this.dataForm.subtitle = knowledge.subtitle
this.dataForm.content = knowledge.content
this.dataForm.plainContent = knowledge.plainContent
this.dataForm.summary = knowledge.summary
this.dataForm.categoryId = knowledge.categoryId
this.dataForm.coverImage = knowledge.coverImage
this.dataForm.status = knowledge.status
this.dataForm.tags = knowledge.tags
this.dataForm.isTop = knowledge.isTop
this.dataForm.expireTime = knowledge.expireTime
}
})
},
@@ -92,17 +170,20 @@ export default {
dataFormSubmit () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
this.submitLoading = true
// 生成纯文本内容去除HTML标签
this.dataForm.plainContent = this.dataForm.content.replace(/<[^>]+>/g, '')
const url = !this.dataForm.id ? '/knowledge/save' : '/knowledge/update'
const method = !this.dataForm.id ? 'post' : 'put'
this.$http({
url: this.$http.adornUrl(`/material/material/${!this.dataForm.materialId ? 'save' : 'update'}`),
method: 'post',
data: this.$http.adornData({
'materialId': this.dataForm.materialId || undefined,
'materialCode': this.dataForm.materialCode,
'materialName': this.dataForm.materialName,
'materialType': this.dataForm.materialType,
'isOn': this.dataForm.isOn
})
url: this.$http.adornUrl(url),
method: method,
data: this.$http.adornData(this.dataForm)
}).then(({data}) => {
this.submitLoading = false
if (data && data.code === 200) {
this.$message({
message: '操作成功',
@@ -116,6 +197,8 @@ export default {
} else {
this.$message.error(data.msg)
}
}).catch(() => {
this.submitLoading = false
})
}
})
@@ -123,3 +206,6 @@ export default {
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,493 @@
<template>
<el-dialog
title="知识文档详情"
:close-on-click-modal="false"
:visible.sync="visible"
width="80%"
top="5vh"
class="knowledge-detail-dialog">
<div v-loading="loading" class="knowledge-detail">
<!-- 文档头部 -->
<div class="knowledge-header">
<h1 class="knowledge-title">
<el-tag v-if="knowledge.isTop === 1" type="danger" size="small" style="margin-right: 10px;">置顶</el-tag>
{{ knowledge.title }}
</h1>
<p class="knowledge-subtitle" v-if="knowledge.subtitle">{{ knowledge.subtitle }}</p>
<div class="knowledge-meta">
<span class="meta-item">
<i class="el-icon-user"></i> {{ knowledge.authorName }}
</span>
<span class="meta-item">
<i class="el-icon-time"></i> {{ knowledge.publishTime || knowledge.createTime }}
</span>
<span class="meta-item">
<i class="el-icon-view"></i> {{ knowledge.viewCount || 0 }} 浏览
</span>
<span class="meta-item">
<i class="el-icon-star-off"></i> {{ knowledge.likeCount || 0 }} 点赞
</span>
<el-tag :type="getStatusType(knowledge.status)" size="small">{{ getStatusText(knowledge.status) }}</el-tag>
</div>
<!-- 标签 -->
<div class="knowledge-tags" v-if="knowledge.tags">
<el-tag
v-for="(tag, index) in getTagList(knowledge.tags)"
:key="index"
size="small"
style="margin-right: 8px;">
{{ tag }}
</el-tag>
</div>
</div>
<!-- 封面图 -->
<div class="knowledge-cover" v-if="knowledge.coverImage">
<el-image
:src="knowledge.coverImage"
fit="cover"
style="width: 100%; max-height: 400px;">
</el-image>
</div>
<!-- 摘要 -->
<div class="knowledge-summary" v-if="knowledge.summary">
<el-alert
:title="knowledge.summary"
type="info"
:closable="false">
</el-alert>
</div>
<!-- 文档内容 -->
<div class="knowledge-content" v-html="knowledge.content"></div>
<!-- 操作按钮 -->
<div class="knowledge-actions">
<el-button
:type="isLiked ? 'primary' : 'default'"
:icon="isLiked ? 'el-icon-star-on' : 'el-icon-star-off'"
@click="toggleLike">
{{ isLiked ? '已点赞' : '点赞' }} ({{ knowledge.likeCount || 0 }})
</el-button>
<el-button icon="el-icon-edit" @click="editKnowledge">编辑</el-button>
<el-button icon="el-icon-share" @click="shareKnowledge">分享</el-button>
</div>
<!-- 评论区 -->
<div class="knowledge-comments">
<el-divider content-position="left">
<h3>评论区 ({{ commentTotal }})</h3>
</el-divider>
<!-- 发表评论 -->
<div class="comment-input">
<el-input
type="textarea"
:rows="3"
placeholder="请输入评论内容..."
v-model="commentContent"
maxlength="500"
show-word-limit>
</el-input>
<el-button
type="primary"
size="small"
style="margin-top: 10px;"
@click="submitComment"
:loading="commentSubmitting">
发表评论
</el-button>
</div>
<!-- 评论列表 -->
<div class="comment-list" v-loading="commentLoading">
<div class="comment-item" v-for="comment in commentList" :key="comment.id">
<div class="comment-avatar">
<el-avatar :src="comment.userAvatar || defaultAvatar" size="small"></el-avatar>
</div>
<div class="comment-body">
<div class="comment-header">
<span class="comment-author">{{ comment.nickname || comment.username }}</span>
<span class="comment-time">{{ comment.createTime }}</span>
</div>
<div class="comment-content">{{ comment.content }}</div>
<div class="comment-actions">
<el-button
type="text"
size="mini"
@click="likeComment(comment.id)">
<i class="el-icon-star-off"></i> 点赞 ({{ comment.likeCount || 0 }})
</el-button>
<el-button
type="text"
size="mini"
@click="replyComment(comment)">
<i class="el-icon-chat-line-round"></i> 回复
</el-button>
<el-button
type="text"
size="mini"
@click="deleteComment(comment.id)">
<i class="el-icon-delete"></i> 删除
</el-button>
</div>
</div>
</div>
<!-- 评论分页 -->
<el-pagination
v-if="commentTotal > 0"
@current-change="commentPageChange"
:current-page="commentPage"
:page-size="commentPageSize"
:total="commentTotal"
layout="total, prev, pager, next"
style="text-align: center; margin-top: 20px;">
</el-pagination>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
data () {
return {
visible: false,
loading: false,
knowledge: {},
isLiked: false,
commentContent: '',
commentSubmitting: false,
commentLoading: false,
commentList: [],
commentPage: 1,
commentPageSize: 10,
commentTotal: 0,
defaultAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
}
},
methods: {
init (id) {
this.visible = true
this.knowledge = {}
this.commentList = []
this.commentContent = ''
this.isLiked = false
this.$nextTick(() => {
this.getKnowledgeDetail(id)
this.getCommentList(id)
})
},
// 获取知识详情
getKnowledgeDetail (id) {
this.loading = true
this.$http({
url: this.$http.adornUrl(`/knowledge/info/${id}`),
method: 'get'
}).then(({data}) => {
this.loading = false
if (data && data.code === 200) {
this.knowledge = data.knowledge
}
}).catch(() => {
this.loading = false
})
},
// 获取评论列表
getCommentList (knowledgeId) {
this.commentLoading = true
this.$http({
url: this.$http.adornUrl(`/knowledge/comment/listByKnowledge/${knowledgeId || this.knowledge.id}`),
method: 'get',
params: this.$http.adornParams({
page: this.commentPage,
limit: this.commentPageSize
})
}).then(({data}) => {
this.commentLoading = false
if (data && data.code === 200) {
this.commentList = data.page.list
this.commentTotal = data.page.totalCount
}
}).catch(() => {
this.commentLoading = false
})
},
// 点赞/取消点赞
toggleLike () {
const url = this.isLiked ? `/knowledge/unlike/${this.knowledge.id}` : `/knowledge/like/${this.knowledge.id}`
this.$http({
url: this.$http.adornUrl(url),
method: 'post'
}).then(({data}) => {
if (data && data.code === 200) {
this.isLiked = !this.isLiked
this.knowledge.likeCount = this.isLiked ? (this.knowledge.likeCount || 0) + 1 : (this.knowledge.likeCount || 0) - 1
this.$message.success(this.isLiked ? '点赞成功' : '取消点赞')
}
})
},
// 提交评论
submitComment () {
if (!this.commentContent.trim()) {
this.$message.warning('请输入评论内容')
return
}
this.commentSubmitting = true
this.$http({
url: this.$http.adornUrl('/knowledge/comment/add'),
method: 'post',
data: this.$http.adornData({
knowledgeId: this.knowledge.id,
parentId: 0,
content: this.commentContent
})
}).then(({data}) => {
this.commentSubmitting = false
if (data && data.code === 200) {
this.$message.success('评论成功')
this.commentContent = ''
this.commentPage = 1
this.getCommentList()
} else {
this.$message.error(data.msg)
}
}).catch(() => {
this.commentSubmitting = false
})
},
// 点赞评论
likeComment (commentId) {
this.$http({
url: this.$http.adornUrl(`/knowledge/comment/like/${commentId}`),
method: 'post'
}).then(({data}) => {
if (data && data.code === 200) {
this.$message.success('点赞成功')
this.getCommentList()
}
})
},
// 回复评论
replyComment (comment) {
this.$prompt('请输入回复内容', '回复评论', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'textarea'
}).then(({ value }) => {
if (!value) {
this.$message.warning('请输入回复内容')
return
}
this.$http({
url: this.$http.adornUrl('/knowledge/comment/add'),
method: 'post',
data: this.$http.adornData({
knowledgeId: this.knowledge.id,
parentId: comment.id,
content: value
})
}).then(({data}) => {
if (data && data.code === 200) {
this.$message.success('回复成功')
this.getCommentList()
}
})
}).catch(() => {})
},
// 删除评论
deleteComment (commentId) {
this.$confirm('确定要删除该评论吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl(`/knowledge/comment/delete/${commentId}`),
method: 'delete'
}).then(({data}) => {
if (data && data.code === 200) {
this.$message.success('删除成功')
this.getCommentList()
}
})
}).catch(() => {})
},
// 评论分页
commentPageChange (page) {
this.commentPage = page
this.getCommentList()
},
// 编辑知识
editKnowledge () {
this.visible = false
this.$emit('refreshDataList')
// 可以触发父组件的编辑方法
},
// 分享知识
shareKnowledge () {
this.$message.info('分享功能开发中...')
},
// 获取状态类型
getStatusType (status) {
const typeMap = {
'DRAFT': 'info',
'PUBLISHED': 'success',
'REVIEWING': 'warning',
'ARCHIVED': 'danger'
}
return typeMap[status] || 'info'
},
// 获取状态文本
getStatusText (status) {
const textMap = {
'DRAFT': '草稿',
'PUBLISHED': '已发布',
'REVIEWING': '审核中',
'ARCHIVED': '已归档'
}
return textMap[status] || status
},
// 解析标签
getTagList (tags) {
if (!tags) return []
return tags.split(',').filter(tag => tag.trim())
}
}
}
</script>
<style scoped>
.knowledge-detail {
padding: 20px;
}
.knowledge-header {
margin-bottom: 30px;
}
.knowledge-title {
font-size: 28px;
font-weight: bold;
color: #303133;
margin-bottom: 10px;
line-height: 1.4;
}
.knowledge-subtitle {
font-size: 16px;
color: #909399;
margin-bottom: 15px;
}
.knowledge-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 15px;
color: #909399;
font-size: 14px;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
}
.knowledge-tags {
margin-top: 15px;
}
.knowledge-cover {
margin-bottom: 30px;
border-radius: 8px;
overflow: hidden;
}
.knowledge-summary {
margin-bottom: 30px;
}
.knowledge-content {
font-size: 16px;
line-height: 1.8;
color: #303133;
margin-bottom: 40px;
min-height: 200px;
}
.knowledge-content >>> img {
max-width: 100%;
height: auto;
}
.knowledge-actions {
text-align: center;
padding: 20px 0;
border-top: 1px solid #EBEEF5;
border-bottom: 1px solid #EBEEF5;
margin-bottom: 30px;
}
.knowledge-comments {
margin-top: 40px;
}
.comment-input {
margin-bottom: 30px;
}
.comment-list {
min-height: 200px;
}
.comment-item {
display: flex;
padding: 15px 0;
border-bottom: 1px solid #EBEEF5;
}
.comment-avatar {
margin-right: 15px;
}
.comment-body {
flex: 1;
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.comment-author {
font-weight: bold;
color: #303133;
}
.comment-time {
color: #909399;
font-size: 12px;
}
.comment-content {
color: #606266;
line-height: 1.6;
margin-bottom: 8px;
}
.comment-actions {
display: flex;
gap: 15px;
}
</style>

View File

@@ -1,19 +1,38 @@
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" size="mini" @keyup.enter.native="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="参数名" clearable></el-input>
<div class="mod-knowledge">
<!-- 搜索表单 -->
<el-form :inline="true" :model="dataForm" size="small" @keyup.enter.native="getDataList()">
<el-form-item label="标题">
<el-input v-model="dataForm.title" placeholder="请输入标题关键字" clearable style="width: 200px;"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="dataForm.status" placeholder="请选择状态" clearable style="width: 150px;">
<el-option label="草稿" value="DRAFT"></el-option>
<el-option label="已发布" value="PUBLISHED"></el-option>
<el-option label="审核中" value="REVIEWING"></el-option>
<el-option label="已归档" value="ARCHIVED"></el-option>
</el-select>
</el-form-item>
<el-form-item label="排序">
<el-select v-model="dataForm.orderBy" placeholder="排序字段" style="width: 120px;">
<el-option label="创建时间" value="create_time"></el-option>
<el-option label="发布时间" value="publish_time"></el-option>
<el-option label="浏览量" value="view_count"></el-option>
<el-option label="点赞数" value="like_count"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button v-if="isAuth('material:material:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button v-if="isAuth('material:material:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
<el-button @click="getDataList()" icon="el-icon-search">查询</el-button>
<el-button type="primary" @click="addOrUpdateHandle()" icon="el-icon-plus">新增</el-button>
<el-button type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0" icon="el-icon-delete">批量删除</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table
:data="dataList"
border
size="mini"
size="small"
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%;">
@@ -27,59 +46,150 @@
type="index"
header-align="center"
align="center"
width="60"
label="序号">
</el-table-column>
<!-- <el-table-column-->
<!-- prop="coverImage"-->
<!-- header-align="center"-->
<!-- align="center"-->
<!-- width="80"-->
<!-- label="封面">-->
<!-- <template slot-scope="scope">-->
<!-- <el-image -->
<!-- v-if="scope.row.coverImage"-->
<!-- style="width: 50px; height: 50px"-->
<!-- :src="scope.row.coverImage"-->
<!-- :preview-src-list="[scope.row.coverImage]"-->
<!-- fit="cover">-->
<!-- </el-image>-->
<!-- <span v-else>-</span>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column
prop="materialType"
prop="title"
header-align="center"
align="center"
label="文章类型">
align="left"
min-width="200"
label="标题"
show-overflow-tooltip>
<template slot-scope="scope">
{{ dictData[0] | findByValue(scope.row.materialType) }}
<el-tag v-if="scope.row.isTop === 1" type="danger" size="mini" style="margin-right: 5px;">置顶</el-tag>
<span class="title-link" @click="viewDetail(scope.row.id)">{{ scope.row.title }}</span>
</template>
</el-table-column>
<el-table-column
prop="materialCode"
prop="summary"
header-align="center"
align="center"
label="文章标题">
align="left"
min-width="180"
label="摘要"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="materialName"
prop="authorName"
header-align="center"
align="center"
label="文章描述">
width="100"
label="作者">
</el-table-column>
<el-table-column
prop="createName"
prop="status"
header-align="center"
align="center"
label="创建人">
width="90"
label="状态">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 'DRAFT'" type="info" size="small">草稿</el-tag>
<el-tag v-else-if="scope.row.status === 'PUBLISHED'" type="success" size="small">已发布</el-tag>
<el-tag v-else-if="scope.row.status === 'REVIEWING'" type="warning" size="small">审核中</el-tag>
<el-tag v-else-if="scope.row.status === 'ARCHIVED'" type="danger" size="small">已归档</el-tag>
</template>
</el-table-column>
<el-table-column
prop="createTime"
prop="viewCount"
header-align="center"
align="center"
label="创建时间">
width="80"
label="浏览">
<template slot-scope="scope">
<span><i class="el-icon-view"></i> {{ scope.row.viewCount || 0 }}</span>
</template>
</el-table-column>
<el-table-column
prop="storageId"
prop="likeCount"
header-align="center"
align="center"
label="附件">
width="80"
label="点赞">
<template slot-scope="scope">
<span><i class="el-icon-star-off"></i> {{ scope.row.likeCount || 0 }}</span>
</template>
</el-table-column>
<el-table-column
prop="tags"
header-align="center"
align="center"
width="120"
label="标签">
<template slot-scope="scope">
<el-tag
v-for="(tag, index) in getTagList(scope.row.tags)"
:key="index"
size="mini"
style="margin: 2px;">
{{ tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="publishTime"
header-align="center"
align="center"
width="150"
label="发布时间">
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
width="260"
label="操作">
<template slot-scope="scope">
<el-button type="text" size="small" @click="addOrUpdateHandle(scope.row.materialId)">修改</el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.materialId)">删除</el-button>
<el-button type="text" size="small" @click="viewDetail(scope.row.id)" icon="el-icon-view">查看</el-button>
<!-- 只有草稿状态才允许编辑 -->
<el-button
v-if="scope.row.status === 'DRAFT'"
type="text"
size="small"
@click="addOrUpdateHandle(scope.row.id)"
icon="el-icon-edit">
编辑
</el-button>
<!-- 只有草稿状态才显示发布按钮 -->
<el-button
v-if="scope.row.status === 'DRAFT'"
type="text"
size="small"
@click="publishHandle(scope.row.id)"
icon="el-icon-upload2">
发布
</el-button>
<!-- 只有已发布状态才显示下架按钮 -->
<el-button
v-if="scope.row.status === 'PUBLISHED'"
type="text"
size="small"
@click="unpublishHandle(scope.row.id)"
icon="el-icon-download">
下架
</el-button>
<el-button type="text" size="small" @click="deleteHandle(scope.row.id)" icon="el-icon-delete">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
@@ -89,19 +199,32 @@
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" :dictData="dictData" @refreshDataList="getDataList"></add-or-update>
<!-- 弹窗新增/修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdate" @refreshDataList="getDataList"></add-or-update>
<!-- 弹窗详情 -->
<knowledge-detail v-if="detailVisible" ref="knowledgeDetail" @refreshDataList="getDataList"></knowledge-detail>
</div>
</template>
<script>
import AddOrUpdate from './knowledge-add-or-update'
import { apiUtils } from '@/utils/dict'
import KnowledgeDetail from './knowledge-detail'
export default {
name: 'Knowledge',
components: {
AddOrUpdate,
KnowledgeDetail
},
data () {
return {
dataForm: {
key: ''
title: '',
status: '',
orderBy: 'create_time',
order: 'desc'
},
dataList: [],
pageIndex: 1,
@@ -110,39 +233,39 @@ export default {
dataListLoading: false,
dataListSelections: [],
addOrUpdateVisible: false,
dictConfigs: [{type: 'dict', code: 'material_type'}],
dictData: []
detailVisible: false
}
},
mixins: [apiUtils],
components: {
AddOrUpdate
},
activated () {
this.getDataList()
},
methods: {
// 获取数据列表
getDataList () {
// this.dataListLoading = true
// this.$http({
// url: this.$http.adornUrl('/material/material/list'),
// method: 'get',
// params: this.$http.adornParams({
// 'page': this.pageIndex,
// 'limit': this.pageSize,
// 'key': this.dataForm.key
// })
// }).then(({data}) => {
// if (data && data.code === 200) {
// this.dataList = data.page.list
// this.totalPage = data.page.totalCount
// } else {
// this.dataList = []
// this.totalPage = 0
// }
// this.dataListLoading = false
// })
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/knowledge/list'),
method: 'post',
data: this.$http.adornData({
page: this.pageIndex,
limit: this.pageSize,
title: this.dataForm.title,
status: this.dataForm.status,
orderBy: this.dataForm.orderBy,
order: this.dataForm.order
})
}).then(({data}) => {
if (data && data.code === 200) {
this.dataList = data.page.list
this.totalPage = data.page.totalCount
} else {
this.dataList = []
this.totalPage = 0
}
this.dataListLoading = false
}).catch(() => {
this.dataListLoading = false
})
},
// 每页数
sizeChangeHandle (val) {
@@ -159,31 +282,34 @@ export default {
selectionChangeHandle (val) {
this.dataListSelections = val
},
// 新增 / 修改
// 新增/修改
addOrUpdateHandle (id) {
this.addOrUpdateVisible = true
this.$nextTick(() => {
this.$refs.addOrUpdate.init(id)
})
},
// 删除
deleteHandle (id) {
var ids = id ? [id] : this.dataListSelections.map(item => {
return item.materialId
// 查看详情
viewDetail (id) {
this.detailVisible = true
this.$nextTick(() => {
this.$refs.knowledgeDetail.init(id)
})
this.$confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
},
// 发布
publishHandle (id) {
this.$confirm('确定要发布该知识文档吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/material/material/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
url: this.$http.adornUrl(`/knowledge/publish/${id}`),
method: 'post'
}).then(({data}) => {
if (data && data.code === 200) {
this.$message({
message: '操作成功',
message: '发布成功',
type: 'success',
duration: 1500,
onClose: () => {
@@ -195,7 +321,85 @@ export default {
}
})
}).catch(() => {})
},
// 下架
unpublishHandle (id) {
this.$confirm('确定要下架该知识文档吗?下架后将变为草稿状态,可重新编辑。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl(`/knowledge/unpublish/${id}`),
method: 'post'
}).then(({data}) => {
if (data && data.code === 200) {
this.$message({
message: '下架成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
} else {
this.$message.error(data.msg)
}
})
}).catch(() => {})
},
// 删除
deleteHandle (id) {
var ids = id ? [id] : this.dataListSelections.map(item => {
return item.id
})
this.$confirm(`确定对选中的${ids.length}条记录进行删除操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 批量删除
let deletePromises = ids.map(itemId => {
return this.$http({
url: this.$http.adornUrl(`/knowledge/delete/${itemId}`),
method: 'delete'
})
})
Promise.all(deletePromises).then(() => {
this.$message({
message: '操作成功',
type: 'success',
duration: 1500,
onClose: () => {
this.getDataList()
}
})
}).catch(() => {
this.$message.error('删除失败')
})
}).catch(() => {})
},
// 解析标签
getTagList (tags) {
if (!tags) return []
return tags.split(',').filter(tag => tag.trim())
}
}
}
</script>
<style scoped>
.mod-knowledge {
padding: 20px;
}
.title-link {
color: #409EFF;
cursor: pointer;
}
.title-link:hover {
text-decoration: underline;
}
</style>