界面优化,markdown渲染优化

This commit is contained in:
sswiki
2024-12-09 22:14:51 +08:00
parent 4ad463845e
commit d6bcfc5492
68 changed files with 1885 additions and 1203 deletions

View File

@@ -19,10 +19,10 @@
<script setup>
import {toRefs, ref, reactive, onMounted, onBeforeUnmount, defineProps, watch, defineEmits, computed, defineExpose} from 'vue';
import {useStoreDisplay} from '@/store/wikiDisplay.js'
import LeftAside from './aside/LeftAside.vue'
import RightHeader from './aside/RightHeader.vue'
import RightResize from './aside/RightResize.vue'
import {useStoreDisplay} from '@/store/wikiDisplay.js';
import LeftAside from './aside/LeftAside.vue';
import RightHeader from './aside/RightHeader.vue';
import RightResize from './aside/RightResize.vue';
import userApi from "@/assets/api/user";
import {useStoreUserData} from "@/store/userData";
@@ -50,82 +50,21 @@ const getSelfUserInfo = () => {
border-right: 1px solid #eee;
background: #fafafa;
}
</style>
<style>
html,
body {
html, body {
margin: 0;
padding: 0;
height: 100%;
}
.global-layout-vue {
height: 100%;
}
.hidTree {
display: none;
}
#app, .el-container, .el-menu {
#app, .el-container, .el-menu, .global-layout-vue {
height: 100%;
}
.el-header {
background-color: #fff !important;
}
.header-right-user-name {
color: #000000;
vertical-align: middle;
}
.el-header {
color: #333;
height: 60px !important;
background-color: #fff !important;
border-bottom: 1px solid #eee;
}
.head-icon {
margin-right: 15px;
margin-top: 15px;
font-size: 16px;
cursor: pointer;
color: #000000;
vertical-align: middle;
}
.header-user-message .page-info-box {
text-align: right;
margin-top: 10px;
}
.upgrade-info {
max-height: 150px;
overflow-y: auto;
word-break: break-all;
white-space: pre-wrap;
line-height: 26px;
}
.search-option-item {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-option-item .title {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search-option-item .content {
font-size: 12px;
color: #888;
}
</style>

View File

@@ -1,12 +1,12 @@
<template>
<div class="page-show-vue" v-if="storePage.pageInfo.editorType !== 0">
<a-row class="view-body-comment-box">
<a-col flex="auto" class="view-body-outer-box">
<a-col id="pageContentScrollBox" flex="auto" class="view-body-outer-box">
<div class="view-body-box">
<div class="wiki-title" ref="wikiTitleRef">{{ storePage.pageInfo.name }}</div>
<div id="pageContentBox" ref="pageContentRef" class="wiki-page-content">
<div v-if="wikiPage.editorType === 2" v-html="pageShowDetail" class="markdown-body" v-highlight></div>
<div v-else v-html="pageShowDetail" class="wang-editor-body"></div>
<div v-if="wikiPage.editorType === 2" v-html="pageContentShow" class="page-view-content markdown-body" v-highlight></div>
<div v-else v-html="pageContentShow" class="page-view-content wang-editor-body"></div>
</div>
<PageZan></PageZan>
</div>
@@ -18,16 +18,10 @@
<Comment></Comment>
</a-tab-pane>
<a-tab-pane tab="附件" key="files">
<Annex/>
<Files/>
</a-tab-pane>
<a-tab-pane tab="修改历史" key="history">
<PageHistory
:pageHistoryList="pageHistoryList"
:pageHistoryChoice="pageHistoryChoice"
:pageHistoryDetail="pageHistoryDetail"
@historyClickHandle="historyClickHandle"
@previewPageImage="previewPageImage"
@createNavigationHeading="createNavigationHeading"/>
<PageHistory :history="pageHistoryList" @choice="historyClickHandle"/>
</a-tab-pane>
<template #rightExtra>
<el-tooltip content="关闭" placement="top">
@@ -46,19 +40,18 @@ import { CloseOutlined } from '@ant-design/icons-vue';
import {toRefs, ref, reactive, onMounted, watch, defineProps, h, nextTick, defineEmits, defineExpose, computed} from 'vue';
import {onBeforeRouteUpdate, useRoute, useRouter} from "vue-router";
import { ElMessageBox, ElMessage, ElNotification } from 'element-plus';
import QRCode from 'qrcode'
import unitUtil from '../../assets/lib/UnitUtil.js'
import htmlUtil from '../../assets/lib/HtmlUtil.js'
import pageApi from '../../assets/api/page'
import userApi from '../../assets/api/user'
import Navigation from './show/Navigation.vue'
import Annex from './show/Annex.vue'
import PageHistory from './show/PageHistory.vue'
import Comment from './show/Comment.vue'
import PageZan from './show/PageZan.vue'
import {mavonEditor} from 'mavon-editor'
import 'mavon-editor/dist/markdown/github-markdown.min.css'
import 'mavon-editor/dist/css/index.css'
import QRCode from 'qrcode';
import unitUtil from '../../assets/lib/UnitUtil.js';
import htmlUtil from '../../assets/lib/HtmlUtil.js';
import pageApi from '../../assets/api/page';
import userApi from '../../assets/api/user';
import Navigation from './show/Navigation.vue';
import Files from './show/Files.vue';
import PageHistory from './show/PageHistory.vue';
import Comment from './show/Comment.vue';
import PageZan from './show/PageZan.vue';
import {mavonEditor} from 'mavon-editor';
import 'mavon-editor/dist/css/index.css';
import {useStorePageData} from "@/store/pageData";
import {useStoreDisplay} from "@/store/wikiDisplay";
import ImageViewer from "@/components/base/ImageViewer.vue";
@@ -81,15 +74,9 @@ onBeforeRouteUpdate((to) => {
// 页面展示相关
let wikiPage = ref({});
let wikiPageAuth = ref({});
let pageContent = ref({});
let selfUserId = ref(0);
// 右侧标签页
let pageHistoryDetail = ref('');
let pageShowDetail = ref('');
let pageHistoryChoice = ref({});
let pageContent = ref('');
let pageContentShow = ref('');
let pageHistoryList = ref([]);
let pageHistoryPageNum = ref(1);
// 左侧导航菜单
let navigationList = ref([]);
let actionTabActiveName = ref('comment');
let imageViewerRef = ref();
@@ -102,45 +89,39 @@ const closeActionTab = () => {
storeDisplay.commentShow = false;
clearHistory();
}
const getPageHistory = (pageId, pageNum) => {
if (pageNum === 1) {
pageHistoryList.value = [];
pageHistoryPageNum.value = 1;
}
let param = {pageId: pageId, pageNum: pageNum};
pageApi.pageHistoryList(param).then((json) => {
const getPageHistory = (pageId) => {
pageHistoryList.value = [];
pageApi.pageHistoryList({pageId: pageId}).then((json) => {
let historyList = json.data || [];
if (historyList.length <= 0) {
pageHistoryPageNum.value = 0;
} else {
historyList.forEach((item) => (item.loading = 0));
pageHistoryList.value = pageHistoryList.value.concat(historyList);
}
})
historyList.forEach((item) => (item.loading = 0));
pageHistoryList.value = historyList;
});
}
const historyClickHandle = (history) => {
pageHistoryChoice.value.loading = 0;
pageHistoryChoice.value = history;
pageHistoryDetail.value = history.content;
pageShowDetail.value =history.content;
pageContentShow.value = history.content;
afterLoadPage();
}
const clearHistory = () => {
pageHistoryChoice.value.loading = 0;
pageHistoryDetail.value = '';
pageHistoryChoice.value = {};
pageHistoryList.value.forEach((item) => (item.loading = 0));
pageShowDetail.value = pageContent.value.content;
pageContentShow.value = pageContent.value;
afterLoadPage();
}
const afterLoadPage = () => {
setTimeout(() => {
previewPageImage();
createNavigationHeading();
}, 500);
}
const loadPageDetail = (pageId) => {
clearHistory();
pageContent.value = '';
pageApi.pageDetail({id: pageId}).then(async (json) => {
let result = json.data || {};
let wikiPageRes = result.wikiPage || {};
wikiPageRes.selfZan = result.selfZan || 0;
wikiPageRes.zanNum = wikiPageRes.zanNum || 0;
wikiPage.value = wikiPageRes;
pageContent.value = result.pageContent || {};
storePage.fileList = result.fileList || [];
selfUserId.value = result.selfUserId || 0;
wikiPageAuth.value = {
canEdit: result.canEdit,
canDelete: result.canDelete,
@@ -148,10 +129,13 @@ const loadPageDetail = (pageId) => {
canDeleteFile: result.canDeleteFile,
canConfigAuth: result.canConfigAuth,
}
if (wikiPage.value.editorType === 2) {
pageContent.value.content = mavonEditor.getMarkdownIt().render(pageContent.value.content);
if (result.pageContent) {
pageContent.value = result.pageContent.content || '';
}
pageShowDetail.value = pageContent.value.content;
if (wikiPage.value.editorType === 2) {
pageContent.value = mavonEditor.getMarkdownIt().render(pageContent.value);
}
pageContentShow.value = pageContent.value;
// 修改标题
document.title = wikiPageRes.name || 'WIKI-内容展示';
// 修改最后点击的项,保证刷新后点击编辑能展示编辑的项
@@ -162,18 +146,15 @@ const loadPageDetail = (pageId) => {
emit('switchSpace', wikiPage.value.spaceId);
// 调用父方法展开目录树
emit('changeExpandedKeys', pageId);
setTimeout(() => {
previewPageImage();
createNavigationHeading();
}, 500);
storePage.pageInfo = wikiPageRes;
storePage.pageAuth = wikiPageAuth.value;
})
getPageHistory(pageId, 1)
afterLoadPage();
});
getPageHistory(pageId);
}
let wikiTitleRef = ref();
const createNavigationHeading = () => {
let navigationListVal = htmlUtil.createNavigationHeading()
let navigationListVal = htmlUtil.createNavigationHeading();
// 标题加到导航里面去
if (navigationListVal.length > 0) {
let wikiTile = wikiPage.value.name || 'WIKI-内容展示'
@@ -181,7 +162,7 @@ const createNavigationHeading = () => {
level: 1,
node: wikiTitleRef.value,
text: wikiTile,
})
});
}
navigationList.value = navigationListVal;
}
@@ -204,6 +185,7 @@ const initQueryParam = (to) => {
.view-body-comment-box {
height: 100%;
flex-flow: row nowrap;
.view-body-outer-box {
height: 100%;
@@ -230,8 +212,6 @@ const initQueryParam = (to) => {
</style>
<style>
@import '../../assets/lib/wangEditor.css';
.page-show-vue .icon-collapse {
float: left;
font-size: 25px;

View File

@@ -29,13 +29,13 @@
<script setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import {FolderOpen, Word as IconParkWord, Afferent as IconParkAfferent, PageTemplate as IconParkPageTemplate,} from '@icon-park/vue-next'
import {FolderOpen, Word as IconParkWord, Afferent as IconParkAfferent, PageTemplate as IconParkPageTemplate,} from '@icon-park/vue-next';
import {ref, defineProps, defineEmits, h} from 'vue';
import {useRouter} from "vue-router";
import {ElMessage} from 'element-plus'
import pageApi from '@/assets/api/page'
import {ElMessage} from 'element-plus';
import pageApi from '@/assets/api/page';
import axios from "axios";
import IconDocument from '@/components/base/IconDocument.vue'
import IconDocument from '@/components/base/IconDocument.vue';
import {useStorePageData} from "@/store/pageData";
import {useStoreSpaceData} from "@/store/spaceData";
import MessagePrompt from "@/components/single/MessagePrompt";

View File

@@ -62,7 +62,7 @@
<el-tooltip :content="node.label" placement="top-start" :show-after="700">{{ node.label }}</el-tooltip>
</span>
<!--操作-->
<div class="page-action-box">
<div @click.stop class="page-action-box">
<AddMenu :pageId="data.id"/>
<a-dropdown :trigger="['click']" @click="choosePageIdFunc(data.id)">
<a-button :icon="h(EllipsisOutlined)" type="text" style="color: #888;"></a-button>
@@ -115,12 +115,12 @@ import {
import { EllipsisOutlined } from '@ant-design/icons-vue';
import {ref, defineProps, defineEmits, defineExpose, onMounted, h, watch} from 'vue';
import {useRouter, useRoute} from "vue-router";
import pageApi from '@/assets/api/page'
import pageApi from '@/assets/api/page';
import {useStoreDisplay} from "@/store/wikiDisplay";
import {useStorePageData} from "@/store/pageData";
import AddMenu from "./AddMenu.vue";
import IconDocument from "@/components/base/IconDocument.vue";
import {ElMessageBox, ElMessage} from 'element-plus'
import {ElMessageBox, ElMessage} from 'element-plus';
import {useStoreSpaceData} from "@/store/spaceData";
import Navigation from "@/views/page/show/Navigation.vue";
import PageZan from "@/views/page/show/PageZan.vue";
@@ -181,7 +181,7 @@ const loadSpaceList = (spaceId) => {
console.log(e);
}
}
})
});
}
let optionPageId = ref('');
const changeNodeOptionStatus = (param) => {
@@ -200,7 +200,7 @@ const changeWikiPageExpandedKeys = (pageId) => {
}
const createWikiByTemplate = () => {
// TODO
// templateManageRef.value.showTemplateManage()
// templateManageRef.value.showTemplateManage();
}
const choosePageIdFunc = (id) => {
storePage.optionPageId = id;
@@ -231,15 +231,15 @@ const renamePage = (node, data) => {
}
const openMoveMenu = (onlyMove) => {
// TODO
// onlyMoveMode.value = onlyMove
// visibleMoveMenu.value = true
// onlyMoveMode.value = onlyMove;
// visibleMoveMenu.value = true;
// moveToPageId.value = storePage.choosePageId
// moveToSpaceId.value = storeSpace.chooseSpaceId
// moveToWikiPageList.value = storePage.wikiPageList
}
const openTemplateCreate = (exsit) => {
// TODO
// templateManageRef.value.showTemplateCreate(exsit)
// templateManageRef.value.showTemplateCreate(exsit);
}
const deleteWikiPage = (data) => {
let msg = '确定要删除此页面及其所有子页面吗?'
@@ -299,7 +299,7 @@ const searchByKeywords = () => {
wikiPageTreeRef.value.filter(searchKeywords.value);
}
const handleNodeClick = (data) => {
//console.log('点击节点:', data, props.nowPageId)
//console.log('点击节点:', data, props.nowPageId);
if (data.editorType !== 0) {
router.push({path: `/view/${data.spaceId}/${data.id}`});
}
@@ -320,7 +320,7 @@ const handlePageDrop = (draggingNode, dropNode, dropType, ev) => {
doGetPageList();
});
}
defineExpose({searchByKeywords})
defineExpose({searchByKeywords});
</script>
<style lang="scss">

View File

@@ -27,10 +27,8 @@
<a-button class="hover-button hover-bg" @click="showCommentWiki" size="large" :icon="h(MessageOutlined)"></a-button>
</a-tooltip>
<UserMessagePopover/>
<a-dropdown trigger="click" placement="bottom" overlayClassName="header-action-more-dropdown">
<span style="display:inline-block;margin: 0 12px;">
<a-button class="hover-button hover-bg" size="large" :icon="h(EllipsisOutlined)"></a-button>
</span>
<a-dropdown trigger="click" placement="bottom" arrow overlayClassName="header-action-more-dropdown">
<a-button class="hover-button hover-bg" size="large" :icon="h(EllipsisOutlined)"></a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="editWikiAuth" v-if="storePage.pageAuth.canConfigAuth === 1"><el-icon><ElIconSCheck/></el-icon> 权限设置</a-menu-item>
@@ -43,10 +41,8 @@
</template>
</a-dropdown>
</template>
<a-dropdown trigger="click" placement="bottom" overlayClassName="header-action-user-dropdown">
<span style="display:inline-block;">
<a-button class="hover-button hover-bg" size="large" :icon="h(UserOutlined)"></a-button>
</span>
<a-dropdown trigger="click" placement="bottomRight" arrow overlayClassName="header-action-user-dropdown">
<a-button class="hover-button hover-bg" size="large" :icon="h(UserOutlined)"></a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="showAbout">关于</a-menu-item>
@@ -86,17 +82,17 @@ import {
} from '@ant-design/icons-vue';
import {toRefs, ref, reactive, onMounted, watch, defineEmits, h, computed} from 'vue';
import {useRouter, useRoute} from "vue-router";
import { ElMessageBox, ElMessage } from 'element-plus'
import { useStoreDisplay } from '@/store/wikiDisplay.js'
import { useStorePageData } from '@/store/pageData.js'
import { useStoreUserData } from '@/store/userData.js'
import { ElMessageBox, ElMessage } from 'element-plus';
import { useStoreDisplay } from '@/store/wikiDisplay.js';
import { useStorePageData } from '@/store/pageData.js';
import { useStoreUserData } from '@/store/userData.js';
import pageApi from "@/assets/api/page";
import {useStoreSpaceData} from "@/store/spaceData";
import userApi from "@/assets/api/user";
import PageAuthDialog from '@/views/page/show/PageAuthDialog.vue'
import MobileQrScanDialog from '@/views/page/show/MobileQrScanDialog.vue'
import AboutDialog from "@/views/common/AboutDialog.vue"
import UserMessagePopover from "../../../components/layouts/UserMessagePopover.vue"
import PageAuthDialog from '@/views/page/show/PageAuthDialog.vue';
import MobileQrScanDialog from '@/views/page/show/MobileQrScanDialog.vue';
import AboutDialog from "@/views/common/AboutDialog.vue";
import UserMessagePopover from "../../../components/layouts/UserMessagePopover.vue";
import {fixRequestUrl} from "@/assets/api/hostUtils";
let router = useRouter();
@@ -150,13 +146,13 @@ const deleteWikiPage = () => {
}).then(() => {
pageApi.pageDelete({pageId: storePage.pageInfo.id}).then(() => {
pageApi.pageList({spaceId: storeSpace.chooseSpaceId}).then((json) => {
storePage.wikiPageList = json.data || []
storePage.wikiPageList = json.data || [];
}).then(()=>{
router.push({path: '/home', query: {spaceId: storePage.pageInfo.spaceId}});
})
});
});
}).catch((e) => {
console.log(e)
console.log(e);
});
}
// 下载为Word
@@ -190,7 +186,7 @@ const showAbout = () => {
aboutDialogVisible.value = true;
}
const showConsole = () => {
window.open(import.meta.env.VITE_APP_BASE_API, '_blank')
window.open(import.meta.env.VITE_APP_BASE_API, '_blank');
}
</script>

View File

@@ -1,80 +0,0 @@
<template>
<div class="wiki-file">
<el-upload v-if="storePage.pageAuth.canUploadFile === 1"
:on-success="uploadFileSuccess"
:on-error="uploadFileError"
:before-upload="beforeUpload"
:action="uploadFileUrl"
:data="uploadFormData"
:with-credentials="true" class="action-btn upload-page-file" name="files"
show-file-list multiple :limit="999">
<el-button type="primary" :underline="false" :icon="ElIconUpload" style="margin: 10px;width: 100%"> 上传附件</el-button>
</el-upload>
<el-table v-show="storePage.fileList.length > 0" :data="storePage.fileList" border
style="width: 100%; margin-bottom: 5px">
<el-table-column label="文件名" show-overflow-tooltip>
<template v-slot="scope">
<el-link target="_blank" :href="scope.row.fileUrl" type="primary">{{scope.row.fileName }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="createUserName" label="创建人" width="110px"
show-overflow-tooltip></el-table-column>
<el-table-column label="文件大小" width="120px">
<template v-slot="scope">{{computeFileSize(scope.row.fileSize) }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160px"></el-table-column>
<el-table-column prop="downloadNum" label="下载次数" width="90px">
<template v-slot="scope">{{scope.row.downloadNum || 0}}</template>
</el-table-column>
<el-table-column label="操作" width="90px" v-if="storePage.pageAuth.canDeleteFile == 1">
<template v-slot="scope">
<el-button @click="deletePageFile(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import {Upload as ElIconUpload} from '@element-plus/icons-vue'
import {useStorePageData} from "@/store/pageData";
let storePage = useStorePageData();
import {ref} from 'vue';
import {ElMessageBox, ElMessage} from 'element-plus';
import pageApi from "@/assets/api/page";
import unitUtil from "@/assets/lib/UnitUtil";
let uploadFormData = ref({pageId: 0});
let uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + '/zyplayer-doc-wiki/page/file/upload');
const beforeUpload = () => {
uploadFormData.value.pageId = storePage.pageInfo.id;
}
const uploadFileError = (err) => {
ElMessage.error('上传失败,' + err);
}
const uploadFileSuccess = (response) => {
if (response.errCode === 200) {
storePage.fileList.push(response.data);
ElMessage.success('上传成功!');
} else {
ElMessage('上传失败:' + (response.errMsg || '未知错误'));
}
}
const deletePageFile = (row) => {
ElMessageBox.confirm('确定要删除此文件吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
let param = {id: row.id};
pageApi.deletePageFile(param).then(() => {
storePage.fileList = storePage.fileList.filter(item => item.id !== row.id);
});
})
}
const computeFileSize = (fileSize) => {
return unitUtil.computeFileSize(fileSize)
}
</script>

View File

@@ -45,7 +45,7 @@ import {
import {toRefs, ref, reactive, onMounted, watch, defineProps, defineEmits, defineExpose, computed} from 'vue';
import {onBeforeRouteUpdate, useRoute, useRouter} from "vue-router";
import { ElMessageBox, ElMessage, ElNotification } from 'element-plus';
import pageApi from '@/assets/api/page'
import pageApi from '@/assets/api/page';
import {useStorePageData} from "@/store/pageData";
import {useStoreUserData} from "@/store/userData";
@@ -67,55 +67,55 @@ watch(() => storePage.pageInfo, (newVal) => {
if (storePage.pageInfo.editorType !== 0){
loadCommentList();
}
})
});
onMounted(() => {
loadCommentList();
});
let actionTabCommentRef = ref();
const scrollActionTabComment = () => {
setTimeout(() => {
let actionTabComment = actionTabCommentRef.value
actionTabComment.scrollTop = actionTabComment.scrollHeight
}, 0)
let actionTabComment = actionTabCommentRef.value;
actionTabComment.scrollTop = actionTabComment.scrollHeight;
}, 0);
}
const loadCommentList = () => {
if (!storePage.pageInfo || !storePage.pageInfo.id) {
return;
}
cancelCommentUser()
cancelCommentUser();
pageApi.pageCommentList({pageId: storePage.pageInfo.id}).then((json) => {
let commentListRes = json.data || []
let commentListRes = json.data || [];
for (let i = 0; i < commentListRes.length; i++) {
commentListRes[i].color = getUserHeadBgColor(commentListRes[i].createUserId)
let subCommentList = commentListRes[i].commentList || []
commentListRes[i].color = getUserHeadBgColor(commentListRes[i].createUserId);
let subCommentList = commentListRes[i].commentList || [];
for (let j = 0; j < subCommentList.length; j++) {
let subItem = subCommentList[j]
subItem.color = getUserHeadBgColor(subItem.createUserId)
let subItem = subCommentList[j];
subItem.color = getUserHeadBgColor(subItem.createUserId);
}
commentListRes[i].commentList = subCommentList
commentListRes[i].visible = false
commentListRes[i].commentList = subCommentList;
commentListRes[i].visible = false;
}
commentList.value = commentListRes
scrollActionTabComment()
})
commentList.value = commentListRes;
scrollActionTabComment();
});
}
let canDeleteComment = (row) => {
return storeUser.userInfo.id === row.createUserId
|| storeUser.userInfo.id === storePage.pageInfo.createUserId
|| storeUser.userInfo.id === storePage.pageInfo.createUserId;
}
const deleteComment = (id) => {
pageApi.deletePageComment({id: id}).then(() => {
// ElMessage.success("删除成功!");
loadCommentList()
})
loadCommentList();
});
}
const cancelCommentUser = () => {
recommentInfo.value = {}
recommentInfo.value = {};
}
const submitPageComment = () => {
if (commentTextInput.value.length <= 0) {
ElMessage.error('请输入评论内容')
return
ElMessage.error('请输入评论内容');
return;
}
let param = {
pageId: storePage.pageInfo.id,
@@ -123,26 +123,26 @@ const submitPageComment = () => {
parentId: recommentInfo.value.id,
}
pageApi.updatePageComment(param).then((json) => {
let data = json.data
data.color = getUserHeadBgColor(data.createUserId)
commentTextInput.value = ''
loadCommentList()
})
let data = json.data;
data.color = getUserHeadBgColor(data.createUserId);
commentTextInput.value = '';
loadCommentList();
});
}
const getUserHeadBgColor = (userId) => {
let color = page.userHeadColor[userId]
let color = page.userHeadColor[userId];
if (!color) {
color = page.colorArr[Math.ceil(Math.random() * page.colorArr.length) - 1]
page.userHeadColor[userId] = color
color = page.colorArr[Math.ceil(Math.random() * page.colorArr.length) - 1];
page.userHeadColor[userId] = color;
}
return color
return color;
}
</script>
<style lang="scss">
.comment-box {
padding: 8px;
height: calc(100vh - 315px);
height: calc(100vh - 275px);
overflow: auto;
.comment-card {

View File

@@ -0,0 +1,196 @@
<template>
<div class="files-box">
<div class="upload-file-box">
<el-upload class="upload-page-file" :action="uploadFileUrl" ref="uploadFileRef"
:with-credentials="true" :before-upload="beforeUploadFile"
:on-success="uploadFileSuccess" :on-error="uploadFileError"
name="files" show-file-list multiple :data="uploadFormData" :limit="999"
style="display: inline;vertical-align: sub;">
<a-button :icon="h(UploadOutlined)" type="primary" style="width: 260px;">上传附件</a-button>
</el-upload>
</div>
<div v-if="storePage.fileList.length <= 0" class="action-box-empty">
<el-empty description="暂无附件" />
</div>
<div v-else class="files-list">
<template v-for="file in storePage.fileList">
<a-card class="box-card files-card" :body-style="{ padding: '10px' }">
<div class="head">{{file.createUserName}}</div>
<div class="files-user-name">
<span>{{file.createUserName}}</span>
<el-tooltip :content="file.createTime" placement="top-start" :show-after="1000">
<span class="time">{{ file.createTime }}</span>
</el-tooltip>
<el-icon v-if="storePage.pageAuth.canDeleteFile === 1" @click="deleteFile(file)" class="icon-delete"><ElIconDelete /></el-icon>
</div>
<div class="files-name">
<a target="_blank" :href="file.fileUrl">{{file.fileName || '--'}}</a>
</div>
<div class="files-size">
<IconParkMemoryOne/><span class="value">{{computeFileSize(file.fileSize)}}</span>
<IconParkDownload style="margin-left: 15px;"/> <span class="value">{{file.downloadNum || 0}}</span>
</div>
</a-card>
</template>
</div>
</div>
</template>
<script setup>
import {Delete as ElIconDelete,} from '@element-plus/icons-vue'
import {UploadOutlined} from '@ant-design/icons-vue';
import {toRefs, ref, reactive, onMounted, watch, h, defineEmits, computed} from 'vue';
import {useRouter, useRoute} from "vue-router";
import { message, Modal } from 'ant-design-vue';
import pageApi from "@/assets/api/page";
import {useStorePageData} from "@/store/pageData";
import unitUtil from "@/assets/lib/UnitUtil";
import {
MemoryOne as IconParkMemoryOne,
Download as IconParkDownload,
} from '@icon-park/vue-next'
let storePage = useStorePageData();
let uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + '/zyplayer-doc-wiki/page/file/upload');
const deleteFile = (file) => {
Modal.confirm({
maskClosable: true,
title: '提示',
content: '确定要删除此文件吗?',
okText: '删除',
cancelText: '确定',
onOk: () => {
pageApi.deletePageFile({id: file.id}).then(() => {
storePage.fileList = storePage.fileList.filter(item => item !== file);
});
}
});
}
let uploadFileRef = ref();
let uploadFormData = computed(() => {
return {pageId: storePage.pageInfo.id};
});
const beforeUploadFile = (file) => {
}
const uploadFileError = (err) => {
message.error('上传失败,' + err);
}
let uploadOverStatus = ['success', 'fail'];
const uploadFileSuccess = (response, file, fileList) => {
if (response.errCode !== 200) {
message('上传失败:' + (response.errMsg || '未知错误'));
} else {
storePage.fileList.push(response.data);
}
// 是否全部上传完成
if (fileList.every(file => uploadOverStatus.includes(file.status))) {
message.success('上传成功!');
uploadFileRef.value.clearFiles();
}
}
const computeFileSize = (fileSize) => {
return unitUtil.computeFileSize(fileSize);
}
</script>
<style lang="scss">
.files-box {
.upload-page-file {
.el-upload-list {
display: none;
}
}
}
</style>
<style scoped lang="scss">
.files-box {
padding: 50px 8px 8px;
height: calc(100vh - 130px);
overflow: auto;
box-sizing: border-box;
position: relative;
.upload-file-box {
position: absolute;
top: 0;
left: 0;
text-align: center;
background: #fff;
width: 100%;
z-index: 1;
padding-bottom: 5px;
}
ul {
padding-left: 0;
}
.files-card {
margin-bottom: 10px;
}
.files-user-name {
margin-bottom: 6px;
font-size: 13px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
position: relative;
}
.files-card .files-user-name .icon-delete {
color: #888;
font-size: 13px;
cursor: pointer;
float: right;
display: none;
}
.files-card .files-user-name .icon-delete {
color: #888;
font-size: 13px;
cursor: pointer;
display: none;
position: absolute;
right: 0;
top: 2px;
background: #fff;
}
.files-card:hover .files-user-name .icon-delete {
display: inline-block;
}
.head {
float: left;
margin-right: 10px;
line-height: 40px;
}
.files-user-name .time {
color: #888;
margin-left: 8px;
}
.files-name {
padding: 0;
color: #666;
margin: 0;
line-height: 20px;
font-size: 14px;
}
.files-size {
margin-left: 50px;
margin-top: 5px;
font-size: 14px;
.value {
margin-left: 4px;
vertical-align: middle;
}
}
}
</style>

View File

@@ -15,14 +15,25 @@
import {
Search as ElIconSearch,
} from '@element-plus/icons-vue'
import {markRaw} from 'vue'
import {toRefs, ref, reactive, onMounted, onBeforeUnmount, defineProps, watch, defineEmits, computed, defineExpose} from 'vue';
import {markRaw} from 'vue';
import {
toRefs,
ref,
reactive,
onMounted,
onBeforeUnmount,
defineProps,
watch,
defineEmits,
computed,
defineExpose
} from 'vue';
import {useRouter, useRoute} from "vue-router";
import {ElMessageBox, ElMessage, ElLoading, ElNotification} from 'element-plus'
import {ElMessageBox, ElMessage, ElLoading, ElNotification} from 'element-plus';
import pageApi from "@/assets/api/page";
import {useStorePageData} from "@/store/pageData";
import userApi from "@/assets/api/user";
import QRCode from 'qrcode'
import QRCode from 'qrcode';
import {useStoreSpaceData} from "@/store/spaceData";
let storePage = useStorePageData();
@@ -59,18 +70,13 @@ const initMobileQrScan = () => {
});
let hostPath = window.location.href.split('#')[0];
setTimeout(() => {
qrCodeUrl.value = hostPath + routeUrl.href
qrCodeUrl.value = hostPath + routeUrl.href;
QRCode.toCanvas(qrCodeDivRef.value, qrCodeUrl.value, {
scale: 5, height: 250, wight: 250,
}, (error) => {
if (error) console.error(error);
}
)
);
}, 0);
}
</script>
<style lang="scss">
.page-auth-dialog {
}
</style>

