知识库-上传-功能联调

main
ysn 2 weeks ago
parent 455d614f4a
commit 174a714286
  1. 4
      public/index.html
  2. 84
      public/static/aws-sdk.min.js
  3. 1
      public/static/minio.min.js
  4. 13
      src/api/knowledge.js
  5. 3
      src/store/getters.js
  6. 37
      src/store/modules/user.js
  7. 627
      src/utils/requestMinio.js
  8. 290
      src/views/knowledge/index.vue
  9. 42
      src/views/videoCommunication/index.vue

@ -18,6 +18,10 @@
<script type="text/javascript" src="/static/sdk.js"></script> <script type="text/javascript" src="/static/sdk.js"></script>
<!-- 第三库,非必须。使用本地录制功能需要 --> <!-- 第三库,非必须。使用本地录制功能需要 -->
<script type="text/javascript" src="/static/saver.js"></script> <script type="text/javascript" src="/static/saver.js"></script>
<!-- AWS SDK -->
<script type="text/javascript" src="/static/aws-sdk.min.js"></script>
<!-- PDF.js for PDF thumbnail generation -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style> <style>
html, html,

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
Not found: /minio@7.1.3/dist/minio.min.js

@ -20,14 +20,18 @@ export function postKnowledgeCateDelete(data) {
return request({ url: '/knowledge/cate/delete', method: 'post', data }) return request({ url: '/knowledge/cate/delete', method: 'post', data })
} }
// 知识库文件管理 // 知识库文件管理
// 知识库文件-列表 // 知识库文件-列表
export function postKnowledgeList(data) { export function postKnowledgeList(data) {
return request({ url: '/knowledge/list', method: 'post', data }) return request({ url: '/knowledge/list', method: 'post', data })
} }
// 知识库文件-新增 // 知识库文件-新增
export function postKnowledgeCreate(data) { export function postKnowledgeCreate(data) {
return request({ url: '/knowledge/create', method: 'post', data }) return request({ url: '/knowledge/create', method: 'post', data })
} }
// 知识库-预览/播放文件
export function postKnowledgePlay(data) {
return request({ url: '/knowledge/play', method: 'post', data })
}
// 知识库文件-详情 // 知识库文件-详情
export function postKnowledgeDetail(data) { export function postKnowledgeDetail(data) {
return request({ url: '/knowledge/detail', method: 'post', data }) return request({ url: '/knowledge/detail', method: 'post', data })
@ -41,11 +45,6 @@ export function postKnowledgeDelete(data) {
return request({ url: '/knowledge/delete', method: 'post', data }) return request({ url: '/knowledge/delete', method: 'post', data })
} }
// // 知识库-预览/播放文件
// export function knowledgePlay(data) {
// return request({ url: '/knowledge/play', method: 'post', data })
// }
// // 知识库-上传文件 // // 知识库-上传文件
// export function knowledgeUpload(data) { // export function knowledgeUpload(data) {
// return request({ // return request({

@ -31,7 +31,8 @@ const getters = {
isInMeeting: state => state.meeting.isInMeeting, isInMeeting: state => state.meeting.isInMeeting,
meetingType: state => state.meeting.meetingType, meetingType: state => state.meeting.meetingType,
mqttConnected: state => state.mqtt.connected, mqttConnected: state => state.mqtt.connected,
mqttMessages: state => state.mqtt.messages mqttMessages: state => state.mqtt.messages,
config: state => state.user.netConfig
} }
export default getters export default getters

