知识库-上传-功能联调

main
ysn 1 day 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/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]-->
<style>
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 })
}
// 知识库文件管理
// 知识库文件-列表
// 知识库文件-列表
export function postKnowledgeList(data) {
return request({ url: '/knowledge/list', method: 'post', data })
}
// 知识库文件-新增
export function postKnowledgeCreate(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) {
return request({ url: '/knowledge/detail', method: 'post', data })
@ -41,11 +45,6 @@ export function postKnowledgeDelete(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) {
// return request({

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

@ -6,6 +6,7 @@ import { login, logout, getInfo, getCommonConfigOptions } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
import defAva from '@/assets/images/profile.jpg'
import { initMinioClient, parseMinioFilePath } from '@/utils/requestMinio'
const user = {
state: {
@ -18,11 +19,7 @@ const user = {
permissions: [],
dept: '',
status: '',
netConfig: {
httpProtocol: 'http',
ipAddress: '',
port: ''
}
netConfig: {}
},
mutations: {
@ -62,6 +59,7 @@ const user = {
Login({ commit }, userInfo) {
return new Promise((resolve, reject) => {
login(userInfo).then(res => {
setToken(res.data.token)
commit('SET_TOKEN', res.data.token)
store.dispatch('lock/unlockScreen')
@ -112,12 +110,35 @@ const user = {
})
})
},
// 获取网络配置
// 获取网络配置 - 数组转 key-value 对象
GetNetConfig({ commit }) {
return new Promise((resolve, reject) => {
getCommonConfigOptions().then(res => {
const data = res.data
commit('SET_NET_CONFIG', data)
const arr = res.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)
}).catch(error => {
reject(error)

@ -1,26 +1,627 @@
// MinIO client stub - install 'minio' package for full functionality
let minioClient = null
// MinIO client implementation using AWS S3 SDK (MinIO is S3-compatible)
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) {
console.warn('MinIO client not available. Install minio package for full functionality.')
minioClient = {
endPoint: config.endPoint,
port: config.port
// 检查是否已加载 AWS SDK
if (typeof AWS === 'undefined') {
console.error('AWS SDK not loaded! Please include aws-sdk.min.js in your HTML')
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() {
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-col :span="6">
<el-form>
<el-form @submit.native.prevent>
<el-form-item prop="name">
<el-input
v-model="queryRightParams.title"
@ -12,7 +12,6 @@
size="small"
prefix-icon="el-icon-search"
@keyup.enter.native="handleRightQuery"
@submit.native.prevent
>
<template slot="append">
<el-button
@ -25,31 +24,18 @@
</el-form-item>
</el-form>
<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
v-for="(item, index) in userCateList"
v-for="(item, index) in cateList"
:key="item.id"
:index="item.id"
class="custom-item"
>
<div class="left">
<i class="el-icon-collection-tag" />
<i :class="getBuiltInIcon(index) || 'el-icon-collection-tag'" />
<span>{{ item.name }}</span>
</div>
<div class="right">
<div class="right" v-if="item.type == 'user'">
<el-button
type="text"
icon="el-icon-edit"
@ -82,7 +68,10 @@
icon="el-icon-upload2"
@click="openUploadTypeDialog"
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
class="resource-thumb"
fit="contain"
:src="scope.row.thumbnail_path"
:src="getFullFilePath(scope.row.thumbnail_path)"
@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)" />
</div>
</el-image>
@ -118,25 +111,6 @@
prop="title"
: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
label="创建时间"
align="center"
@ -277,21 +251,13 @@
</div>
</el-dialog>
<!-- el-upload 组件隐藏只用于触发上传 -->
<el-upload
ref="upload"
:action="uploadUrl"
: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"
<!-- 隐藏的文件输入用于触发上传 -->
<input
ref="fileInput"
type="file"
:accept="uploadAccept"
class="hidden-upload-input"
@change="handleFileSelect"
/>
<!-- 上传进度弹窗 -->
@ -330,27 +296,37 @@ import {
postKnowledgeList,
postKnowledgeCreate,
postKnowledgeDetail,
postKnowledgePlay,
postKnowledgeEdit,
postKnowledgeDelete,
} from "@/api/knowledge";
import { listUser, delUser, deptTreeSelect } from "@/api/system/user";
import { getToken } from "@/utils/auth";
import { listUser } from "@/api/system/user";
import { mapGetters } from "vuex";
import CreateGroupDialog from "@/views/message/components/CreateGroupDialog";
import {
uploadFileWithThumbnail,
parseMinioFilePath,
uploadFile,
} from "@/utils/requestMinio";
export default {
name: "Knowledge",
components: {
CreateGroupDialog,
},
computed: {
...mapGetters(["config"]),
},
data() {
return {
// -
defaultActive: "",
defaultActive: "2",
// -
systemCateList: [],
// -
userCateList: [],
cateList: [],
// -
tagDialogVisible: false,
// --
@ -387,13 +363,10 @@ export default {
// ================== ==================
uploadTypeDialogVisible: false, //
currentFileType: "", // video/pdf
uploadUrl: "/api/file/upload", //
uploadHeaders: { Authorization: "Bearer " + getToken() },
uploadParams: { deptId: "", fileType: "" },
uploadAccept: "",
fileList: [], //
uploadProgressDialogVisible: false,
uploadPercent: 0,
currentUploadFile: null, //
// ================== ==================
shareDialogVisible: false, //
shareItem: null, //
@ -405,6 +378,36 @@ export default {
this.getKnowledgeCateList();
},
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) {
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) => ({
...item,
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) => ({
...item,
id: item.id.toString(),
is_mine: 0,
cate_id: item.id.toString(),
type: "user",
}));
this.cateList = [...this.systemCateList, ...this.userCateList];
console.log("知识库左侧分类列表", res.data);
this.handleSelect(this.defaultActive);
this.handleSelect(this.cateList[0].id);
});
},
// --
@ -461,20 +469,11 @@ export default {
},
// -
handleSelect(key) {
console.log("handleSelect", 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;
}
this.defaultActive = key;
// s
const selectedItem = this.findMenuItem(key);
this.queryRightParams.is_mine = selectedItem.is_mine;
this.queryRightParams.cate_id = selectedItem.cate_id;
if (selectedItem) {
this.queryRightParams.name = selectedItem.name;
}
@ -498,11 +497,8 @@ export default {
//
findMenuItem(key) {
//
const systemItem = this.systemCateList.find((item) => item.id === key);
if (systemItem) return systemItem;
//
const userItem = this.userCateList.find((item) => item.id === key);
if (userItem) return userItem;
const item = this.cateList.find((item) => item.id === key);
if (item) return item;
return null;
},
// -
@ -513,7 +509,7 @@ export default {
// -
resetRightQuery() {
this.resetForm("queryRightForm");
this.queryRightParams.deptId = null;
this.queryRightParams.title = null;
this.handleRightQuery();
},
// -
@ -564,7 +560,6 @@ export default {
getKnowledgeList() {
this.list = [];
postKnowledgeList(this.queryRightParams).then((res) => {
console.log("知识库右侧-列表", res.data.list);
this.list = res.data.list;
this.queryRightParams.total = Number(res.data.total);
this.queryRightParams.is_mine = res.data.is_mine;
@ -573,7 +568,9 @@ export default {
},
// -
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) {
this.fileNameForm = { title: row.title, knowledge_id: row.id };
@ -640,41 +637,110 @@ export default {
this.uploadTypeDialogVisible = false;
this.currentFileType = type;
this.uploadAccept = type === "video" ? "video/mp4" : "application/pdf";
//
//
this.$nextTick(() => {
this.$refs.upload.$children[0].$el.click();
this.$refs.fileInput.click();
});
},
//
beforeUpload(file) {
this.uploadParams.deptId = this.systemCateList[2].id;
this.uploadParams.fileType = this.currentFileType;
//
handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
this.currentUploadFile = file;
this.uploadProgressDialogVisible = true;
this.uploadPercent = 0;
return true;
},
//
handleExceed() {
this.$modal.msgError("每次只能上传一个文件!");
},
//
handleUploadProgress(e) {
this.uploadPercent = Math.round(e.percent);
// MinIO
this.uploadToMinio(file);
//
event.target.value = "";
},
//
handleUploadSuccess() {
this.uploadPercent = 100;
setTimeout(() => {
// MinIO PDF
async uploadToMinio(file) {
try {
// 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.$modal.msgSuccess("上传成功");
this.getKnowledgeList(); //
this.fileList = [];
}, 500);
this.$modal.msgError("上传失败: " + error.message);
}
},
// 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() {
this.uploadProgressDialogVisible = false;
this.$modal.msgError("上传失败");
//
async saveKnowledgeToDB(filePath, file, thumbnailPath = "", bucket = "") {
console.log("saveKnowledgeToDB:", filePath, file, thumbnailPath, bucket);
// 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;
}
.hidden-upload-input {
display: none;
}
.upload-type-box {
display: flex;
justify-content: space-around;

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

Loading…
Cancel
Save