View File

@@ -1,8 +1,13 @@
<template>
<div class="navigation-box">
<div class="navigation-content-box">
<div></div>
<div class="nav-heading" :style="{ width: navigationWidth }">
<div :style="navigationStyle" class="navigation-box">
<div v-if="navigationMin">
<a-button @click="navigationToMax" class="navigation-max-btn" size="large" type="text" shape="circle">
<IconParkViewList :size="18"/>
</a-button>
</div>
<div v-else :style="navigationStyle" @mouseover="navigationMouseover" @mouseleave="navigationMouseleave"
:class="navigationShow?'':'box-shadow'" class="navigation-content-box">
<div class="nav-heading">
<div v-for="item in heading" :class="'heading-item heading-' + item.level" @click="headingItemClick(item)">
{{ item.text }}
</div>
@@ -12,14 +17,10 @@
</template>
<script setup>
import {toRefs, ref, reactive, onMounted, watch, defineEmits, defineProps, defineExpose,} from 'vue'
import {useStoreDisplay} from '@/store/wikiDisplay.js'
import {useStorePageData} from "@/store/pageData";
import {ViewList as IconParkViewList} from '@icon-park/vue-next'
import {toRefs, ref, reactive, onMounted, watch, defineEmits, defineProps, defineExpose,} from 'vue';
import {useResizeEvent} from "@/composable/windowsScroll";
let storePage = useStorePageData();
const storeDisplay = useStoreDisplay();
let navigationWidth = ref('100px');
const props = defineProps({
heading: {type: Array, default: []},
});
@@ -29,8 +30,44 @@ onMounted(() => {
useResizeEvent(() => {
computeNavigationWidth();
});
let isLeave = false;
let leaveTimer = undefined;
const navigationMouseover = () => {
isLeave = false;
if (leaveTimer) {
clearTimeout(leaveTimer);
leaveTimer = undefined;
}
}
const navigationMouseleave = () => {
if (isLeave) return;
isLeave = true;
leaveTimer = setTimeout(() => {
isLeave = false;
navigationMin.value = !navigationShow.value;
}, 500);
}
const navigationToMax = () => {
navigationMin.value = false;
}
let navigationMin = ref(false);
let navigationShow = ref(true);
let navigationStyle = ref({width: '200px'});
const computeNavigationWidth = () => {
let pageViewContent = document.getElementById('pageContentBox');
let pageContentScrollBox = document.getElementById('pageContentScrollBox');
// pageContentScrollBox的宽度减去pageViewContent的宽度除以2
if (pageViewContent && pageContentScrollBox) {
let outerWidth = pageContentScrollBox.clientWidth - pageViewContent.clientWidth;
if (outerWidth > 160) {
navigationShow.value = true;
navigationStyle.value.width = (outerWidth / 2 - 50) + 'px';
} else {
navigationStyle.value.width = '200px';
navigationShow.value = false;
}
navigationMin.value = !navigationShow.value;
}
}
const headingItemClick = (item) => {
// 滚动到指定节点
@@ -40,29 +77,43 @@ const headingItemClick = (item) => {
inline: 'nearest',
});
// 距离顶部高度
//console.log(item.node.offsetTop - item.node.scrollHeight)
//console.log(item.node.offsetTop - item.node.scrollHeight);
}
</script>
<style lang="scss">
.navigation-box {
width: 100px;
position: absolute;
top: 150px;
top: 60px;
right: 10px;
z-index: 4;
.navigation-max-btn {
position: fixed;
margin-left: 160px;
opacity: 0.7;
box-shadow: var(--el-box-shadow-light);
&:hover {
opacity: 1;
background: #fff;
}
}
.navigation-content-box {
position: fixed;
background: #fff;
border-radius: 8px;
padding: 16px;
margin-left: -40px;
box-shadow: var(--el-box-shadow-lighter);
margin-left: -30px;
&.box-shadow {
box-shadow: var(--el-box-shadow-lighter);
}
}
.nav-heading {
max-height: calc(100vh - 250px);
max-height: calc(100vh - 200px);
overflow-y: auto;
.heading-item {

View File

@@ -2,7 +2,7 @@
<!--人员权限弹窗-->
<el-dialog title="页面权限" v-model="dataItemEditVisible" width="800px" class="page-auth-dialog">
<el-row>
<el-select v-model="pageAuthNewUser" filterable remote reserve-keyword autoComplete="new-password" placeholder="请输入名字、邮箱、账号搜索用户" :remote-method="getSearchUserList" :loading="pageAuthUserLoading" style="width: 690px; margin-right: 10px">
<el-select v-model="pageAuthNewUser" filterable remote reserve-keyword autoComplete="new-password" placeholder="请输入名字、邮箱、账号搜索用户" :remote-method="getSearchUserList" :loading="pageAuthUserLoading" style="width: 690px; margin-right: 10px;">
<el-option v-for="item in searchUserList" :key="item.id" :label="item.userName" :value="item.id"></el-option>
</el-select>
<el-button @click="addPageAuthUser">添加</el-button>
@@ -35,10 +35,10 @@
import {
Search as ElIconSearch,
} from '@element-plus/icons-vue'
import {markRaw} from 'vue'
import {markRaw} from 'vue';
import {toRefs, ref, reactive, onMounted, onBeforeUnmount, defineProps, watch, defineEmits, computed, defineExpose} from 'vue';
import {useRouter, useRoute} from "vue-router";
import {ElMessageBox, ElMessage, ElLoading, ElNotification} from 'element-plus'
import {ElMessageBox, ElMessage, ElLoading, ElNotification} from 'element-plus';
import pageApi from "@/assets/api/page";
import {useStorePageData} from "@/store/pageData";
import userApi from "@/assets/api/user";
@@ -106,7 +106,7 @@ const addPageAuthUser = () => {
pageFileUpload: 0,
pageFileDelete: 0,
pageAuthManage: 0,
})
});
pageAuthNewUser.value = '';
}
const getSearchUserList = (query) => {

View File

@@ -1,22 +1,22 @@
<template>
<div class="action-tab-box">
<div v-if="props.pageHistoryList.length <= 0" class="action-box-empty">
<div v-if="history.length <= 0" class="action-box-empty">
暂无修改历史记录
</div>
<el-timeline v-else>
<el-timeline-item v-for="history in props.pageHistoryList">
<el-tag :type="props.pageHistoryChoice.id === history.id ? history.loading === 3 ? 'danger' : 'success' : 'info'"
<el-timeline-item v-for="history in history">
<el-tag :type="pageHistoryChoice.id === history.id ? (history.loading === 3 ? 'danger' : 'success') : 'info'"
class="history-item" @click="historyClick(history)">
<div>{{ history.createUserName }}</div>
<div>{{ history.createTime }}</div>
</el-tag>
<el-icon class="history-loading-status" v-show="history.loading===1">
<el-icon class="history-loading-status" v-if="history.loading===1">
<el-icon-loading/>
</el-icon>
<el-icon class="history-loading-status" v-show="history.loading===2">
<el-icon class="history-loading-status" v-else-if="history.loading===2">
<el-icon-circle-check/>
</el-icon>
<el-icon class="history-loading-status" v-show="history.loading===3">
<el-icon class="history-loading-status" v-else-if="history.loading===3">
<el-icon-circle-close/>
</el-icon>
</el-timeline-item>
@@ -31,50 +31,40 @@ import {
Loading as ElIconLoading,
} from '@element-plus/icons-vue'
import pageApi from '@/assets/api/page'
import pageApi from '@/assets/api/page';
import {mavonEditor} from "mavon-editor";
import {ref, defineProps, defineEmits} from 'vue';
import {useStorePageData} from "@/store/pageData";
let storePage = useStorePageData();
let props= defineProps({
pageHistoryList:Array,
pageHistoryChoice:Object,
pageHistoryDetail:String,
})
let emit = defineEmits(['historyClickHandle','previewPageImage','createNavigationHeading'])
let props = defineProps({
history: Array,
});
let emit = defineEmits(['choice']);
let pageHistoryChoice = ref({});
const historyClick = (history) => {
if (props.pageHistoryChoice.id === history.id && !!props.pageHistoryDetail.value) {
if (pageHistoryChoice.value.id === history.id && !!pageHistoryChoice.value.content) {
return;
}
pageHistoryChoice.value.loading = 0;
pageHistoryChoice.value = history;
// 缓存一下,但如果历史页面多了而且很大就占内存,也可以每次去拉取,先这样吧
if (history.content) {
history.loading = 2;
emit('historyClickHandle',history)
setTimeout(() => {
emit('previewPageImage',history)
emit('createNavigationHeading',history)
}, 500)
emit('choice', history);
} else {
history.loading = 1
history.loading = 1;
pageApi.pageHistoryDetail({id: history.id}).then((json) => {
history.loading = 2;
history.content = json.data || '--';
if (storePage.pageInfo.editorType === 2) {
history.content = mavonEditor.getMarkdownIt().render(history.content);
}
emit('historyClickHandle',history)
setTimeout(() => {
emit('previewPageImage',history)
emit('createNavigationHeading',history)
}, 500);
emit('choice', history);
}).catch(() => {
history.loading = 3;
});
}
}
</script>
<style lang="scss">
</style>

View File

@@ -1,25 +1,29 @@
<template>
<div style="margin-top: 40px; font-size: 14px">
<span style="vertical-align: top" class="is-link">
<span v-show="storePage.pageInfo.selfZan == 0" @click="zanPage(1)"><img src="../../../assets/img/zan.png" style="vertical-align: middle"/> </span>
<span v-show="storePage.pageInfo.selfZan == 1" @click="zanPage(0)"><img src="../../../assets/img/zan.png" style="vertical-align: middle; transform: rotateX(180deg)"/> </span>
</span>
<span style="margin-left: 10px; vertical-align: top">
<span v-if="storePage.pageInfo.selfZan == 0 && storePage.pageInfo.zanNum <= 0">成为第一个赞同者</span>
<span v-else-if="storePage.pageInfo.selfZan == 0 && storePage.pageInfo.zanNum > 0">
<span class="is-link" @click="showZanPageUser">{{ storePage.pageInfo.zanNum }}</span>赞了它
</span>
<span v-else-if="storePage.pageInfo.selfZan == 1 && storePage.pageInfo.zanNum <= 1">我赞了它</span>
<span v-else-if="storePage.pageInfo.selfZan == 1 && storePage.pageInfo.zanNum > 1">
<span class="is-link" @click="showZanPageUser">我和{{ storePage.pageInfo.zanNum - 1 }}个其他人</span>赞了它
</span>
</span>
<span style="margin-left: 10px">
<el-icon style="font-size: 16px; color: #666;vertical-align: middle;"><el-icon-view/></el-icon> {{ storePage.pageInfo.viewNum }}次阅读
</span>
<div class="zan-box">
<div class="zan-btn-box">
<VueStarPlus v-model="starActive" animate="animated tada">
<template #icon>
<a-button @click="zanPage()" :type="starActive?'primary':'default'" shape="circle" class="zan-btn">
<IconParkThumbsUp :size="30"/>
</a-button>
</template>
</VueStarPlus>
</div>
<template v-if="storePage.pageInfo.zanNum <= 0">
<el-divider>为它点赞</el-divider>
</template>
<template v-else-if="storePage.pageInfo.zanNum > 0">
<template v-if="storePage.pageInfo.selfZan === 1">
<template v-if="storePage.pageInfo.zanNum === 1">
<el-divider>我赞了它</el-divider>
</template>
<el-divider v-else>我和<a @click="showZanPageUser">{{storePage.pageInfo.zanNum - 1}} </a> 赞了它</el-divider>
</template>
<el-divider v-else><a @click="showZanPageUser">{{storePage.pageInfo.zanNum}} </a> 赞了它</el-divider>
</template>
</div>
<el-dialog title="赞了它的人" v-model="zanUserDialogVisible" width="600px">
<el-table :data="zanUserList" border :show-header="false" style="width: 100%; margin-bottom: 5px">
<el-table :data="zanUserList" border :show-header="false" style="width: 100%; margin-bottom: 5px;">
<el-table-column prop="createUserName" label="用户"></el-table-column>
<el-table-column prop="createTime" label="时间"></el-table-column>
</el-table>
@@ -27,29 +31,69 @@
</template>
<script setup>
import pageApi from '../../../assets/api/page'
import {ref} from 'vue';
import {ThumbsUp as IconParkThumbsUp} from '@icon-park/vue-next'
import pageApi from '../../../assets/api/page';
import {ref, watch, onMounted} from 'vue';
import {useStorePageData} from "@/store/pageData";
import {
View as ElIconView,
} from '@element-plus/icons-vue'
let zanUserList = ref([]);
let zanUserDialogVisible = ref(false);
import VueStarPlus from 'vue-star-plus'
import 'vue-star-plus/style.css'
let storePage = useStorePageData();
const zanPage = (yn) => {
let param = {yn: yn, pageId: storePage.pageInfo.id}
let zanUserList = ref([]);
let zanUserDialogVisible = ref(false);
let starActive = ref(false);
watch(() => storePage.pageInfo, () => {
starActive.value = (storePage.pageInfo.selfZan === 1);
});
onMounted(() => {
starActive.value = (storePage.pageInfo.selfZan === 1);
});
const zanPage = () => {
let yn = storePage.pageInfo.selfZan === 1 ? 0 : 1;
let param = {yn: yn, pageId: storePage.pageInfo.id};
pageApi.updatePageZan(param).then(() => {
storePage.pageInfo.selfZan = yn
storePage.pageInfo.zanNum = storePage.pageInfo.zanNum + (yn == 1 ? 1 : -1)
})
starActive.value = (yn === 1);
storePage.pageInfo.selfZan = yn;
storePage.pageInfo.zanNum = storePage.pageInfo.zanNum + (yn === 1 ? 1 : -1);
});
}
const showZanPageUser = () => {
zanUserDialogVisible.value = true
zanUserList.value = []
let param = {pageId: storePage.pageInfo.id}
zanUserDialogVisible.value = true;
zanUserList.value = [];
let param = {pageId: storePage.pageInfo.id};
pageApi.pageZanList(param).then((json) => {
zanUserList.value = json.data
})
zanUserList.value = json.data || [];
});
}
</script>
<style lang="scss">
.zan-box {
margin-top: 50px;
text-align: center;
.zan-btn-box {
position: relative;
.vue-star-plus {
width: 100%;
position: unset;
.vue-star-plus__ground {
margin: 0 auto;
}
}
.zan-btn {
height: 60px;
width: 60px;
}
}
.el-divider {
width: 300px;
margin: 20px auto;
}
}
</style>