知识库-页面搭建

main
ysn 2 weeks ago
parent 6659f74e64
commit cbd249fec4
  1. 78
      src/router/index.js
  2. 720
      src/views/knowledge/index.vue

@ -87,32 +87,32 @@ export const constantRoutes = [
} }
] ]
}, },
{ // {
path: '/message', // path: '/message',
component: Layout, // component: Layout,
redirect: '/message/index', // redirect: '/message/index',
children: [ // children: [
{ // {
path: 'index', // path: 'index',
component: () => import('@/views/message/index'), // component: () => import('@/views/message/index'),
name: 'Message', // name: 'Message',
meta: { title: '消息', icon: 'message' } // meta: { title: '消息', icon: 'message' }
} // }
] // ]
}, // },
{ // {
path: '/contacts', // path: '/contacts',
component: Layout, // component: Layout,
redirect: '/contacts/index', // redirect: '/contacts/index',
children: [ // children: [
{ // {
path: 'index', // path: 'index',
component: () => import('@/views/contacts/index'), // component: () => import('@/views/contacts/index'),
name: 'Contacts', // name: 'Contacts',
meta: { title: '通讯录', icon: 'peoples' } // meta: { title: '通讯录', icon: 'peoples' }
} // }
] // ]
}, // },
{ {
path: '/knowledge', path: '/knowledge',
component: Layout, component: Layout,
@ -126,19 +126,19 @@ export const constantRoutes = [
} }
] ]
}, },
{ // {
path: '/cases', // path: '/cases',
component: Layout, // component: Layout,
redirect: '/cases/index', // redirect: '/cases/index',
children: [ // children: [
{ // {
path: 'index', // path: 'index',
component: () => import('@/views/cases/index'), // component: () => import('@/views/cases/index'),
name: 'Cases', // name: 'Cases',
meta: { title: '病例库', icon: 'example' } // meta: { title: '病例库', icon: 'example' }
} // }
] // ]
}, // },
{ {
path: '/utility', path: '/utility',
component: Layout, component: Layout,

@ -0,0 +1,720 @@
<template>
<div class="app-container">
<el-row>
<!-- 左侧菜单栏 -->
<el-col :span="6">
<el-card>
<div slot="header" class="card-header">
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索"
prefix-icon="el-icon-search"
style="margin-right: 8px"
@input="handleSearch"
>
<el-button
slot="append"
icon="el-icon-refresh-right"
@click="handleRefresh"
/>
</el-input>
</div>
<el-menu
class="content-list"
:default-active="activeMenu"
@select="handleMenuClick"
>
<el-menu-item index="1">
<i class="el-icon-menu"></i>
<span slot="title">海信产品介绍</span>
</el-menu-item>
<el-menu-item index="2">
<i class="el-icon-video-camera"></i>
<span slot="title">公共教学视频</span>
</el-menu-item>
<el-menu-item index="3">
<i class="el-icon-user"></i>
<span slot="title">个人收藏录像</span>
</el-menu-item>
<div v-for="item in customMenuList" :key="item.index">
<el-menu-item :index="item.index" class="custom-item">
<div class="item-content">
<div class="left">
<i class="el-icon-collection-tag" style="color: #20c0c0" />
<span style="color: #20c0c0">{{ item.title }}</span>
</div>
<div class="right">
<el-button
type="text"
icon="el-icon-edit"
@click.stop="openEditDialog(item)"
/>
<el-button
type="text"
icon="el-icon-delete"
@click.stop="deleteCustomTag(item)"
/>
</div>
</div>
</el-menu-item>
</div>
<el-menu-item index="custom" @click="openAddDialog">
<i class="el-icon-edit-outline"></i>
<span slot="title">自定义标签</span>
</el-menu-item>
</el-menu>
</el-card>
</el-col>
<!-- 右侧内容区 -->
<el-col :span="18">
<el-card>
<div slot="header" class="card-header">
<span>{{ currentMenuTitle }}</span>
<el-button
v-if="currentMenuTitle === '个人收藏录像' || isCustomMenu"
type="text"
@click="openUploadDialog"
>
上传
</el-button>
</div>
<div class="content-list" v-if="filteredFileList.length > 0">
<div
class="list-item"
v-for="file in filteredFileList"
:key="file.id"
@click="previewFile(file)"
>
<div class="item-cover">
<img :src="file.cover" alt="" />
<div class="play-btn" v-if="file.type === 'video'">
<i class="el-icon-video-play"></i>
</div>
<div class="play-btn" v-if="file.type === 'pdf'">
<i class="el-icon-document"></i>
</div>
</div>
<div class="item-info">
<div class="item-name">{{ file.name }}</div>
</div>
<div class="item-upload-time">{{ file.uploadTime }} 上传</div>
<div class="item-size">{{ file.size }}</div>
<div class="actions">
<el-button
type="text"
icon="el-icon-share"
@click.stop="handleShare(file)"
/>
<el-button
v-if="isCustomMenu || currentMenuTitle === '个人收藏录像'"
type="text"
icon="el-icon-edit"
@click.stop="openEditFileDialog(file)"
/>
<el-button
v-if="isCustomMenu || currentMenuTitle === '个人收藏录像'"
type="text"
icon="el-icon-delete"
@click.stop="deleteFile(file)"
/>
</div>
</div>
</div>
<div class="empty-state" v-else>
<i class="el-icon-file-empty"></i>
<span>暂无相关文件数据</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 预览弹窗 -->
<el-dialog
:title="previewFileData.name"
:visible.sync="previewVisible"
fullscreen
append-to-body
>
<video
v-if="previewFileData.type === 'video'"
:src="previewFileData.url"
controls
autoplay
playsinline
style="
width: 100%;
height: calc(100vh - 75px);
object-fit: contain;
background: #000;
"
/>
<iframe
v-if="previewFileData.type === 'pdf'"
:src="previewFileData.url"
style="width: 100%; height: calc(100vh - 75px); border: none"
/>
</el-dialog>
<!-- 上传弹窗按原型图修改 -->
<el-dialog
title="请选择上传文件类型"
:visible.sync="uploadDialogVisible"
width="17%"
:close-on-click-modal="false"
@closed="resetUploadDialog"
>
<div class="upload-type-container">
<!-- 视频上传 -->
<div class="type-card" @click="selectUploadType('video')">
<el-upload
ref="videoUpload"
:auto-upload="false"
accept=".mp4"
:on-change="handleFileChange"
:file-list="videoFileList"
class="uploader"
>
<div class="type-icon">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none">
<path
d="M14 2H6C4.89 2 4 2.89 4 4V20C4 21.11 4.89 22 6 22H18C19.11 22 20 21.11 20 20V8L14 2Z"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 2V8H20"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M10 15L15 12L10 9V15Z" fill="white" />
</svg>
</div>
<div class="type-text">请选择视频文件(仅支持MP4格式)</div>
</el-upload>
</div>
<!-- PDF上传 -->
<div class="type-card" @click="selectUploadType('pdf')">
<el-upload
ref="pdfUpload"
:auto-upload="false"
accept=".pdf"
:on-change="handleFileChange"
:file-list="pdfFileList"
class="uploader"
>
<div class="type-icon">
<svg width="80" height="80" viewBox="0 0 24 24" fill="none">
<path
d="M14 2H6C4.89 2 4 2.89 4 4V20C4 21.11 4.89 22 6 22H18C19.11 22 20 21.11 20 20V8L14 2Z"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 2V8H20"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<rect x="6" y="14" width="12" height="4" rx="1" fill="white" />
<text
x="12"
y="17"
text-anchor="middle"
fill="#20c0c0"
font-size="5"
font-weight="bold"
>
PDF
</text>
</svg>
</div>
<div class="type-text">请选择PDF文件(仅支持PDF格式)</div>
</el-upload>
</div>
</div>
</el-dialog>
<!-- 新增/编辑标签 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="17%"
@close="resetDialog"
>
<el-form ref="tagForm" :model="tagForm" :rules="rules" label-width="80px">
<el-form-item label="标签名称" prop="name">
<el-input
v-model="tagForm.name"
placeholder="请输入标签名称"
maxlength="8"
show-word-limit
/>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确认</el-button>
</span>
</el-dialog>
<!-- 编辑文件名 -->
<el-dialog
title="编辑文件名称"
:visible.sync="editFileDialogVisible"
width="300px"
>
<el-form
ref="fileForm"
:model="fileForm"
:rules="fileRules"
label-width="80px"
>
<el-form-item label="文件名称" prop="name">
<el-input
v-model="fileForm.name"
placeholder="请输入文件名称"
maxlength="8"
show-word-limit
/>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="editFileDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitFileEdit">确认</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: "KnowledgeBase",
data() {
return {
searchKeyword: "",
activeMenu: "1",
currentMenuTitle: "海信产品介绍",
fixedMenuList: [
{ index: "1", title: "海信产品介绍" },
{ index: "2", title: "公共教学视频" },
{ index: "3", title: "个人收藏录像" },
],
customMenuList: [{ index: "4", title: "我的收藏" }],
menuFiles: {
1: [
{
id: 1,
name: "海信超声图像调节",
cover: "https://via.placeholder.com/160x90/42b983/ffffff?text=视频",
uploadTime: "2026-05-11 09:22:15",
size: "269.75M",
type: "video",
url: "https://www.w3schools.com/html/mov_bbb.mp4",
},
{
id: 4,
name: "信联用户手册V1.2_20220428.pdf",
cover: "https://via.placeholder.com/160x90/9b59b6/ffffff?text=PDF",
uploadTime: "2026-05-12 16:44:02",
size: "3.82M",
type: "pdf",
url: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf",
},
],
2: [
{
id: 7,
name: "超声在PsA多学科诊疗中的价值",
cover: "https://via.placeholder.com/160x90/34495e/ffffff?text=视频",
uploadTime: "2026-05-09 14:33:16",
size: "634.11M",
type: "video",
url: "https://www.w3schools.com/html/mov_bbb.mp4",
},
],
3: [],
4: [
{
id: 17,
name: "1633500241136.mp4",
cover: "https://via.placeholder.com/160x90/000000/ffffff?text=视频",
uploadTime: "2026-05-13 14:32:04",
size: "17.92M",
type: "video",
url: "https://www.w3schools.com/html/mov_bbb.mp4",
},
],
},
dialogVisible: false,
dialogTitle: "",
currentEditItem: null,
tagForm: { name: "" },
rules: {
name: [
{ required: true, message: "请输入标签名称", trigger: "blur" },
{ min: 1, max: 8, message: "1-8个字符", trigger: "blur" },
],
},
uploadDialogVisible: false,
selectedUploadType: "",
videoFileList: [],
pdfFileList: [],
editFileDialogVisible: false,
currentEditFile: null,
fileForm: { name: "" },
fileRules: {
name: [
{ required: true, message: "请输入文件名称", trigger: "blur" },
{ min: 1, max: 8, message: "1-8个字符", trigger: "blur" },
],
},
previewVisible: false,
previewFileData: {},
};
},
computed: {
isCustomMenu() {
return this.customMenuList.some((item) => item.index === this.activeMenu);
},
currentFileList() {
return this.menuFiles[this.activeMenu] || [];
},
filteredFileList() {
if (!this.searchKeyword.trim()) return this.currentFileList;
const k = this.searchKeyword.toLowerCase();
return this.currentFileList.filter((f) =>
f.name.toLowerCase().includes(k)
);
},
},
created() {
this.loadLocalData();
},
methods: {
loadLocalData() {
const t = localStorage.getItem("customMenuList");
const f = localStorage.getItem("menuFiles");
if (t) this.customMenuList = JSON.parse(t);
if (f) this.menuFiles = JSON.parse(f);
},
saveLocalData() {
localStorage.setItem(
"customMenuList",
JSON.stringify(this.customMenuList)
);
localStorage.setItem("menuFiles", JSON.stringify(this.menuFiles));
},
handleRefresh() {
this.searchKeyword = "";
this.$modal.msgSuccess("刷新成功");
},
handleSearch() {},
handleMenuClick(index) {
if (index === "custom") return;
this.activeMenu = index;
const f = this.fixedMenuList.find((x) => x.index === index);
const c = this.customMenuList.find((x) => x.index === index);
this.currentMenuTitle = f?.title || c?.title || "";
},
openAddDialog() {
this.dialogTitle = "新增自定义标签";
this.tagForm.name = "";
this.currentEditItem = null;
this.dialogVisible = true;
},
openEditDialog(item) {
this.dialogTitle = "编辑自定义标签";
this.tagForm.name = item.title;
this.currentEditItem = item;
this.dialogVisible = true;
},
submitForm() {
this.$refs.tagForm.validate((v) => {
if (!v) return;
const n = this.tagForm.name.trim();
if (this.currentEditItem) {
this.currentEditItem.title = n;
this.$modal.msgSuccess("编辑成功");
} else {
const all = [
...this.fixedMenuList.map((x) => +x.index),
...this.customMenuList.map((x) => +x.index),
];
const idx = String(Math.max(...all) + 1);
this.customMenuList.push({ index: idx, title: n });
this.menuFiles[idx] = [];
this.$modal.msgSuccess("新增成功");
}
this.dialogVisible = false;
this.saveLocalData();
});
},
resetDialog() {
this.$refs.tagForm?.clearValidate();
this.tagForm.name = "";
},
// ====================== 使$modal.confirm======================
deleteCustomTag(item) {
this.$modal
.confirm(
"是否删除该自定义标签?若删除,该标签下资源将迁移至[个人收藏视频]标签下。"
)
.then(() => {
const files = this.menuFiles[item.index] || [];
this.menuFiles["3"] = [...(this.menuFiles["3"] || []), ...files];
const i = this.customMenuList.findIndex(
(x) => x.index === item.index
);
if (i > -1) this.customMenuList.splice(i, 1);
delete this.menuFiles[item.index];
this.handleMenuClick("3");
this.saveLocalData();
this.$modal.msgSuccess("删除成功");
})
.catch(() => {});
},
// ====================== 使$modal.confirm======================
deleteFile(file) {
this.$modal
.confirm(`是否要删除【${file.name}】此资源?删除后不可恢复!`)
.then(() => {
const list = this.menuFiles[this.activeMenu];
const i = list.findIndex((x) => x.id === file.id);
if (i > -1) list.splice(i, 1);
this.saveLocalData();
this.$modal.msgSuccess("删除成功");
})
.catch(() => {});
},
openEditFileDialog(file) {
this.currentEditFile = file;
this.fileForm.name = file.name;
this.editFileDialogVisible = true;
},
submitFileEdit() {
this.$refs.fileForm.validate((v) => {
if (!v) return;
this.currentEditFile.name = this.fileForm.name.trim();
this.editFileDialogVisible = false;
this.$modal.msgSuccess("修改成功");
this.saveLocalData();
});
},
openUploadDialog() {
this.uploadDialogVisible = true;
this.selectedUploadType = "";
},
selectUploadType(type) {
this.selectedUploadType = type;
},
handleFileChange(file, list) {
if (file.raw.type.includes("video")) {
this.videoFileList = list;
} else if (file.raw.type.includes("pdf")) {
this.pdfFileList = list;
}
},
resetUploadDialog() {
this.videoFileList = [];
this.pdfFileList = [];
this.selectedUploadType = "";
this.$refs.videoUpload?.clearFiles();
this.$refs.pdfUpload?.clearFiles();
},
previewFile(file) {
this.previewFileData = file;
this.previewVisible = true;
},
handleShare(file) {
navigator.clipboard
.writeText(file.url)
.then(() => {
this.$modal.msgSuccess("分享链接已复制");
})
.catch(() => {
this.$modal.msgInfo(file.url);
});
},
},
};
</script>
<style lang="scss" scoped>
$primary-color: #20c0c0;
$gray-color: #999;
$light-gray: #eee;
.custom-item {
.item-content {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
.left {
display: flex;
align-items: center;
gap: 8px;
}
.right {
display: flex;
gap: 8px;
}
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
height: 48px;
}
.content-list {
height: calc(100vh - 255px);
max-height: calc(100vh - 255px);
overflow-y: auto;
padding: 10px 0;
.list-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid $light-gray;
cursor: pointer;
transition: background 0.2s;
&:hover {
background: rgba($primary-color, 0.05);
}
.item-cover {
position: relative;
width: 160px;
height: 90px;
border-radius: 4px;
overflow: hidden;
margin-right: 16px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
}
}
.item-info {
flex: 1;
min-width: 0;
.item-name {
font-size: 14px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.item-upload-time,
.item-size {
font-size: 13px;
color: $gray-color;
width: 180px;
text-align: center;
flex-shrink: 0;
}
.item-size {
width: 100px;
text-align: right;
}
.actions {
display: flex;
gap: 8px;
margin-left: 16px;
flex-shrink: 0;
::v-deep .el-button {
color: $gray-color;
&:hover {
color: $primary-color;
}
}
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: $gray-color;
height: 200px;
i {
font-size: 48px;
margin-bottom: 12px;
}
}
/* 上传弹窗样式,完全匹配原型 */
.upload-type-container {
display: flex;
justify-content: space-around;
align-items: center;
padding: 20px 0;
.type-card {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
.type-icon {
width: 90px;
height: 100px;
background-color: $primary-color;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
}
.type-text {
font-size: 14px;
color: #333;
margin-bottom: 10px;
}
}
}
.dialog-footer {
text-align: center;
}
</style>
Loading…
Cancel
Save