@ -6,6 +6,7 @@ import { login, logout, getInfo, getCommonConfigOptions } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth' import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate" import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg' import defAva from '@/assets/images/profile.jpg'
import { initMinioClient, parseMinioFilePath } from '@/utils/requestMinio'
const user = { const user = {
state: { state: {
@ -18,11 +19,7 @@ const user = {
permissions: [], permissions: [],
dept: '', dept: '',
status: '', status: '',
netConfig: { netConfig: {}
httpProtocol: 'http',
ipAddress: '',
port: ''
}
}, },
mutations: { mutations: {
@ -62,6 +59,7 @@ const user = {
Login({ commit }, userInfo) { Login({ commit }, userInfo) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
login(userInfo).then(res => { login(userInfo).then(res => {
setToken(res.data.token) setToken(res.data.token)
commit('SET_TOKEN', res.data.token) commit('SET_TOKEN', res.data.token)
store.dispatch('lock/unlockScreen') store.dispatch('lock/unlockScreen')
@ -112,12 +110,35 @@ const user = {
}) })
}) })
}, },
// 获取网络配置 // 获取网络配置 - 数组转 key-value 对象
GetNetConfig({ commit }) { GetNetConfig({ commit }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getCommonConfigOptions().then(res => { getCommonConfigOptions().then(res => {
const data = res.data const arr = res.data
commit('SET_NET_CONFIG', data) // 数组转为 key:value 对象
const netConfig = Object.fromEntries(arr.map(item => [item.code, item.value]))
commit('SET_NET_CONFIG', netConfig)
// 初始化 MinIO 客户端
// user.js - GetNetConfig 方法
if (netConfig.MINIO_ENDPOINT) {
const endpointParts = netConfig.MINIO_ENDPOINT.split(":")
initMinioClient({
endPoint: endpointParts[0],
port: parseInt(endpointParts[1]) || 9000,
accessKey: netConfig.MINIO_ACCESS_KEY,
secretKey: netConfig.MINIO_SECRET_KEY,
useSSL: netConfig.MINIO_SECURE === '1' || netConfig.MINIO_SECURE === true,
})
// 补充存储桶配置映射(与 C++ 端一致)
store.commit('SET_MINIO_BUCKETS', {
AVATAR: netConfig.MINIO_BUCKET_AVATAR || 'remote-avatar-test',
FILE: netConfig.MINIO_BUCKET_FILE || 'remote-file-test',
KNOWLEDGE_PERSONAL: netConfig.MINIO_BUCKET_KNOWLEDGE_PERSONAL || 'personal-test',
KNOWLEDGE_PUBLIC: netConfig.MINIO_BUCKET_KNOWLEDGE_PUBLIC || 'public-test',
DOWNLOAD: netConfig.MINIO_BUCKET_DOWNLOAD || 'utalk-test',
KERNEL: netConfig.MINIO_BUCKET_KERNEL || 'remote-kernel-test'
})
}
resolve(res) resolve(res)
}).catch(error => { }).catch(error => {
reject(error) reject(error)

@ -1,26 +1,627 @@
// MinIO client stub - install 'minio' package for full functionality // MinIO client implementation using AWS S3 SDK (MinIO is S3-compatible)
let minioClient = null let s3Client = null
/**
* 初始化 MinIO 客户端使用 AWS S3 SDK
* @param {object} config - MinIO 配置
* @param {string} config.endPoint - 服务器地址
* @param {number} config.port - 端口号
* @param {string} config.accessKey - 访问密钥
* @param {string} config.secretKey - 秘密密钥
* @param {boolean} config.useSSL - 是否使用 HTTPS
*/
export function initMinioClient(config) { export function initMinioClient(config) {
console.warn('MinIO client not available. Install minio package for full functionality.') // 检查是否已加载 AWS SDK
minioClient = { if (typeof AWS === 'undefined') {
endPoint: config.endPoint, console.error('AWS SDK not loaded! Please include aws-sdk.min.js in your HTML')
port: config.port return
} }
s3Client = new AWS.S3({
endpoint: `${config.useSSL ? 'https' : 'http'}://${config.endPoint}:${config.port}`,
accessKeyId: config.accessKey,
secretAccessKey: config.secretKey,
s3ForcePathStyle: true, // 必须强制路径风格以兼容 MinIO
signatureVersion: 'v4', // 使用 SigV4 签名
region: 'us-east-1' // MinIO 默认区域
})
console.log('✅ MinIO client initialized with AWS S3 SDK:', {
endPoint: config.endPoint,
port: config.port,
useSSL: config.useSSL,
region: 'us-east-1'
})
} }
export function getMinioClient() { export function getMinioClient() {
return minioClient return s3Client
} }
export function uploadFile(bucket, objectName, filePath, metaData = {}) { /**
return Promise.reject(new Error('MinIO client not initialized')) * 上传文件到 MinIO
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称文件路径
* @param {File} file - 文件对象
* @param {object} metaData - 元数据可选
* @param {function} onProgress - 进度回调函数可选
* @returns {Promise} - 上传结果
*/
export async function uploadFile(bucket, objectName, file, metaData = {}, onProgress) {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
console.log('=== MinIO Upload ===')
console.log('Bucket:', bucket)
console.log('Object:', objectName)
console.log('File:', file.name, file.size, 'bytes')
// 准备上传参数
const params = {
Bucket: bucket,
Key: objectName,
Body: file,
ContentType: file.type || 'application/octet-stream',
...metaData
}
// 使用 AWS SDK 的 upload 方法
const upload = s3Client.upload(params)
// 进度监控
upload.on('httpUploadProgress', (progress) => {
if (progress && onProgress) {
const percent = Math.round((progress.loaded / progress.total) * 100)
onProgress(percent)
}
})
// 执行上传
upload.promise()
.then((data) => {
console.log('✅ Upload successful:', data)
resolve({
bucket,
objectName: data.Key,
etag: data.ETag,
url: `${s3Client.endpoint.host}/${bucket}/${data.Key}`
})
})
.catch((err) => {
console.error('❌ Upload failed:', err)
reject(new Error('Upload failed: ' + err.message))
})
})
} }
export function downloadFile(bucket, objectName, filePath) { /**
return Promise.reject(new Error('MinIO client not initialized')) * 下载文件获取预签名 URL
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称
* @param {number} expiry - 过期时间默认 24 小时
* @returns {Promise} - 预签名 URL
*/
export async function downloadFile(bucket, objectName, expiry = 24 * 60 * 60) {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
const params = {
Bucket: bucket,
Key: objectName,
Expires: expiry
}
s3Client.getSignedUrl('getObject', params, (err, url) => {
if (err) {
reject(new Error('Failed to get presigned URL: ' + err.message))
} else {
resolve(url)
}
})
})
}
/**
* 获取预签名上传 URL
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称
* @param {number} expiry - 过期时间默认 24 小时
* @returns {Promise} - 预签名上传 URL
*/
export async function getPresignedUrl(bucket, objectName, expiry = 24 * 60 * 60) {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
const params = {
Bucket: bucket,
Key: objectName,
Expires: expiry
}
s3Client.getSignedUrl('putObject', params, (err, url) => {
if (err) {
reject(new Error('Failed to get presigned URL: ' + err.message))
} else {
resolve(url)
}
})
})
}
/**
* 检查存储桶是否存在
* @param {string} bucket - 存储桶名称
* @returns {Promise} - 是否存在
*/
export async function bucketExists(bucket) {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
s3Client.headBucket({ Bucket: bucket }, (err) => {
if (err) {
if (err.code === 'NotFound') {
resolve(false)
} else {
reject(new Error('Failed to check bucket: ' + err.message))
}
} else {
resolve(true)
}
})
})
} }
export function getPresignedUrl(bucket, objectName, expiry = 24 * 60 * 60) { /**
return Promise.reject(new Error('MinIO client not initialized')) * 获取存储桶列表
* @returns {Promise} - 存储桶列表
*/
export async function listBuckets() {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
s3Client.listBuckets((err, data) => {
if (err) {
reject(new Error('Failed to list buckets: ' + err.message))
} else {
resolve(data.Buckets)
}
})
})
}
/**
* 删除对象
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称
* @returns {Promise} - 删除结果
*/
export async function deleteObject(bucket, objectName) {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
const params = {
Bucket: bucket,
Key: objectName
}
s3Client.deleteObject(params, (err, data) => {
if (err) {
reject(new Error('Failed to delete object: ' + err.message))
} else {
resolve(data)
}
})
})
}
/**
* 获取对象信息
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称
* @returns {Promise} - 对象元数据
*/
export async function getObjectMetadata(bucket, objectName) {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
const params = {
Bucket: bucket,
Key: objectName
}
s3Client.headObject(params, (err, data) => {
if (err) {
reject(new Error('Failed to get object metadata: ' + err.message))
} else {
resolve(data)
}
})
})
}
/**
* 解析服务器返回的 MinIO 路径兼容 C++ ParseMinioFilePath 逻辑
* @param {string} path - 格式bucket/object_path remote-file-test/file/meeting/xxx.pdf
* @returns {object} { bucket, object }
*/
export function parseMinioFilePath(path) {
const result = { bucket: '', object: '' }
if (!path || typeof path !== 'string') return result
const pathList = path.split('/')
if (pathList.length >= 2) {
result.bucket = pathList[0]
// 拼接剩余部分为 object 路径(移除末尾多余的 /)
result.object = pathList.slice(1).join('/').replace(/\/$/, '')
}
return result
}
/**
* 获取完整的文件访问 URL
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称
* @returns {string} - 完整 URL
*/
export function getFullFileUrl(bucket, objectName) {
if (!s3Client) {
console.warn('MinIO client not initialized')
return ''
}
const endpoint = s3Client.endpoint.host
const useSSL = s3Client.endpoint.protocol === 'https:'
const protocol = useSSL ? 'https' : 'http'
return `${protocol}://${endpoint}/${bucket}/${objectName}`
}
/**
* 列出存储桶中的对象
* @param {string} bucket - 存储桶名称
* @param {string} prefix - 对象前缀可选
* @returns {Promise} - 对象列表
*/
export async function listObjects(bucket, prefix = '') {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
const params = {
Bucket: bucket,
Prefix: prefix
}
s3Client.listObjectsV2(params, (err, data) => {
if (err) {
reject(new Error('Failed to list objects: ' + err.message))
} else {
resolve(data.Contents)
}
})
})
}
/**
* 生成 PDF 缩略图
* @param {File} file - PDF 文件对象
* @param {number} width - 缩略图宽度默认 200
* @param {number} height - 缩略图高度默认 280
* @returns {Promise} - 缩略图 Blob 对象
*/
export async function generatePdfThumbnail(file, width = 200, height = 280) {
return new Promise((resolve, reject) => {
// 检查是否是 PDF 文件
if (!file.name.toLowerCase().endsWith('.pdf')) {
return reject(new Error('File is not a PDF'))
}
// 检查 PDF.js 是否加载
if (typeof pdfjsLib === 'undefined') {
return reject(new Error('PDF.js is not loaded'))
}
// 设置 PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'
const fileReader = new FileReader()
fileReader.onload = async function() {
try {
// 加载 PDF
const typedArray = new Uint8Array(this.result)
const pdf = await pdfjsLib.getDocument(typedArray).promise
// 获取第一页
const page = await pdf.getPage(1)
// 创建 canvas
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 获取页面尺寸
const viewport = page.getViewport({ scale: 1 })
// 计算缩放比例以适应指定尺寸
const scale = Math.min(width / viewport.width, height / viewport.height)
const scaledViewport = page.getViewport({ scale })
// 设置 canvas 尺寸
canvas.width = scaledViewport.width
canvas.height = scaledViewport.height
// 渲染页面到 canvas
await page.render({
canvasContext: ctx,
viewport: scaledViewport
}).promise
// 转换为 Blob
canvas.toBlob((blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create thumbnail'))
}
}, 'image/png', 1.0)
} catch (error) {
reject(new Error('Failed to generate thumbnail: ' + error.message))
}
}
fileReader.onerror = function() {
reject(new Error('Failed to read file'))
}
fileReader.readAsArrayBuffer(file)
})
}
/**
* 生成视频缩略图截取第一帧
* @param {File} file - 视频文件对象
* @param {number} width - 缩略图宽度默认 200
* @param {number} height - 缩略图高度默认 150
* @returns {Promise} - 缩略图 Blob 对象
*/
export async function generateVideoThumbnail(file, width = 200, height = 150) {
return new Promise((resolve, reject) => {
// 检查是否是视频文件
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv']
const fileName = file.name.toLowerCase()
const isVideo = videoExtensions.some(ext => fileName.endsWith(ext))
if (!isVideo) {
return reject(new Error('File is not a video'))
}
// 创建视频元素
const video = document.createElement('video')
video.crossOrigin = 'anonymous'
// 创建 canvas
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 设置视频源
const url = URL.createObjectURL(file)
video.src = url
// 设置视频属性
video.autoplay = false
video.muted = true
video.playsInline = true
// 监听视频加载完成
video.addEventListener('loadedmetadata', () => {
try {
// 设置当前时间为第一帧(0.5秒处,避免黑屏)
video.currentTime = 0.5
// 监听视频帧加载
video.addEventListener('seeked', () => {
try {
// 获取视频原始尺寸
const videoWidth = video.videoWidth
const videoHeight = video.videoHeight
// 计算缩放比例以适应指定尺寸
const scale = Math.min(width / videoWidth, height / videoHeight)
const newWidth = Math.round(videoWidth * scale)
const newHeight = Math.round(videoHeight * scale)
// 设置 canvas 尺寸
canvas.width = newWidth
canvas.height = newHeight
// 绘制视频帧到 canvas
ctx.drawImage(video, 0, 0, newWidth, newHeight)
// 转换为 Blob
canvas.toBlob((blob) => {
// 清理资源
URL.revokeObjectURL(url)
video.remove()
canvas.remove()
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create video thumbnail'))
}
}, 'image/png', 1.0)
} catch (error) {
URL.revokeObjectURL(url)
video.remove()
canvas.remove()
reject(new Error('Failed to capture video frame: ' + error.message))
}
}, { once: true })
// 如果 seeked 事件没有触发,使用 timeout 作为备选
setTimeout(() => {
try {
const videoWidth = video.videoWidth
const videoHeight = video.videoHeight
const scale = Math.min(width / videoWidth, height / videoHeight)
const newWidth = Math.round(videoWidth * scale)
const newHeight = Math.round(videoHeight * scale)
canvas.width = newWidth
canvas.height = newHeight
ctx.drawImage(video, 0, 0, newWidth, newHeight)
canvas.toBlob((blob) => {
URL.revokeObjectURL(url)
video.remove()
canvas.remove()
if (blob) {
resolve(blob)
}
}, 'image/png', 1.0)
} catch (e) {
console.warn('Timeout fallback for video thumbnail:', e)
}
}, 3000)
} catch (error) {
URL.revokeObjectURL(url)
video.remove()
canvas.remove()
reject(new Error('Failed to load video metadata: ' + error.message))
}
})
// 错误处理
video.addEventListener('error', () => {
URL.revokeObjectURL(url)
video.remove()
canvas.remove()
reject(new Error('Failed to load video: ' + video.error?.message || 'Unknown error'))
})
})
}
/**
* 获取文件类型
* @param {string} fileName - 文件名
* @returns {string} - 文件类型'pdf' | 'video' | 'other'
*/
function getFileType(fileName) {
const lowerName = fileName.toLowerCase()
if (lowerName.endsWith('.pdf')) {
return 'pdf'
}
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv']
if (videoExtensions.some(ext => lowerName.endsWith(ext))) {
return 'video'
}
return 'other'
}
/**
* 上传文件并生成缩略图支持 PDF 和视频
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称
* @param {File} file - 文件对象
* @param {object} metaData - 元数据
* @param {function} onProgress - 进度回调
* @returns {Promise} - 上传结果包含缩略图路径
*/
export async function uploadFileWithThumbnail(bucket, objectName, file, metaData = {}, onProgress) {
const fileType = getFileType(file.name)
// 对于非 PDF/视频文件,直接上传,不执行缩略图逻辑
if (fileType === 'other') {
console.log('Non-PDF/Video file detected, skipping thumbnail generation')
const uploadResult = await uploadFile(bucket, objectName, file, metaData, onProgress)
return {
...uploadResult,
thumbnailPath: ''
}
}
// 先上传原始文件
console.log(`${fileType.toUpperCase()} file detected, will generate thumbnail after upload`)
const uploadResult = await uploadFile(bucket, objectName, file, metaData, onProgress)
// 生成并上传缩略图
let thumbnailPath = ''
try {
console.log(`Generating ${fileType} thumbnail...`)
let thumbnailBlob
if (fileType === 'pdf') {
thumbnailBlob = await generatePdfThumbnail(file, 200, 280)
} else {
thumbnailBlob = await generateVideoThumbnail(file, 200, 150)
}
// 生成缩略图文件名(替换扩展名)
const thumbnailName = objectName.replace(/\.[^/.]+$/, '.png')
// 上传缩略图
const thumbnailResult = await uploadFile(bucket, thumbnailName, new File([thumbnailBlob], thumbnailName, { type: 'image/png' }))
thumbnailPath = thumbnailResult.objectName
console.log(`${fileType.toUpperCase()} thumbnail uploaded:`, thumbnailPath)
} catch (error) {
console.warn(`Failed to generate ${fileType} thumbnail:`, error.message)
}
return {
...uploadResult,
thumbnailPath
}
}
/**
* 简单上传文件不分块适用于小文件
* @param {string} bucket - 存储桶名称
* @param {string} objectName - 对象名称
* @param {File} file - 文件对象
* @param {object} metaData - 元数据
* @returns {Promise} - 上传结果
*/
export async function simpleUpload(bucket, objectName, file, metaData = {}) {
return new Promise((resolve, reject) => {
if (!s3Client) {
return reject(new Error('MinIO client not initialized'))
}
const params = {
Bucket: bucket,
Key: objectName,
Body: file,
ContentType: file.type || 'application/octet-stream',
...metaData
}
// 使用 putObject 而不是 upload,避免分块
s3Client.putObject(params, (err, data) => {
if (err) {
reject(new Error('Upload failed: ' + err.message))
} else {
resolve({
bucket,
objectName,
etag: data.ETag,
url: `${s3Client.endpoint.host}/${bucket}/${objectName}`
})
}
})
})
} }

