海信医疗-远程超声管理平台-信创国产化
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

830 lines
25 KiB

<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 左侧分类菜单 -->
<el-col :span="6">
<el-form @submit.native.prevent>
<el-form-item prop="name">
<el-input
v-model="queryRightParams.title"
placeholder="输入内容过滤"
clearable
size="small"
prefix-icon="el-icon-search"
@keyup.enter.native="handleRightQuery"
>
<template slot="append">
<el-button
slot="append"
icon="el-icon-refresh-right"
@click="resetRightQuery"
/>
</template>
</el-input>
</el-form-item>
</el-form>
<el-menu :default-active="defaultActive" @select="handleSelect">
<!-- 自定义标签 -->
<el-menu-item
v-for="(item, index) in cateList"
:key="item.id"
:index="item.id"
class="custom-item"
>
<div class="left">
<i :class="getBuiltInIcon(index) || 'el-icon-collection-tag'" />
<span>{{ item.name }}</span>
</div>
<div class="right" v-if="item.type == 'user'">
<el-button
type="text"
icon="el-icon-edit"
@click.stop="handleEditTag(item)"
/>
<el-button
type="text"
icon="el-icon-delete"
@click.stop="handleLeftDelete(item)"
/>
</div>
</el-menu-item>
<el-menu-item>
<div class="left" @click.stop="handleAddTag">
<i class="el-icon-edit-outline" />
<span>自定义标签</span>
</div>
</el-menu-item>
</el-menu>
</el-col>
<!-- 右侧资源列表 -->
<el-col :span="18">
<el-card shadow="never">
<div slot="header" class="clearfix">
<span>{{ queryRightParams.name }}</span>
<el-button
style="float: right; padding: 3px 0"
type="text"
icon="el-icon-upload2"
@click="openUploadTypeDialog"
v-if="
(queryRightParams.cate_id == 0 &&
queryRightParams.is_mine == 1) ||
(queryRightParams.cate_id != 0 &&
queryRightParams.is_mine === 0)
"
>
上传
</el-button>
</div>
<!-- 资源列表 -->
<el-table
:data="list"
height="calc(100vh - 264px)"
:show-header="false"
>
<el-table-column
label="内容"
prop="thumbnail_path"
align="center"
width="250"
>
<template slot-scope="scope">
<el-image
class="resource-thumb"
fit="contain"
:src="getFullFilePath(scope.row.thumbnail_path)"
@click="handlePlay(scope.row)"
>
<div
slot="error"
class="image-slot"
@click="handlePlay(scope.row)"
>
<i :class="getFileIconClass(scope.row.file_type)" />
</div>
</el-image>
</template>
</el-table-column>
<el-table-column
label="标题"
prop="title"
:show-overflow-tooltip="true"
/>
<el-table-column
label="创建时间"
align="center"
prop="create_time"
width="200"
>
<template slot-scope="scope">
<span>{{ parseTime(scope.row.create_time) }}上传</span>
</template>
</el-table-column>
<el-table-column
label="文件大小"
align="center"
prop="file_size"
width="120"
>
<template slot-scope="scope">
<span>{{ formatfile_size(scope.row.file_size) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
width="240"
class-name="small-padding fixed-width"
>
<template slot-scope="scope">
<el-button
type="text"
icon="el-icon-share"
@click="handleShare(scope.row)"
>
分享
</el-button>
<el-button
v-if="scope.row.is_mine == 1"
type="text"
icon="el-icon-edit"
@click="handleUpdateFileName(scope.row)"
>
修改
</el-button>
<el-button
v-if="scope.row.is_mine == 1"
type="text"
icon="el-icon-delete"
@click="handleRightDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="queryRightParams.total > 0"
:total="queryRightParams.total"
:page.sync="queryRightParams.page"
:limit.sync="queryRightParams.pageSize"
@pagination="getKnowledgeList"
/>
</el-card>
</el-col>
</el-row>
<!-- 自定义标签弹窗 -->
<el-dialog
title="自定义标签名称"
:visible.sync="tagDialogVisible"
width="400px"
destroy-on-close
>
<el-form
:model="tagForm"
:rules="tagRules"
ref="tagFormRef"
label-width="80px"
>
<el-form-item label="标签名称" prop="name">
<el-input
v-model="tagForm.name"
placeholder="请输入最多8个字符"
maxlength="8"
show-word-limit
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitTagForm">确认</el-button>
<el-button @click="cancelTagForm">取消</el-button>
</div>
</el-dialog>
<!-- 修改文件名弹窗 -->
<el-dialog
title="修改"
:visible.sync="fileNameDialogVisible"
width="400px"
destroy-on-close
>
<el-form
:model="fileNameForm"
:rules="fileNameRules"
ref="fileNameFormRef"
label-width="80px"
>
<el-form-item label="标题" prop="title">
<el-input
v-model="fileNameForm.title"
placeholder="请输入标题"
show-word-limit
maxlength="8"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileNameForm">确认</el-button>
<el-button @click="cancelFileNameForm">取消</el-button>
</div>
</el-dialog>
<!-- 选择上传类型弹窗 -->
<el-dialog
title="请选择上传文件类型"
:visible.sync="uploadTypeDialogVisible"
width="400px"
:show-footer="false"
>
<div class="upload-type-box">
<div class="type-item" @click="selectFileType('video')">
<i class="el-icon-video-play" />
<span>视频文件(MP4)</span>
</div>
<div class="type-item" @click="selectFileType('pdf')">
<i class="el-icon-document" />
<span>PDF 文件</span>
</div>
</div>
</el-dialog>
<!-- 隐藏的文件输入(用于触发上传) -->
<input
ref="fileInput"
type="file"
:accept="uploadAccept"
class="hidden-upload-input"
@change="handleFileSelect"
/>
<!-- 上传进度弹窗 -->
<el-dialog
title="文件上传中"
:visible.sync="uploadProgressDialogVisible"
width="400px"
:close-on-click-modal="false"
:show-footer="false"
>
<div class="progress-box">
<el-progress :percentage="uploadPercent" />
<p style="margin-top: 15px">资源上传中请稍候...</p>
</div>
</el-dialog>
<!-- 分享弹窗 - 选择联系人 -->
<CreateGroupDialog
:visible.sync="shareDialogVisible"
title="选择人员"
:min-select-count="1"
@confirm="handleShareToContacts"
/>
</div>
</template>
<script>
import {
postKnowledgeCateList,
postKnowledgeCateCreate,
postKnowledgeCateEdit,
postKnowledgeCateDelete,
postKnowledgeList,
postKnowledgeCreate,
postKnowledgeDetail,
postKnowledgePlay,
postKnowledgeEdit,
postKnowledgeDelete,
postMessagePushToUser,
} from "@/api/knowledge";
import { mapGetters } from "vuex";
import CreateGroupDialog from "@/views/message/components/CreateGroupDialog";
import {
uploadFileWithThumbnail,
parseMinioFilePath,
uploadFile,
} from "@/utils/requestMinio";
import { MessageType } from "@/utils/constants";
export default {
name: "Knowledge",
components: {
CreateGroupDialog,
},
computed: {
...mapGetters(["config"]),
},
data() {
return {
// 知识库左侧-默认选中分类
defaultActive: "2",
// 知识库左侧-系统分类
systemCateList: [],
// 知识库左侧-用户分类
userCateList: [],
cateList: [],
// 知识库左侧-自定义标签
tagDialogVisible: false,
// 知识库左侧-自定义标签-表单数据
tagForm: { id: null, name: "" },
// 知识库左侧-自定义标签-表单验证规则
tagRules: {
name: [
{ required: true, message: "标签名称不能为空", trigger: "blur" },
{ max: 8, message: "最多8个字符", trigger: "blur" },
],
},
// 知识库右侧-查询参数
queryRightParams: {
title: "",
cate_id: 0,
is_mine: "-1",
page: 1,
pageSize: 10,
total: 0,
},
// 知识库右侧-列表
list: [],
// 知识库右侧-文件名弹窗
fileNameDialogVisible: false,
// 知识库右侧-文件名弹窗-表单数据
fileNameForm: { userId: undefined, title: "" },
// 知识库右侧-文件名弹窗-表单验证规则
fileNameRules: {
title: [{ required: true, message: "标题不能为空", trigger: "blur" }],
},
loading: true,
userList: [],
// ================== 上传相关 ==================
uploadTypeDialogVisible: false, // 类型选择弹窗
currentFileType: "", // 当前选择的文件类型 video/pdf
uploadAccept: "",
uploadProgressDialogVisible: false,
uploadPercent: 0,
currentUploadFile: null, // 当前上传的文件对象
// ================== 分享相关 ==================
shareDialogVisible: false, // 分享弹窗
shareItem: null, // 当前分享的文件
};
},
created() {
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];
},
// 知识库左侧-分类列表
getKnowledgeCateList() {
postKnowledgeCateList().then((res) => {
this.systemCateList = res.data.system_cate_list.map((item) => ({
...item,
id: item.id.toString(),
is_mine: item.id.toString(),
type: "system",
}));
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.cateList[0].id);
});
},
// 知识库左侧-自定义标签-新增
handleAddTag() {
this.tagForm = { name: "" };
this.tagDialogVisible = true;
},
// 知识库左侧-自定义标签-编辑
handleEditTag(t) {
this.tagForm = { ...t, cate_id: t.id };
this.tagDialogVisible = true;
},
// 知识库左侧-自定义标签-提交
submitTagForm() {
this.$refs["tagFormRef"].validate((valid) => {
if (valid) {
if (this.tagForm.id) {
postKnowledgeCateEdit(this.tagForm).then(() => {
this.$modal.msgSuccess("修改成功");
this.tagDialogVisible = false;
this.getKnowledgeCateList();
});
} else {
postKnowledgeCateCreate(this.tagForm).then(() => {
this.$modal.msgSuccess("新增成功");
this.tagDialogVisible = false;
this.getKnowledgeCateList();
});
}
}
});
},
// 知识库左侧-自定义标签-取消
cancelTagForm() {
this.tagDialogVisible = false;
},
// 知识库右侧-查询
handleSelect(key) {
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;
}
this.handleRightQuery();
},
// 删除资源
handleLeftDelete(row) {
this.$modal
.confirm(
`是否删除该自定义标签?若删除,该标签下资源将迁移至[个人收藏录像]标签下。`
)
.then(function () {
return postKnowledgeCateDelete({ cate_id: row.id });
})
.then(() => {
this.getKnowledgeCateList();
this.$modal.msgSuccess("删除成功");
})
.catch(() => {});
},
// 查找菜单项
findMenuItem(key) {
// 先在系统分类中查找
const item = this.cateList.find((item) => item.id === key);
if (item) return item;
return null;
},
// 知识库右侧-列表
handleRightQuery() {
this.queryRightParams.page = 1;
this.getKnowledgeList();
},
// 知识库右侧-重置查询条件
resetRightQuery() {
this.resetForm("queryRightForm");
this.queryRightParams.title = null;
this.handleRightQuery();
},
// 知识库右侧-文件图标
getFileIconClass(fileName) {
if (!fileName) return "el-icon-document";
const suffix = fileName.split(".").pop().toLowerCase();
const iconMap = {
txt: "el-icon-tickets",
pdf: "el-icon-document-copy",
mp3: "el-icon-headset",
wav: "el-icon-headset",
avi: "el-icon-video-camera",
mp4: "el-icon-video-camera",
wmv: "el-icon-video-camera",
mov: "el-icon-video-camera",
zip: "el-icon-folder-opened",
rar: "el-icon-folder-opened",
ppt: "el-icon-data-board",
pps: "el-icon-data-board",
xls: "el-icon-s-grid",
xlsx: "el-icon-s-grid",
doc: "el-icon-document",
docx: "el-icon-document",
jpg: "el-icon-picture",
jpeg: "el-icon-picture",
jpe: "el-icon-picture",
bmp: "el-icon-picture",
gif: "el-icon-picture",
png: "el-icon-picture",
tif: "el-icon-picture",
tiff: "el-icon-picture",
};
return iconMap[suffix] || "el-icon-document";
},
// 知识库右侧-格式化文件大小
formatfile_size(bytes) {
if (!bytes) return "0B";
const u = ["B", "KB", "MB", "GB"];
let s = bytes,
i = 0;
while (s >= 1024 && i < 3) {
s /= 1024;
i++;
}
return s.toFixed(2) + u[i];
},
// 知识库右侧-列表
getKnowledgeList() {
this.list = [];
postKnowledgeList(this.queryRightParams).then((res) => {
this.list = res.data.list;
this.queryRightParams.total = Number(res.data.total);
this.queryRightParams.is_mine = res.data.is_mine;
console.log("知识库右侧-列表", this.queryRightParams);
});
},
// 知识库右侧-预览
handlePlay(row) {
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 };
this.fileNameDialogVisible = true;
},
submitFileNameForm() {
this.$refs.fileNameFormRef.validate((valid) => {
if (valid) {
postKnowledgeEdit(this.fileNameForm).then(() => {
this.$modal.msgSuccess("修改成功");
this.fileNameDialogVisible = false;
this.getKnowledgeList();
});
}
});
},
cancelFileNameForm() {
this.fileNameDialogVisible = false;
},
// 知识库右侧-删除
handleRightDelete(row) {
this.$modal
.confirm("确认要删除此资源?")
.then(function () {
return postKnowledgeDelete({ knowledge_id: row.id });
})
.then(() => {
this.getKnowledgeList();
this.$modal.msgSuccess("删除成功");
})
.catch(() => {});
},
handleShare(row) {
this.shareItem = row;
this.shareDialogVisible = true;
},
handleShareToContacts(selectedMembers) {
if (selectedMembers && selectedMembers.length > 0) {
const { id, netConfig } = this.$store.state.user;
const currentUserId = id;
const minioEndpoint =
netConfig?.MINIO_ENDPOINT_HTTPS?.trim() || "http://47.92.6.51:9100/";
selectedMembers.forEach((shareTarget) => {
const payloadContent = {
file_name: this.shareItem?.title || "",
file_path: this.shareItem?.file_path || "",
file_size: this.shareItem?.file_size || "",
file_time: this.shareItem?.create_time || "",
knowledge_id: this.shareItem?.id || 0,
thumbnail_path: this.shareItem?.thumbnail_path || "",
};
const messageData = {
client_id: `utalk-client-${currentUserId}`,
message: {
at_users: [],
message_id: 0,
payload: {
content: JSON.stringify(payloadContent),
file_duration: 0,
file_ico: "",
file_name: this.shareItem?.title || "",
file_path: this.shareItem?.file_path || "",
file_size: this.shareItem?.file_size || "",
file_type: this.shareItem?.file_type || "",
},
scene: 1,
source_id: currentUserId,
target_id: shareTarget.id,
timestamp: 0,
type: MessageType.KNOWLEDGE_SHAREs,
},
target_id: shareTarget.id,
topic: "/user/",
};
postMessagePushToUser(messageData).then((res) => {
this.$modal.msgSuccess(`已分享给 ${shareTarget.name}`);
});
});
this.shareDialogVisible = false;
this.shareItem = null;
}
},
// ====================== 上传核心逻辑 ======================
// 打开类型选择
openUploadTypeDialog() {
this.uploadTypeDialogVisible = true;
},
// 选择文件类型
selectFileType(type) {
this.uploadTypeDialogVisible = false;
this.currentFileType = type;
this.uploadAccept = type === "video" ? "video/mp4" : "application/pdf";
// 触发文件选择
this.$nextTick(() => {
this.$refs.fileInput.click();
});
},
// 文件选择处理
handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
this.currentUploadFile = file;
this.uploadProgressDialogVisible = true;
this.uploadPercent = 0;
// 执行 MinIO 上传
this.uploadToMinio(file);
// 清空文件输入
event.target.value = "";
},
// 上传到 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.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");
}
},
// 保存知识库记录到业务数据库
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("知识库记录保存成功");
},
},
};
</script>
<style lang="scss" scoped>
// 自定义项
.custom-item {
display: flex;
justify-content: space-between;
align-items: center;
.left {
display: flex;
align-items: center;
gap: 8px;
}
}
// 资源缩略图
.resource-thumb {
width: 230px;
height: 80px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.hidden-upload-input {
display: none;
}
.upload-type-box {
display: flex;
justify-content: space-around;
padding: 20px 0;
.type-item {
width: 120px;
height: 120px;
border: 1px dashed #ccc;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
i {
font-size: 40px;
color: #409eff;
margin-bottom: 10px;
}
&:hover {
border-color: #409eff;
background: #f5f7fa;
}
}
}
.progress-box {
padding: 20px;
text-align: center;
}
</style>