登录页开发,控制台页面开发,页面跳转逻辑调整

This commit is contained in:
sswiki
2024-12-24 22:50:32 +08:00
parent e9f1c3b5cf
commit 91bad1a059
24 changed files with 1020 additions and 111 deletions

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import router from '@/routes.js'
import {ElMessageBox, ElMessage} from 'element-plus';
const service = axios.create({
@@ -24,7 +25,6 @@ service.interceptors.request.use((config) => {
return Promise.reject(error);
}
);
let lastToastLoginTime = new Date().getTime();
service.interceptors.response.use(
(response) => {
if (!!response.message) {
@@ -33,13 +33,9 @@ service.interceptors.response.use(
if (!response.config.needValidateResult || response.data.errCode === 200) {
return response.data;
} else if (response.data.errCode === 400) {
// 两秒钟只提示一次
if (new Date().getTime() - lastToastLoginTime > 2000) {
ElMessage.warning('请先登录');
lastToastLoginTime = new Date().getTime();
}
let href = encodeURIComponent(window.location.href);
window.location = import.meta.env.VITE_APP_BASE_API + '#/user/login?redirect=' + href;
let redirectUrl = getRedirectUrl();
router.push({path: `/user/login`, query: {redirect: redirectUrl}});
return Promise.reject(response.data);
} else if (response.data.errCode !== 200) {
ElMessage.error(response.data.errMsg || '未知错误');
}
@@ -51,4 +47,16 @@ service.interceptors.response.use(
return Promise.reject(error);
}
);
function getRedirectUrl() {
let redirectUrl = '';
let locationHref = window.location.href;
if (locationHref.indexOf('?') >= 0) {
let reg = new RegExp('(^|&)redirect=([^&]*)(&|$)', 'i');
let r = locationHref.substring(locationHref.indexOf('?') + 1).match(reg);
if (r != null) {
redirectUrl = unescape(r[2]);
}
}
return redirectUrl || encodeURIComponent(window.location.href);
}
export default service

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,7 +1,6 @@
import hljs from 'highlight.js';
import {createApp} from 'vue';
import App from './App.vue';
import {createRouter, createWebHashHistory} from 'vue-router';
import ElementUI from 'element-plus';
import Antd from 'ant-design-vue';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
@@ -15,10 +14,6 @@ import './assets/scss/markdown.scss';
import './assets/scss/pageView.scss';
import './assets/scss/base.scss';
const router = createRouter({
history: createWebHashHistory(),
routes,
});
const app = createApp(App);
app.config.productionTip = false;
app.use(Antd);
@@ -26,7 +21,7 @@ app.use(ElementUI, {
locale: zhCn,
});
app.use(Vant);
app.use(router);
app.use(routes);
app.use(createPinia());
app.mount('#app');

View File

@@ -1,4 +1,8 @@
import Login from './views/user/Login.vue';
import PageLayout from './views/view/PageLayout.vue';
import WikiLayout from './views/wiki/Layout.vue';
import WikiSpace from './views/wiki/Wiki.vue';
// import ShareLayout from './components/layouts/ShareLayout.vue';
// import ShareMobileLayout from './components/layouts/ShareMobileLayout.vue';
@@ -16,51 +20,63 @@ import Edit from './views/view/Edit.vue';
// import sharePcView from './views/page/share/pc/View.vue';
// import shareMobileView from './views/page/share/mobile/View.vue';
let routes = [
{path: '/', redirect: '/home'},
{path: '/page/search', name: 'WIKI-全局搜索', component: NoAuth},
{path: '/common/noAuth', name: 'WIKI-没有权限', component: NoAuth},
{
path: '/',
name: '文档管理',
component: PageLayout,
children: [
{path: '/home', name: 'WIKI文档管理', component: NoAuth},
{path: '/user/myInfo', name: 'WIKI-我的信息', component: NoAuth},
{path: '/view/:spaceId?/:pageId?', name: 'WIKI-页面查看', component: Show},
{path: '/edit/:spaceId/:pageId', name: 'WIKI-编辑内容', component: Edit},
{path: '/space/manage', name: 'WIKI-空间管理', component: NoAuth},
],
},
{
path: '/',
name: 'PC端开放文档',
component: NoAuth,
children: [
{
path: '/page/share/home',
name: 'WIKI-开放文档',
component: NoAuth,
},
{
path: '/page/share/view',
name: 'WIKI-内容展示',
component: NoAuth,
},
],
},
{
path: '/',
name: 'APP端开放文档',
component: NoAuth,
children: [
{
path: '/page/share/mobile/view',
name: 'WIKI-开放文档-APP',
component: NoAuth,
},
],
},
];
import {createRouter, createWebHashHistory} from 'vue-router';
export default routes;
export default createRouter({
history: createWebHashHistory(),
routes: [
{path: '/', redirect: '/wiki/space'},
{path: '/user/login', name: 'systemLogin', component: Login},
{path: '/page/search', name: 'WIKI-全局搜索', component: NoAuth},
{path: '/common/noAuth', name: 'WIKI-没有权限', component: NoAuth},
{
path: '/',
name: 'WikiLayout',
component: WikiLayout,
children: [
{path: '/wiki/space', name: 'WIKI文档管理', component: WikiSpace},
],
},
{
path: '/',
name: '文档管理',
component: PageLayout,
children: [
// {path: '/home', name: 'WIKI文档管理', component: NoAuth},
{path: '/user/myInfo', name: 'WIKI-我的信息', component: NoAuth},
{path: '/view/:spaceId?/:pageId?', name: 'WIKI-页面查看', component: Show},
{path: '/edit/:spaceId/:pageId', name: 'WIKI-编辑内容', component: Edit},
{path: '/space/manage', name: 'WIKI-空间管理', component: NoAuth},
],
},
{
path: '/',
name: 'PC端开放文档',
component: NoAuth,
children: [
{
path: '/page/share/home',
name: 'WIKI-开放文档',
component: NoAuth,
},
{
path: '/page/share/view',
name: 'WIKI-内容展示',
component: NoAuth,
},
],
},
{
path: '/',
name: 'APP端开放文档',
component: NoAuth,
children: [
{
path: '/page/share/mobile/view',
name: 'WIKI-开放文档-APP',
component: NoAuth,
},
],
},
]
});

View File

@@ -3,10 +3,11 @@ import {defineStore} from 'pinia';
export const useStoreSpaceData = defineStore('spaceData', {
state: () => {
return {
spaceInfo:{},
chooseSpaceId:1,
spaceInfo: {},
chooseSpaceId: 1,
spaceOptions: [],
spaceList:[]
spaceList: [],
wholeSpaceList: [],
}
},
});

View File

@@ -6,6 +6,9 @@ export const useStoreUserData = defineStore('userData', {
// 用户信息
userInfo: {},
upgradeInfo: {},
// 左侧菜单的路由和收起状态
menuRouteKey: [],
menuCollapsed: false,
}
},
});

View File

@@ -0,0 +1,108 @@
<template>
<div class="login-page-view login-background" style="background-image: url('src/assets/img/login-bg.jpg')">
<div class="login-warp-flex">
<div class="login-content">
<a-form :model="loginParam" :rules="loginRules" layout="vertical" ref="loginParamRef" label-position="left" label-width="0px" class="login-form">
<h3 class="login-title">文档管理系统 - 账号登录</h3>
<a-form-item name="username">
<a-input type="text" v-model:value="loginParam.username" auto-complete="off" size="large"
placeholder="请输入账号" @keyup.enter.native="loginSubmit()"/>
</a-form-item>
<a-form-item name="password">
<a-input type="password" v-model:value="loginParam.password" auto-complete="off" size="large"
placeholder="请输入密码" @keyup.enter.native="loginSubmit()"/>
</a-form-item>
<a-form-item style="width: 100%">
<a-button type="primary" style="width: 100%" size="large" @click.native.prevent="loginSubmit()" :loading="loginLoading">登录</a-button>
</a-form-item>
</a-form>
</div>
</div>
<LoginFooter/>
</div>
</template>
<script setup>
import {onMounted, ref, computed, h} from 'vue';
import {useRoute, useRouter} from "vue-router";
import userApi from '@/assets/api/user.js'
import {useStoreUserData} from "@/store/userData";
import LoginFooter from './LoginFooter.vue'
let storeUser = useStoreUserData();
let route = useRoute();
let router = useRouter();
onMounted(() => {
redirect.value = route.query.redirect;
});
let loginLoading = ref(false);
let redirect = ref('');
let loginParam = ref({
username: '',
password: '',
});
let loginRules = computed(() => ({
username: [{required: true, message: '请输入账号或邮箱', trigger: 'blur'}],
password: [{required: true, message: '请输入密码', trigger: 'blur'}],
}));
let loginParamRef = ref();
const loginSubmit = () => {
loginParamRef.value.validate().then(() => {
loginLoading.value = true;
userApi.userLogin(loginParam.value).then(() => {
// 跳转回之前的页面
loginLoading.value = false;
if (!!redirect.value) {
location.href = decodeURIComponent(redirect.value);
} else {
// 没有目标页面跳至文档首页
router.replace({path: '/'});
}
}).catch((e) => {
console.log('登录失败', e);
loginLoading.value = false;
});
});
}
</script>
<style lang="scss">
.login-page-view {
.el-form-item {
margin-bottom: 25px;
}
}
</style>
<style lang="scss" scoped>
.login-page-view {
height: 100%;
width: 100%;
.login-warp-flex {
display: flex;
justify-content: center;
align-items: center;
height: 80%;
.login-content {
width: 400px;
max-width: 96%;
padding: 35px;
background: #ffffff99;
backdrop-filter: blur(10px);
border-radius: 10px;
box-sizing: border-box;
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
.login-form {
.login-title {
margin: 0 auto 40px auto;
text-align: center;
color: #505458;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div class="login-footer">
<span class="item">
Powered by <a target="_blank" href="https://doc.zyplayer.com">zyplayer-doc</a>
</span>
</div>
</template>
<script setup>
import {toRefs, ref, reactive, onMounted, watch, defineEmits} from 'vue';
import { useRouter, useRoute } from "vue-router";
import {useStoreUserData} from "@/store/userData";
let storeUser = useStoreUserData();
let route = useRoute();
let router = useRouter();
onMounted(() => {
});
</script>
<style lang="scss" scoped>
.login-footer {
font-size: 14px;
color: #666;
width: 100%;
position: absolute;
bottom: 0;
text-align: center;
padding: 15px 0;
.item + .item {
margin-left: 15px;
}
a {
color: #666;
text-decoration: none;
}
}
</style>

View File

@@ -17,10 +17,10 @@
<div v-if="wikiPage.editorType === 1">
<WangEditor ref="wangEditorRef" :pageId="pageId"></WangEditor>
</div>
<div v-else-if="wikiPage.editorType === 2" style="padding: 10px; background: #fff;">
<div v-else-if="wikiPage.editorType === 2" style="padding: 5px 8px 5px; background: #fff;">
<mavonEditor ref="mavonEditorRef" v-model="markdownContent" :toolbars="toolbars" :externalLink="false"
@save="createWikiSave(0)" @imgAdd="addMarkdownImage" placeholder="请录入文档内容"
class="page-content-editor wang-editor-body" style="height: calc(100vh - 100px);z-index: 1;"/>
class="page-content-editor wang-editor-body" style="height: calc(100vh - 80px);z-index: 1;"/>
</div>
</div>
</div>
@@ -219,8 +219,8 @@ const addMarkdownImage = (pos, file) => {
<style lang="scss">
.fake-header {
color: #333;
height: 60px !important;
line-height: 60px !important;
height: 50px !important;
line-height: 50px !important;
.fold-btn {
font-size: 18px;

View File

@@ -173,10 +173,19 @@ let pageContentRef = ref();
const initQueryParam = (to) => {
spaceId = parseInt(to.params.spaceId);
pageId = parseInt(to.params.pageId);
clearPageData();
if (!!pageId) {
loadPageDetail(pageId);
}
}
const clearPageData = () => {
wikiPage.value = {};
wikiPageAuth.value = {};
pageContent.value = '';
pageContentShow.value = '';
storePage.pageInfo = {};
storePage.pageAuth = {};
}
</script>
<style lang="scss">

View File

@@ -1,14 +1,19 @@
<template>
<div class="left-aside-box">
<div class="left-aside-top-box">
<el-select :model-value="storeSpace.chooseSpaceId" @change="spaceChangeEvents" filterable
placeholder="选择空间" class="choice-space-select">
<el-option-group label="" v-if="!props.readOnly">
<el-option :key="-1" label="空间管理" :value="-1"></el-option>
</el-option-group>
<el-option-group label=""></el-option-group>
<el-option v-for="item in storeSpace.spaceOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
<a-row type="flex" style="flex-flow: row nowrap;">
<a-col flex="32px" style="margin-right: -1px;">
<a-tooltip title="返回首页" placement="right" :mouseEnterDelay="0.5">
<a-button @click="openHomePage" :icon="h(HomeOutlined)" class="home-page-btn"/>
</a-tooltip>
</a-col>
<a-col flex="auto">
<el-select v-model="storeSpace.chooseSpaceId" @change="spaceChangeEvents" filterable
placeholder="选择空间" class="choice-space-select">
<el-option v-for="item in storeSpace.spaceOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</a-col>
</a-row>
<el-autocomplete v-model="searchKeywords" v-if="!props.readOnly" :fetch-suggestions="doSearchByKeywords"
@select="handleSearchKeywordsSelect" placeholder="在当前空间搜索"
popper-class="search-autocomplete-popper" class="search-autocomplete">
@@ -38,7 +43,7 @@
</div>
<div v-show="!spaceTreeIsClose" class="wiki-page-tree-box">
<el-tree ref="wikiPageTreeRef" :current-node-key="props.nowPageId" :data="storePage.wikiPageList"
:default-expanded-keys="wikiPageExpandedKeys" :expand-on-click-node="true" :class="explanClass"
:default-expanded-keys="wikiPageExpandedKeys" :expand-on-click-node="true"
:filter-node-method="filterPageNode" :props="defaultProps" :draggable="!props.readOnly"
@node-click="handleNodeClick" @node-drop="handlePageDrop" node-key="id" highlight-current
style="background-color: #fafafa;">
@@ -112,7 +117,7 @@ import {
EditTwo as IconParkEditTwo,
PageTemplate as IconParkPageTemplate,
} from '@icon-park/vue-next'
import { EllipsisOutlined } from '@ant-design/icons-vue';
import { EllipsisOutlined, HomeOutlined } 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';
@@ -122,8 +127,6 @@ import AddMenu from "./AddMenu.vue";
import IconDocument from "@/components/base/IconDocument.vue";
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";
import MessagePrompt from "@/components/single/MessagePrompt";
let route = useRoute();
@@ -132,11 +135,7 @@ let storePage = useStorePageData();
let storeDisplay = useStoreDisplay();
let storeSpace = useStoreSpaceData();
let emit = defineEmits(['spaceChangeEvents', 'setNowPageId']);
let searchKeywords = ref('');
let descriptorForTree = ref("点击收起目录");
let explan = ref(false);
let explanClass = ref("el-tree");
let wikiPageExpandedKeys = ref([]);
let defaultProps = ref({children: 'children', label: 'name',});
let wikiPageTreeRef = ref();
@@ -153,8 +152,20 @@ onMounted(() => {
watch(() => storePage.eventPageListUpdate, () => {
loadSpaceList();
});
watch(() => storeSpace.spaceInfo, () => {
doGetPageList();
});
let openHomePage = (event) => {
if (event.ctrlKey) {
let routeUrl = router.resolve({path: `/wiki/space`});
window.open(routeUrl.href, '_blank');
} else {
router.push({path: '/wiki/space'});
}
}
let nowSpaceShow = ref({});
const loadSpaceList = (spaceId) => {
const loadSpaceList = () => {
let spaceId = parseInt(route.params.spaceId);
pageApi.spaceList({}).then((json) => {
storeSpace.spaceList = json.data || [];
let spaceOptionsNew = [];
@@ -172,14 +183,6 @@ const loadSpaceList = (spaceId) => {
storeSpace.chooseSpaceId = nowSpaceId;
storePage.choosePageId = 0;
doGetPageList();
// TODO 在首页时跳转
try {
if (route.path === '/home') {
router.push({path: '/home', query: {spaceId: nowSpaceId}});
}
} catch (e) {
console.log(e);
}
}
});
}
@@ -188,7 +191,7 @@ const changeNodeOptionStatus = (param) => {
optionPageId.value = param.id;
}
const assisSetCurrentKey = () => {
emit('setNowPageId', route.query.pageId, props.readOnly);
// emit('setNowPageId', route.query.pageId, props.readOnly);
if (props.nowPageId) {
wikiPageTreeRef.value.setCurrentKey(nowPageId.value);
}
@@ -257,7 +260,12 @@ const deleteWikiPage = (data) => {
});
}
const spaceChangeEvents = (data) => {
emit('spaceChangeEvents', data, props.readOnly);
let nowSpaceShowTemp = storeSpace.spaceList.find((item) => item.id === data);
nowSpaceShow.value = nowSpaceShowTemp;
storeSpace.spaceInfo = nowSpaceShowTemp;
storeSpace.chooseSpaceId = data;
storePage.choosePageId = 0;
router.push({path: `/view/${data}`});
}
const doRename = (node, data) => {
pageApi.renamePage({"id": data.id, "name": data.name}).then((json) => {
@@ -266,6 +274,8 @@ const doRename = (node, data) => {
});
}
const doGetPageList = () => {
storePage.pageList = [];
storePage.favoritePageList = [];
let param = {spaceId: storeSpace.chooseSpaceId};
pageApi.pageList(param).then((json) => {
storePage.wikiPageList = json.data || [];
@@ -334,9 +344,22 @@ defineExpose({searchByKeywords});
.left-aside-top-box {
padding: 10px;
.home-page-btn {
border-radius: 4px 0 0 4px;
z-index: 0;
&:hover, &:focus, &:active {
z-index: 1;
}
}
.choice-space-select {
width: 100%;
margin-bottom: 5px;
.el-input__wrapper {
border-radius: 0 4px 4px 0;
}
}
.search-autocomplete {

View File

@@ -9,14 +9,8 @@ import {toRefs, ref, reactive, onMounted, onBeforeUnmount, watch, defineEmits, c
const props = defineProps({
modelValue: Number,
max: {
type: Number,
default: 600
},
min: {
type: Number,
default: 300
}
max: {type: Number, default: 600},
min: {type: Number, default: 200}
});
let emit = defineEmits(['update:modelValue', 'change']);

View File

@@ -124,7 +124,7 @@ defineExpose({setContent, getContent, getPreview});
.wang-editor-box .wang-editor-content {
padding: 20px 0;
overflow: auto;
height: calc(100vh - 140px);
height: calc(100vh - 130px);
}
.wang-editor-box .w-e-bar-item {

View File

@@ -31,7 +31,7 @@
<div class="comment-input-box">
<textarea rows="5" placeholder="发表评论" v-model="commentTextInput" :maxlength="500"></textarea>
<div class="comment-btn-box">
<el-button type="primary" size="small" @click="submitPageComment">发送</el-button>
<a-button @click="submitPageComment">发送</a-button>
</div>
</div>
</div>
@@ -56,7 +56,6 @@ let page = {
// 评论相关
let commentTextInput = ref('');
let commentList = ref([]);
let recommentInfo = ref({});
let route = useRoute();
let router = useRouter();
@@ -82,7 +81,6 @@ const loadCommentList = () => {
if (!storePage.pageInfo || !storePage.pageInfo.id) {
return;
}
cancelCommentUser();
pageApi.pageCommentList({pageId: storePage.pageInfo.id}).then((json) => {
let commentListRes = json.data || [];
for (let i = 0; i < commentListRes.length; i++) {
@@ -109,9 +107,6 @@ const deleteComment = (id) => {
loadCommentList();
});
}
const cancelCommentUser = () => {
recommentInfo.value = {};
}
const submitPageComment = () => {
if (commentTextInput.value.length <= 0) {
ElMessage.error('请输入评论内容');
@@ -120,7 +115,6 @@ const submitPageComment = () => {
let param = {
pageId: storePage.pageInfo.id,
content: commentTextInput.value,
parentId: recommentInfo.value.id,
}
pageApi.updatePageComment(param).then((json) => {
let data = json.data;

View File

@@ -0,0 +1,125 @@
<template>
<a-layout class="wiki-container">
<a-layout-header theme="light" class="wiki-container-header">
<HeaderView/>
</a-layout-header>
<a-layout>
<a-layout-sider v-model:collapsed="storeUser.menuCollapsed" collapsible theme="light" class="left-layout-sider">
<ConsoleMenu></ConsoleMenu>
<template #trigger>
<div class="bottom-toggle-btn">
<a-button v-if="storeUser.menuCollapsed" :icon="h(DoubleRightOutlined)" type="text" size="large" block></a-button>
<a-button v-else :icon="h(DoubleLeftOutlined)" type="text" size="large" block></a-button>
</div>
</template>
</a-layout-sider>
<a-layout>
<div class="right-content-box">
<router-view></router-view>
</div>
</a-layout>
</a-layout>
</a-layout>
</template>
<script setup>
import { message } from 'ant-design-vue';
import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
import {toRefs, ref, reactive, onMounted, watch, h, defineEmits, computed} from 'vue';
import {onBeforeRouteUpdate, onBeforeRouteLeave, useRouter, useRoute} from "vue-router";
import HeaderView from "./aside/HeaderView.vue";
import ConsoleMenu from "./aside/ConsoleMenu.vue";
import pageApi from "@/assets/api/page";
import {useStoreUserData} from "@/store/userData";
import {useStoreSpaceData} from "@/store/spaceData";
import userApi from "@/assets/api/user";
let route = useRoute();
let router = useRouter();
let storeUser = useStoreUserData();
let storeSpace = useStoreSpaceData();
onMounted(() => {
getSelfUserInfo();
loadSpaceList();
computeMenuKey(route);
});
onBeforeRouteUpdate((updateGuard) => {
computeMenuKey(updateGuard);
});
// 空间创建或修改
watch(() => storeSpace.eventSpaceUpdate, (newVal) => {
loadSpaceList();
});
const computeMenuKey = (to) => {
// 末级菜单key暂时只有控制台需要
if (to.matched && to.matched.length >= 2) {
let keyIndex = to.matched.length - 1;
let pathArr = to.matched[keyIndex].path.split('/');
let menuKey = '', menuKeyArr = [];
pathArr.filter(item => !!item && item.length > 0).forEach((item, index) => {
if (index > 0) menuKey += '-';
menuKey += item;
menuKeyArr.push(menuKey);
});
storeUser.menuRouteKey = menuKeyArr;
}
}
const getSelfUserInfo = () => {
userApi.getSelfUserInfo().then((json) => {
storeUser.userInfo = json.data || {};
}).catch((e) => {
});
}
const loadSpaceList = () => {
pageApi.spaceList().then(json => {
let resList = json.data || [];
resList.forEach(item => {
item.searchText = ((item.name || '') + ' ' + (item.spaceExplain || '')).toLowerCase();
});
storeSpace.spaceList = resList;
storeSpace.wholeSpaceList = resList;
});
}
</script>
<style lang="scss">
.wiki-container {
height: 100%;
.bottom-toggle-btn {
padding: 0 4px;
border-right: 1px solid #eee;
.ant-btn {
width: 100%;
}
}
.wiki-container-header {
padding-inline: 0;
height: 60px;
line-height: 60px;
}
.ant-layout {
background: #fff;
.left-layout-sider {
height: 100%;
background: #fff;
border-right: 1px solid #eee;
transition: unset;
> .ant-layout-sider-children {
overflow: auto;
}
}
}
.right-content-box {
height: 100%;
overflow: auto;
background: #fff;
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="space-create-box">
<CreateRow></CreateRow>
</div>
<div class="space-view-box">
<Whole></Whole>
</div>
</template>
<script setup>
import CreateRow from './space/CreateRow.vue'
import Whole from './space/Whole.vue'
import {onMounted, watch} from 'vue';
import {useRoute} from "vue-router";
let route = useRoute();
onMounted(() => {
});
</script>
<style lang="scss" scoped>
.space-create-box {
z-index: 101;
position: relative;
padding: 0 10px;
}
.space-view-box {
height: calc(100vh - 130px);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<a-menu v-model:selectedKeys="storeUser.menuRouteKey" mode="inline" class="menu-view-box" style="border-right: 0;" >
<a-menu-item key="wiki-space" :icon="h(IconParkNotebook)">
<router-link to="/wiki/space">知识库</router-link>
</a-menu-item>
<a-sub-menu vxxxx-if="storeUser.userInfo.isManager" title="系统管理" key="manage" :icon="h(IconParkSettingConfig)">
<a-menu-item key="user-userList" :icon="h(IconParkUser)">
<router-link to="/user/userList">用户管理</router-link>
</a-menu-item>
<a-menu-item key="user-department" :icon="h(IconParkPeoples)">
<router-link to="/user/department">部门管理</router-link>
</a-menu-item>
<a-menu-item key="system-setting" :icon="h(IconParkSettingTwo)">
<router-link to="/system/setting">系统配置</router-link>
</a-menu-item>
</a-sub-menu>
</a-menu>
</template>
<script setup>
import {
Peoples as IconParkPeoples,
User as IconParkUser,
SettingConfig as IconParkSettingConfig,
Notebook as IconParkNotebook,
SettingTwo as IconParkSettingTwo,
} from '@icon-park/vue-next'
import {toRefs, ref, reactive, onMounted, onBeforeUnmount, watch, h, defineEmits, computed} from 'vue';
import { message } from 'ant-design-vue';
import {onBeforeRouteUpdate, useRoute, useRouter} from "vue-router";
import {useStoreUserData} from "@/store/userData";
let storeUser = useStoreUserData();
const route = useRoute();
const router = useRouter();
onMounted(() => {
});
</script>
<style lang="scss">
.menu-badge .el-badge__content {
margin-top: 14px;
right: 0 !important;
}
.menu-view-box {
.i-icon {
font-size: 18px;
}
}
// 左侧菜单收缩时的样式,上面图标、下面文字,样式强制覆盖
.left-layout-sider.ant-layout-sider-collapsed {
.ant-menu {
> .ant-menu-item, > .ant-menu-submenu .ant-menu-submenu-title {
height: 60px;
line-height: 40px;
padding-inline: unset;
.ant-menu-item-icon {
display: block;
line-height: 20px;
margin-top: 10px;
}
.ant-menu-title-content {
display: block;
opacity: 1;
margin-inline-start: 8px;
line-height: 24px;
width: 56px;
overflow: hidden;
text-align: center;
}
}
}
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="wiki-header-view">
<a-row>
<a-col :span="18">
<span class="logo-box">
<span class="system-name">文档管理系统</span>
<el-divider direction="vertical" style="margin: 0 10px;"/>
<span class="company-name">开源版</span>
</span>
</a-col>
<a-col :span="6" style="text-align: right;">
<UserHead/>
</a-col>
</a-row>
</div>
</template>
<script setup>
import {toRefs, ref, reactive, onMounted, watch, defineEmits, computed} from 'vue';
import {useRouter, useRoute} from "vue-router";
import { message } from 'ant-design-vue';
import UserHead from './UserHead.vue'
let router = useRouter();
onMounted(() => {
});
</script>
<style scoped lang="scss">
.wiki-header-view {
padding: 0 20px;
height: 60px;
line-height: 60px;
background: #2876d7;
.logo-box {
.image {
height: 41px;
vertical-align: middle;
margin-right: 10px;
}
.system-name {
font-size: 21px;
color: #fff;
vertical-align: middle;
margin-top: -2px;
display: inline-block;
}
.company-name {
font-size: 16px;
color: #fff;
vertical-align: middle;
margin-top: -2px;
display: inline-block;
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<a-dropdown trigger="click" placement="bottomRight" arrow overlayClassName="header-action-user-dropdown">
<a-button :icon="h(UserOutlined)" size="large" type="text" style="color: #fff;"></a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="showAbout">关于</a-menu-item>
<a-menu-divider />
<a-menu-item @click="userSignOut" danger>退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<AboutDialog v-model:visible="aboutDialogVisible"/>
</template>
<script setup>
import {UserOutlined} from '@ant-design/icons-vue';
import {toRefs, ref, reactive, onMounted, watch, defineEmits, h, computed} from 'vue';
import {useRouter, useRoute} from "vue-router";
import userApi from "@/assets/api/user";
import AboutDialog from "@/views/common/AboutDialog.vue";
let router = useRouter();
const userSignOut = () => {
userApi.userLogout().then(() => {
location.reload();
});
}
let aboutDialogVisible = ref(false);
const showAbout = () => {
aboutDialogVisible.value = true;
}
</script>
<style lang="scss">
.header-action-user-dropdown {
width: 120px;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="wiki-create-row">
<a-row>
<a-col :span="12"></a-col>
<a-col :span="12" style="text-align: right;">
<a-button @click="showCreateSpace" :icon="h(PlusOutlined)" type="primary">新建空间</a-button>
</a-col>
</a-row>
</div>
<CreateSpace ref="createSpaceRef"></CreateSpace>
</template>
<script setup>
import {SwapOutlined, PlusOutlined} from '@ant-design/icons-vue';
import {toRefs, ref, reactive, onMounted, watch, defineEmits, h, computed} from 'vue';
import CreateSpace from './CreateSpace.vue'
import {useStoreDisplay} from "@/store/wikiDisplay";
import {useStoreUserData} from "@/store/userData";
let storeUser = useStoreUserData();
let storeDisplay = useStoreDisplay();
onMounted(() => {
});
let createSpaceRef = ref();
const showCreateSpace = () => {
createSpaceRef.value.show();
}
</script>
<style lang="scss" scoped>
.wiki-create-row {
margin: 20px 0 10px 0;
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<!--新建空间弹窗-->
<a-modal :title="newSpaceForm.id?'编辑空间':'新建空间'" v-model:open="newSpaceDialogVisible" width="600px" :maskClosable="false" class="create-space-vue">
<a-form :model="newSpaceForm" :rules="newSpaceFormRules" ref="newSpaceFormRef" @submit.prevent :label-col="{span: 5}" :wrapper-col="{span: 19}">
<a-form-item label="空间名" name="name">
<a-input v-model:value="newSpaceForm.name" placeholder="请输入空间名" :maxlength="25"></a-input>
</a-form-item>
<a-form-item label="空间描述" name="spaceExplain">
<a-textarea v-model:value="newSpaceForm.spaceExplain" placeholder="请输入空间简介" :maxlength="250" :autoSize="{ minRows: 6}" type="textarea" resize="none"/>
</a-form-item>
<a-form-item label="空间类型">
<a-radio-group v-model:value="newSpaceForm.type">
<a-radio :value="2">仅空间成员可见</a-radio>
<a-radio :value="1">所有登录用户可见</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="空间唯一编码" name="uuid">
<a-input v-model:value="newSpaceForm.uuid" placeholder="请输入空间唯一编码" :disabled="!!newSpaceForm.id" :maxlength="30"/>
</a-form-item>
</a-form>
<template #footer>
<a-button @click="onNewSpaceCancel">取消</a-button>
<a-button type="primary" v-if="newSpaceForm.id" @click="onNewSpaceSubmit()" :loading="newSpaceSubmitLoading">保存修改</a-button>
<a-button type="primary" v-else @click="onNewSpaceSubmit()" :loading="newSpaceSubmitLoading">立即创建</a-button>
</template>
</a-modal>
</template>
<script setup>
import pageApi from '@/assets/api/page'
import { message } from 'ant-design-vue';
import {toRefs, ref, defineExpose, reactive, onMounted, watch, defineEmits, computed} from 'vue';
import { useStoreSpaceData } from '@/store/spaceData.js'
let storeSpaceData = useStoreSpaceData();
let newSpaceDialogVisible = ref(false);
let newSpaceForm = ref({id: '', name: '', spaceExplain: '', treeLazyLoad: 0, openDoc: 0, uuid: '', type: 1});
let newSpaceFormRules = computed(() => ({
name: [
{required: true, message: '请输入空间名', trigger: 'blur'},
{max: 25, message: '最多25个字符', trigger: 'blur'},
],
uuid: [{
required: false,
trigger: 'blur',
validator: (rule, value, callback) => {
if (!value || /^[a-zA-Z0-9-_]+$/.test(value)) {
callback();
}
callback(new Error('空间编码只能由大小写字母、数字、中横线和下划线组成'));
}
}],
}));
const show = (spaceId) => {
newSpaceForm.value = {id: '', name: '', spaceExplain: '', treeLazyLoad: 0, openDoc: 0, uuid: '', type: 2};
let editSpaceId = spaceId || '';
if (!!editSpaceId) {
pageApi.spaceDetail({id: editSpaceId}).then(json => {
newSpaceForm.value = json.data || {};
newSpaceForm.value.groupId = newSpaceForm.value.groupId || '';
});
}
newSpaceDialogVisible.value = true;
}
defineExpose({show});
let newSpaceFormRef = ref();
let newSpaceSubmitLoading = ref(false);
const onNewSpaceSubmit = () => {
newSpaceFormRef.value.validate().then(() => {
let param = {
id: newSpaceForm.value.id,
name: newSpaceForm.value.name,
type: newSpaceForm.value.type,
uuid: newSpaceForm.value.uuid,
openDoc: newSpaceForm.value.openDoc,
groupId: newSpaceForm.value.groupId,
spaceExplain: newSpaceForm.value.spaceExplain,
treeLazyLoad: newSpaceForm.value.treeLazyLoad,
};
newSpaceSubmitLoading.value = true;
pageApi.updateSpace(param).then(json => {
message.success(newSpaceForm.value.id ? '修改成功' : '创建成功');
newSpaceSubmitLoading.value = false;
newSpaceDialogVisible.value = false;
storeSpaceData.eventSpaceUpdate = !storeSpaceData.eventSpaceUpdate;
}).catch(() => {
newSpaceSubmitLoading.value = false;
});
});
}
const onNewSpaceCancel = () => {
newSpaceDialogVisible.value = false;
}
</script>
<style lang="scss">
.create-space-vue {
.warning-icon {
height: 32px;
line-height: 32px;
margin-right: 5px;
margin-top: 1px;
}
}
</style>

View File

@@ -0,0 +1,177 @@
<template>
<div @click="openSpace(space)" class="wiki-space-card can-click black-bg">
<div class="top-badge external" v-if="space.openDoc === 1">互联网公开</div>
<div class="top-badge open" v-else-if="space.type === 1">企业公开</div>
<div class="img-box">
<img src="@/assets/img/space-bg-1.png"/>
</div>
<div class="title ellipsis-multi" :title="space.name">{{ space.name }}</div>
<div class="desc ellipsis-multi" :title="space.spaceExplain">{{ space.spaceExplain }}</div>
<div v-if="space.isManager" @click.stop="openSpaceSetting(space)" class="open-space-box">
<el-icon style="vertical-align: middle;"><ElIconSetting/></el-icon> 空间设置
</div>
<div v-else @click.stop="openSpace(space)" class="open-space-box">
进入空间 <el-icon style="vertical-align: middle;"><ElIconArrowRight/></el-icon>
</div>
</div>
</template>
<script setup>
import {Setting as ElIconSetting, ArrowRight as ElIconArrowRight} from '@element-plus/icons-vue'
import {toRefs, ref, reactive, onMounted, watch, defineProps, defineEmits, h, computed} from 'vue';
import { message } from 'ant-design-vue';
import {useRouter, useRoute} from "vue-router";
import pageApi from "@/assets/api/page";
import {useStorePageData} from "@/store/pageData";
const router = useRouter();
let storePage = useStorePageData();
const props = defineProps({
space: {type: Object},
});
const openSpace = (row) => {
storePage.currentSpace = {};
router.push({path: `/view/${row.id}`});
}
const openSpaceSetting = (row) => {
storePage.currentSpace = {};
router.push({path: `/view/setting/${row.id}`});
}
</script>
<style lang="scss" scoped>
.wiki-space-card {
position: relative;
width: 140px;
height: 200px;
margin: 0 20px 20px 0;
cursor: pointer;
padding: 16px;
flex-shrink: 0;
.top-badge {
position: absolute;
border-radius: 8px 0 8px 0;
left: 0;
top: 0;
z-index: 1;
background: #1384e5;
padding: 1px 10px;
font-size: 12px;
color: #fff;
}
.top-badge.open {
background: #67C23A;
}
.top-badge.external {
background: #409EFF;
}
.img-box {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
-webkit-transition: all .3s ease-out;
-o-transition: all .3s ease-out;
transition: all .3s ease-out;
-webkit-transform-origin: center center;
-ms-transform-origin: center center;
transform-origin: center center;
img {
width: 100%;
height: 100%;
border-radius: 8px;
}
img.hide-compute-img {
width: 1px;
height: 1px;
opacity: 0;
position: absolute;
}
}
.ellipsis-multi {
display: -webkit-box;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: break-word;
}
.title {
position: relative;
font-weight: 600;
font-size: 16px;
line-height: 24px;
margin-top: 12px;
letter-spacing: normal;
}
.desc {
position: relative;
font-size: 12px;
line-height: 18px;
font-weight: 400;
margin: 6px 0;
letter-spacing: .5px;
}
.open-space-box {
display: none;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 8px 0;
background: #00000055;
color: #fff;
font-size: 13px;
vertical-align: middle;
transition: all .3s ease-out;
border-radius: 0 0 8px 8px;
}
}
.wiki-space-card.white-bg {
color: #000;
}
.wiki-space-card.black-bg {
color: #fff;
}
.wiki-space-card.can-click:hover {
cursor: pointer;
box-shadow: var(--el-box-shadow-light);
.img-box:before {
content: "";
position: absolute;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-box-shadow: 0 0 0 2px #0cabed;
box-shadow: 0 0 0 2px #0cabed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
border-radius: 8px;
}
.open-space-box {
display: block;
}
}
</style>

View File

@@ -0,0 +1,67 @@
<template>
<div class="search-space-box">
<a-input v-model:value="searchValue" @input="searchSpaceInput" class="search-space-input" placeholder="搜索空间" clearable style="width: 250px;">
<template #prefix>
<SearchOutlined style="color: #aaa;"/>
</template>
</a-input>
</div>
<div v-if="storeSpaceData.spaceList.length <= 0" class="wiki-whole-empty-box">
<el-empty description="暂无空间"/>
</div>
<div v-else class="wiki-whole-box">
<SpaceCard :space="item" v-for="item in storeSpaceData.spaceList"></SpaceCard>
</div>
</template>
<script setup>
import {SearchOutlined, SettingOutlined} from '@ant-design/icons-vue';
import {toRefs, ref, reactive, onMounted, watch, h, defineProps, defineEmits, computed} from 'vue';
import SpaceCard from './SpaceCard.vue'
import {useStoreSpaceData} from "@/store/spaceData";
let storeSpaceData = useStoreSpaceData();
onMounted(() => {
});
let searchValue = ref('');
const searchSpaceInput = () => {
if (!searchValue.value) {
storeSpaceData.spaceList = storeSpaceData.wholeSpaceList;
} else {
let searchText = searchValue.value.toLowerCase();
storeSpaceData.spaceList = storeSpaceData.wholeSpaceList.filter(item => item.searchText.indexOf(searchText) >= 0);
}
}
</script>
<style lang="scss">
.search-space-box {
width: 100%;
background: #fff;
padding: 20px;
box-sizing: border-box;
margin-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
text-align: right;
.search-space-input {
max-width: 300px;
}
}
.wiki-whole-box {
padding: 0 20px;
display: flex;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-ms-flex-line-pack: start;
align-content: flex-start;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
}
</style>