@ -3,7 +3,7 @@
<el-row :gutter="20"> <el-row :gutter="20">
<!-- 左侧分类菜单 --> <!-- 左侧分类菜单 -->
<el-col :span="6"> <el-col :span="6">
<el-form> <el-form @submit.native.prevent>
<el-form-item prop="name"> <el-form-item prop="name">
<el-input <el-input
v-model="queryRightParams.title" v-model="queryRightParams.title"
@ -12,7 +12,6 @@
size="small" size="small"
prefix-icon="el-icon-search" prefix-icon="el-icon-search"
@keyup.enter.native="handleRightQuery" @keyup.enter.native="handleRightQuery"
@submit.native.prevent
> >
<template slot="append"> <template slot="append">
<el-button <el-button
@ -25,31 +24,18 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-menu :default-active="defaultActive" @select="handleSelect"> <el-menu :default-active="defaultActive" @select="handleSelect">
<!-- 内置标签 -->
<el-menu-item
v-for="(item, index) in systemCateList"
:key="item.id"
:index="item.id"
class="custom-item"
>
<div class="left">
<i :class="getBuiltInIcon(index)" />
<span>{{ item.name }}</span>
</div>
<!-- 内置标签无操作按钮 -->
</el-menu-item>
<!-- 自定义标签 --> <!-- 自定义标签 -->
<el-menu-item <el-menu-item
v-for="(item, index) in userCateList" v-for="(item, index) in cateList"
:key="item.id" :key="item.id"
:index="item.id" :index="item.id"
class="custom-item" class="custom-item"
> >
<div class="left"> <div class="left">
<i class="el-icon-collection-tag" /> <i :class="getBuiltInIcon(index) || 'el-icon-collection-tag'" />
<span>{{ item.name }}</span> <span>{{ item.name }}</span>
</div> </div>
<div class="right"> <div class="right" v-if="item.type == 'user'">
<el-button <el-button
type="text" type="text"
icon="el-icon-edit" icon="el-icon-edit"
@ -82,7 +68,10 @@
icon="el-icon-upload2" icon="el-icon-upload2"
@click="openUploadTypeDialog" @click="openUploadTypeDialog"
v-if=" v-if="
queryRightParams.is_mine != 0 && queryRightParams.is_mine != 2 (queryRightParams.cate_id == 0 &&
queryRightParams.is_mine == 1) ||
(queryRightParams.cate_id != 0 &&
queryRightParams.is_mine === 0)
" "
> >
上传 上传
@ -104,10 +93,14 @@
<el-image <el-image
class="resource-thumb" class="resource-thumb"
fit="contain" fit="contain"
:src="scope.row.thumbnail_path" :src="getFullFilePath(scope.row.thumbnail_path)"
@click="handlePlay(scope.row)" @click="handlePlay(scope.row)"
> >
<div slot="error" class="image-slot"> <div
slot="error"
class="image-slot"
@click="handlePlay(scope.row)"
>
<i :class="getFileIconClass(scope.row.file_type)" /> <i :class="getFileIconClass(scope.row.file_type)" />
</div> </div>
</el-image> </el-image>
@ -118,25 +111,6 @@
prop="title" prop="title"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> />
<!-- <el-table-column
label="文件类型"
align="center"
prop="file_type"
width="120"
/>
<el-table-column
label="描述"
align="center"
prop="description"
width="200"
/>
<el-table-column label="播放次数" align="center" prop="play_times" width="120" />
<el-table-column
label="创建人"
align="center"
prop="username"
width="200"
/> -->
<el-table-column <el-table-column
label="创建时间" label="创建时间"
align="center" align="center"
@ -277,21 +251,13 @@
</div> </div>
</el-dialog> </el-dialog>
<!-- el-upload 组件隐藏只用于触发上传 --> <!-- 隐藏的文件输入用于触发上传 -->
<el-upload <input
ref="upload" ref="fileInput"
:action="uploadUrl" type="file"
:headers="uploadHeaders"
:data="uploadParams"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-progress="handleUploadProgress"
:before-upload="beforeUpload"
:file-list="fileList"
:limit="1"
:on-exceed="handleExceed"
:show-file-list="false"
:accept="uploadAccept" :accept="uploadAccept"
class="hidden-upload-input"
@change="handleFileSelect"
/> />
<!-- 上传进度弹窗 --> <!-- 上传进度弹窗 -->
@ -330,27 +296,37 @@ import {
postKnowledgeList, postKnowledgeList,
postKnowledgeCreate, postKnowledgeCreate,
postKnowledgeDetail, postKnowledgeDetail,
postKnowledgePlay,
postKnowledgeEdit, postKnowledgeEdit,
postKnowledgeDelete, postKnowledgeDelete,
} from "@/api/knowledge"; } from "@/api/knowledge";
import { listUser, delUser, deptTreeSelect } from "@/api/system/user"; import { listUser } from "@/api/system/user";
import { getToken } from "@/utils/auth"; import { mapGetters } from "vuex";
import CreateGroupDialog from "@/views/message/components/CreateGroupDialog"; import CreateGroupDialog from "@/views/message/components/CreateGroupDialog";
import {
uploadFileWithThumbnail,
parseMinioFilePath,
uploadFile,
} from "@/utils/requestMinio";
export default { export default {
name: "Knowledge", name: "Knowledge",
components: { components: {
CreateGroupDialog, CreateGroupDialog,
}, },
computed: {
...mapGetters(["config"]),
},
data() { data() {
return { return {
// - // -
defaultActive: "", defaultActive: "2",
// - // -
systemCateList: [], systemCateList: [],
// - // -
userCateList: [], userCateList: [],
cateList: [],
// - // -
tagDialogVisible: false, tagDialogVisible: false,
// -- // --
@ -387,13 +363,10 @@ export default {
// ================== ================== // ================== ==================
uploadTypeDialogVisible: false, // uploadTypeDialogVisible: false, //
currentFileType: "", // video/pdf currentFileType: "", // video/pdf
uploadUrl: "/api/file/upload", //
uploadHeaders: { Authorization: "Bearer " + getToken() },
uploadParams: { deptId: "", fileType: "" },
uploadAccept: "", uploadAccept: "",
fileList: [], //
uploadProgressDialogVisible: false, uploadProgressDialogVisible: false,
uploadPercent: 0, uploadPercent: 0,
currentUploadFile: null, //
// ================== ================== // ================== ==================
shareDialogVisible: false, // shareDialogVisible: false, //
shareItem: null, // shareItem: null, //
@ -405,6 +378,36 @@ export default {
this.getKnowledgeCateList(); this.getKnowledgeCateList();
}, },
methods: { methods: {
//
// index.vue - methods
getFullFilePath(relativePath) {
if (!relativePath) return "";
// 1. URL
if (
relativePath.startsWith("http://") ||
relativePath.startsWith("https://")
) {
return relativePath;
}
// 2. bucket/object C++
const { bucket, object } = parseMinioFilePath(relativePath);
if (!bucket || !object) {
console.warn("无效的 MinIO 路径格式:", relativePath);
return relativePath;
}
// 3. MinIO Vuex
const { netConfig } = this.$store.state.user;
const useSSL =
netConfig.MINIO_SECURE === "1" || netConfig.MINIO_SECURE === true;
const protocol = useSSL ? "https" : "http";
const endpoint = netConfig.MINIO_ENDPOINT || "47.92.6.51:9100";
// 4. URL C++ protocol://endpoint/bucket/object
return `${protocol}://${endpoint}/${bucket}/${object}`;
},
// - // -
getBuiltInIcon(idx) { getBuiltInIcon(idx) {
return ["el-icon-coin", "el-icon-video-camera", "el-icon-user"][idx]; return ["el-icon-coin", "el-icon-video-camera", "el-icon-user"][idx];
@ -415,14 +418,19 @@ export default {
this.systemCateList = res.data.system_cate_list.map((item) => ({ this.systemCateList = res.data.system_cate_list.map((item) => ({
...item, ...item,
id: item.id.toString(), id: item.id.toString(),
is_mine: item.id.toString(),
type: "system",
})); }));
this.defaultActive = this.systemCateList[0].id;
this.userCateList = res.data.user_cate_list.map((item) => ({ this.userCateList = res.data.user_cate_list.map((item) => ({
...item, ...item,
id: item.id.toString(), id: item.id.toString(),
is_mine: 0,
cate_id: item.id.toString(),
type: "user",
})); }));
this.cateList = [...this.systemCateList, ...this.userCateList];
console.log("知识库左侧分类列表", res.data); console.log("知识库左侧分类列表", res.data);
this.handleSelect(this.defaultActive); this.handleSelect(this.cateList[0].id);
}); });
}, },
// -- // --
@ -461,20 +469,11 @@ export default {
}, },
// - // -
handleSelect(key) { handleSelect(key) {
console.log("handleSelect", key); this.defaultActive = key;
if (key == 0 || key == 2) {
console.log("handleSelect1", key);
this.queryRightParams.is_mine = key;
this.queryRightParams.cate_id = 0;
delete this.queryRightParams.creator_id;
} else {
console.log("handleSelect2", key);
this.queryRightParams.is_mine = key;
this.queryRightParams.creator_id = key;
delete this.queryRightParams.cate_id;
}
// s // s
const selectedItem = this.findMenuItem(key); const selectedItem = this.findMenuItem(key);
this.queryRightParams.is_mine = selectedItem.is_mine;
this.queryRightParams.cate_id = selectedItem.cate_id;
if (selectedItem) { if (selectedItem) {
this.queryRightParams.name = selectedItem.name; this.queryRightParams.name = selectedItem.name;
} }
@ -498,11 +497,8 @@ export default {
// //
findMenuItem(key) { findMenuItem(key) {
// //
const systemItem = this.systemCateList.find((item) => item.id === key); const item = this.cateList.find((item) => item.id === key);
if (systemItem) return systemItem; if (item) return item;
//
const userItem = this.userCateList.find((item) => item.id === key);
if (userItem) return userItem;
return null; return null;
}, },
// - // -
@ -513,7 +509,7 @@ export default {
// - // -
resetRightQuery() { resetRightQuery() {
this.resetForm("queryRightForm"); this.resetForm("queryRightForm");
this.queryRightParams.deptId = null; this.queryRightParams.title = null;
this.handleRightQuery(); this.handleRightQuery();
}, },
// - // -
@ -564,7 +560,6 @@ export default {
getKnowledgeList() { getKnowledgeList() {
this.list = []; this.list = [];
postKnowledgeList(this.queryRightParams).then((res) => { postKnowledgeList(this.queryRightParams).then((res) => {
console.log("知识库右侧-列表", res.data.list);
this.list = res.data.list; this.list = res.data.list;
this.queryRightParams.total = Number(res.data.total); this.queryRightParams.total = Number(res.data.total);
this.queryRightParams.is_mine = res.data.is_mine; this.queryRightParams.is_mine = res.data.is_mine;
@ -573,7 +568,9 @@ export default {
}, },
// - // -
handlePlay(row) { handlePlay(row) {
window.open(row.file_path, "_blank"); postKnowledgePlay({ knowledge_id: row.id }).then((res) => {
window.open(this.getFullFilePath(row.file_path), "_blank");
});
}, },
handleUpdateFileName(row) { handleUpdateFileName(row) {
this.fileNameForm = { title: row.title, knowledge_id: row.id }; this.fileNameForm = { title: row.title, knowledge_id: row.id };
@ -640,41 +637,110 @@ export default {
this.uploadTypeDialogVisible = false; this.uploadTypeDialogVisible = false;
this.currentFileType = type; this.currentFileType = type;
this.uploadAccept = type === "video" ? "video/mp4" : "application/pdf"; this.uploadAccept = type === "video" ? "video/mp4" : "application/pdf";
// //
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.upload.$children[0].$el.click(); this.$refs.fileInput.click();
}); });
}, },
// //
beforeUpload(file) { handleFileSelect(event) {
this.uploadParams.deptId = this.systemCateList[2].id; const file = event.target.files[0];
this.uploadParams.fileType = this.currentFileType; if (!file) return;
this.currentUploadFile = file;
this.uploadProgressDialogVisible = true; this.uploadProgressDialogVisible = true;
this.uploadPercent = 0; this.uploadPercent = 0;
return true;
}, // MinIO
// this.uploadToMinio(file);
handleExceed() {
this.$modal.msgError("每次只能上传一个文件!"); //
}, event.target.value = "";
//
handleUploadProgress(e) {
this.uploadPercent = Math.round(e.percent);
}, },
// // MinIO PDF
handleUploadSuccess() { async uploadToMinio(file) {
this.uploadPercent = 100; try {
setTimeout(() => { // MinIO
await this.ensureMinioInitialized();
// is_mine
const isMine = this.queryRightParams.is_mine == "0";
const bucket = this.config?.MINIO_BUCKET_KNOWLEDGE_PERSONAL;
//
const timestamp = Date.now();
const fileName = file.name;
const objectName = `${this.currentFileType}/${this.queryRightParams.cate_id}/${timestamp}/${fileName}`;
console.log(`Uploading to bucket: ${bucket}, object: ${objectName}`);
// 使PDF
const result = await uploadFileWithThumbnail(
bucket,
objectName,
file,
{},
(percent) => {
this.uploadPercent = percent;
}
);
console.log("MinIO 上传成功:", result);
//
await this.saveKnowledgeToDB(
result.objectName,
file,
result.thumbnailPath,
bucket
);
//
this.uploadPercent = 100;
setTimeout(() => {
this.uploadProgressDialogVisible = false;
this.$modal.msgSuccess("上传成功");
this.getKnowledgeList();
}, 500);
} catch (error) {
console.error("上传失败:", error);
this.uploadProgressDialogVisible = false; this.uploadProgressDialogVisible = false;
this.$modal.msgSuccess("上传成功"); this.$modal.msgError("上传失败: " + error.message);
this.getKnowledgeList(); // }
this.fileList = []; },
}, 500); // MinIO
async ensureMinioInitialized() {
const config = this.$store.getters.config;
if (!config || !config.MINIO_ENDPOINT) {
console.log("MinIO config not loaded, fetching...");
await this.$store.dispatch("GetNetConfig");
}
}, },
// //
handleUploadError() { async saveKnowledgeToDB(filePath, file, thumbnailPath = "", bucket = "") {
this.uploadProgressDialogVisible = false; console.log("saveKnowledgeToDB:", filePath, file, thumbnailPath, bucket);
this.$modal.msgError("上传失败");
// bucket/object
const fullFilePath = bucket ? `${bucket}/${filePath}` : filePath;
//
const fullThumbnailPath = thumbnailPath
? `${bucket}/${thumbnailPath}`
: "";
console.log("fullThumbnailPath:", fullThumbnailPath);
const data = {
cate_id: this.queryRightParams.cate_id,
file_name: file.name,
file_path: fullFilePath,
file_size: file.size,
file_type: this.currentFileType,
kid: 21,
thumbnail_path:
fullThumbnailPath ||
"personal-test/video/688/1780036088/1633500241136.png",
title: file.name.replace(/\.[^/.]+$/, ""), //
true_file_size: file.size,
};
await postKnowledgeCreate(data);
console.log("知识库记录保存成功");
}, },
}, },
}; };
@ -704,6 +770,10 @@ export default {
font-size: 20px; font-size: 20px;
} }
.hidden-upload-input {
display: none;
}
.upload-type-box { .upload-type-box {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;

@ -50,14 +50,24 @@
stripe stripe
height="calc(100vh - 550px)" height="calc(100vh - 550px)"
> >
<el-table-column label="视迅头像" prop="avatar" align="center" width="50"> <el-table-column label="头像" prop="avatar" align="center" width="50">
<template slot-scope="scope"> <template slot-scope="scope">
<el-avatar :src="scope.row.avatar" /> <!-- MINIO_ENDPOINT_HTTPS+scope.row.avatar -->
<el-avatar
:src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
scope.row.avatar
"
/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="视迅名称" prop="name" align="center" /> <el-table-column label="会诊名称" prop="name" align="center" />
<el-table-column label="视迅类型" prop="meet_type_name" align="center" /> <el-table-column
<el-table-column label="加入会诊时间" prop="create_time" align="center" /> label="会议类型名称"
prop="meet_type_name"
align="center"
/>
<el-table-column label="创建时间" prop="create_time" align="center" />
<!-- 状态status 1-开始 0-结束 --> <!-- 状态status 1-开始 0-结束 -->
<el-table-column label="操作" align="center" width="50"> <el-table-column label="操作" align="center" width="50">
<template slot-scope="scope"> <template slot-scope="scope">
@ -115,7 +125,12 @@
<el-col :span="3"> <el-col :span="3">
<el-form-item> <el-form-item>
<el-avatar <el-avatar
:src="meetingDetail.user_list && meetingDetail.user_list[0] ? meetingDetail.user_list[0].avatar : ''" :src="
meetingDetail.user_list && meetingDetail.user_list[0]
? $store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
meetingDetail.user_list[0].avatar
: ''
"
class="person-avatar" class="person-avatar"
/> />
</el-form-item> </el-form-item>
@ -133,7 +148,10 @@
<el-col :span="3"> <el-col :span="3">
<el-form-item> <el-form-item>
<el-avatar <el-avatar
:src="item.avatar" :src="
$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS +
item.avatar
"
class="person-avatar" class="person-avatar"
/> />
</el-form-item> </el-form-item>
@ -222,11 +240,11 @@ export default {
}, },
// //
formatDuration(seconds) { formatDuration(seconds) {
if (!seconds) return '-' if (!seconds) return "-";
const h = Math.floor(seconds / 3600) const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60) const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60 const s = seconds % 60;
return `${h}${m}${s}` return `${h}${m}${s}`;
}, },
// //
joinMeeting() { joinMeeting() {

Loading…
Cancel
Save