This commit is contained in:
2024-03-01 20:28:14 +08:00
commit 076c21dc36
491 changed files with 84482 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
import WikiPage from './wikiPage/index';
// const WikiPage = require('./wikiPage/index')
module.exports = function() {
this.bindHook('sub_nav', function(app) {
app.wiki = {
name: 'Wiki',
path: '/project/:id/wiki',
component: WikiPage
};
});
};

View File

@@ -0,0 +1,234 @@
const baseController = require('controllers/base.js');
const wikiModel = require('./wikiModel.js');
const projectModel = require('models/project.js');
const userModel = require('models/user.js');
const jsondiffpatch = require('jsondiffpatch');
const formattersHtml = jsondiffpatch.formatters.html;
const yapi = require('yapi.js');
// const util = require('./util.js');
const fs = require('fs-extra');
const path = require('path');
const showDiffMsg = require('../../common/diff-view.js');
class wikiController extends baseController {
constructor(ctx) {
super(ctx);
this.Model = yapi.getInst(wikiModel);
this.projectModel = yapi.getInst(projectModel);
}
/**
* 获取wiki信息
* @interface wiki_desc/get
* @method get
* @category statistics
* @foldnumber 10
* @returns {Object}
*/
async getWikiDesc(ctx) {
try {
let project_id = ctx.request.query.project_id;
if (!project_id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
}
let result = await this.Model.get(project_id);
return (ctx.body = yapi.commons.resReturn(result));
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
/**
* 保存wiki信息
* @interface wiki_desc/get
* @method get
* @category statistics
* @foldnumber 10
* @returns {Object}
*/
async uplodaWikiDesc(ctx) {
try {
let params = ctx.request.body;
params = yapi.commons.handleParams(params, {
project_id: 'number',
desc: 'string',
markdown: 'string'
});
if (!params.project_id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
}
if (!this.$tokenAuth) {
let auth = await this.checkAuth(params.project_id, 'project', 'edit');
if (!auth) {
return (ctx.body = yapi.commons.resReturn(null, 400, '没有权限'));
}
}
let notice = params.email_notice;
delete params.email_notice;
const username = this.getUsername();
const uid = this.getUid();
// 如果当前数据库里面没有数据
let result = await this.Model.get(params.project_id);
if (!result) {
let data = Object.assign(params, {
username,
uid,
add_time: yapi.commons.time(),
up_time: yapi.commons.time()
});
let res = await this.Model.save(data);
ctx.body = yapi.commons.resReturn(res);
} else {
let data = Object.assign(params, {
username,
uid,
up_time: yapi.commons.time()
});
let upRes = await this.Model.up(result._id, data);
ctx.body = yapi.commons.resReturn(upRes);
}
let logData = {
type: 'wiki',
project_id: params.project_id,
current: params.desc,
old: result ? result.toObject().desc : ''
};
let wikiUrl = `${ctx.request.origin}/project/${params.project_id}/wiki`;
if (notice) {
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, logData);
let annotatedCss = fs.readFileSync(
path.resolve(
yapi.WEBROOT,
'node_modules/jsondiffpatch/dist/formatters-styles/annotated.css'
),
'utf8'
);
let htmlCss = fs.readFileSync(
path.resolve(yapi.WEBROOT, 'node_modules/jsondiffpatch/dist/formatters-styles/html.css'),
'utf8'
);
let project = await this.projectModel.getBaseInfo(params.project_id);
yapi.commons.sendNotice(params.project_id, {
title: `${username} 更新了wiki说明`,
content: `<html>
<head>
<meta charset="utf-8" />
<style>
${annotatedCss}
${htmlCss}
</style>
</head>
<body>
<div><h3>${username}更新了wiki说明</h3>
<p>修改用户: ${username}</p>
<p>修改项目: <a href="${wikiUrl}">${project.name}</a></p>
<p>详细改动日志: ${this.diffHTML(diffView)}</p></div>
</body>
</html>`
});
}
// 保存修改日志信息
yapi.commons.saveLog({
content: `<a href="/user/profile/${uid}">${username}</a> 更新了 <a href="${wikiUrl}">wiki</a> 的信息`,
type: 'project',
uid,
username: username,
typeid: params.project_id,
data: logData
});
return 1;
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
diffHTML(html) {
if (html.length === 0) {
return `<span style="color: #555">没有改动该操作未改动wiki数据</span>`;
}
return html.map(item => {
return `<div>
<h4 class="title">${item.title}</h4>
<div>${item.content}</div>
</div>`;
});
}
// 处理编辑冲突
async wikiConflict(ctx) {
try {
let result;
ctx.websocket.on('message', async message => {
let id = parseInt(ctx.query.id, 10);
if (!id) {
return ctx.websocket.send('id 参数有误');
}
result = await this.Model.get(id);
let data = await this.websocketMsgMap(message, result);
if (data) {
ctx.websocket.send(JSON.stringify(data));
}
});
ctx.websocket.on('close', async () => {});
} catch (err) {
yapi.commons.log(err, 'error');
}
}
websocketMsgMap(msg, result) {
const map = {
start: this.startFunc.bind(this),
end: this.endFunc.bind(this),
editor: this.editorFunc.bind(this)
};
return map[msg](result);
}
// socket 开始链接
async startFunc(result) {
if (result && result.edit_uid === this.getUid()) {
await this.Model.upEditUid(result._id, 0);
}
}
// socket 结束链接
async endFunc(result) {
if (result) {
await this.Model.upEditUid(result._id, 0);
}
}
// 正在编辑
async editorFunc(result) {
let userInst, userinfo, data;
if (result && result.edit_uid !== 0 && result.edit_uid !== this.getUid()) {
userInst = yapi.getInst(userModel);
userinfo = await userInst.findById(result.edit_uid);
data = {
errno: result.edit_uid,
data: { uid: result.edit_uid, username: userinfo.username }
};
} else {
if (result) {
await this.Model.upEditUid(result._id, this.getUid());
}
data = {
errno: 0,
data: result
};
}
return data;
}
}
module.exports = wikiController;

View File

@@ -0,0 +1,64 @@
module.exports = {
server: true,
client: true,
httpCodes: [
100,
101,
102,
200,
201,
202,
203,
204,
205,
206,
207,
208,
226,
300,
301,
302,
303,
304,
305,
307,
308,
400,
401,
402,
403,
404,
405,
406,
407,
408,
409,
410,
411,
412,
413,
414,
415,
416,
417,
418,
422,
423,
424,
426,
428,
429,
431,
500,
501,
502,
503,
504,
505,
506,
507,
508,
510,
511
]
};

View File

@@ -0,0 +1,39 @@
const yapi = require('yapi.js');
const mongoose = require('mongoose');
const controller = require('./controller');
module.exports = function() {
yapi.connect.then(function() {
let Col = mongoose.connection.db.collection('wiki');
Col.createIndex({
project_id: 1
});
});
this.bindHook('add_router', function(addRouter) {
addRouter({
// 获取wiki信息
controller: controller,
method: 'get',
path: 'wiki_desc/get',
action: 'getWikiDesc'
});
addRouter({
// 更新wiki信息
controller: controller,
method: 'post',
path: 'wiki_desc/up',
action: 'uplodaWikiDesc'
});
});
this.bindHook('add_ws_router', function(wsRouter) {
wsRouter({
controller: controller,
method: 'get',
path: 'wiki_desc/solve_conflict',
action: 'wikiConflict'
});
});
};

View File

@@ -0,0 +1,25 @@
// 时间
const convert2Decimal = num => (num > 9 ? num : `0${num}`);
/**
* 格式化 年、月、日、时、分、秒
* @param val {Object or String or Number} 日期对象 或是可new Date的对象或时间戳
* @return {String} 2017-01-20 20:00:00
*/
exports.formatDate = val => {
let date = val;
if (typeof val !== 'object') {
date = new Date(val);
}
return `${[
date.getFullYear(),
convert2Decimal(date.getMonth() + 1),
convert2Decimal(date.getDate())
].join('-')} ${[
convert2Decimal(date.getHours()),
convert2Decimal(date.getMinutes()),
convert2Decimal(date.getSeconds())
].join(':')}`;
};
// const json5_parse = require('../client/common.js').json5_parse;

View File

@@ -0,0 +1,56 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
class statisMockModel extends baseModel {
getName() {
return 'wiki';
}
getSchema() {
return {
project_id: { type: Number, required: true },
username: String,
uid: { type: Number, required: true },
edit_uid: { type: Number, default: 0 },
desc: String,
markdown: String,
add_time: Number,
up_time: Number
};
}
save(data) {
let m = new this.model(data);
return m.save();
}
get(project_id) {
return this.model
.findOne({
project_id: project_id
})
.exec();
}
up(id, data) {
return this.model.update(
{
_id: id
},
data,
{ runValidators: true }
);
}
upEditUid(id, uid) {
return this.model.update(
{
_id: id
},
{ edit_uid: uid },
{ runValidators: true }
);
}
}
module.exports = statisMockModel;

View File

@@ -0,0 +1,67 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Checkbox } from 'antd';
import Editor from 'common/tui-editor/dist/tui-editor-Editor-all.min.js';
require('common/tui-editor/dist/tui-editor.min.css'); // editor ui
require('common/tui-editor/dist/tui-editor-contents.min.css'); // editor content
class WikiEditor extends Component {
constructor(props) {
super(props);
}
static propTypes = {
isConflict: PropTypes.bool,
onUpload: PropTypes.func,
onCancel: PropTypes.func,
notice: PropTypes.bool,
onEmailNotice: PropTypes.func,
desc: PropTypes.string
};
componentDidMount() {
this.editor = new Editor({
el: document.querySelector('#desc'),
initialEditType: 'wysiwyg',
height: '500px',
initialValue: this.props.desc
});
}
onUpload = () => {
let desc = this.editor.getHtml();
let markdown = this.editor.getMarkdown();
this.props.onUpload(desc, markdown);
};
render() {
const { isConflict, onCancel, notice, onEmailNotice } = this.props;
return (
<div>
<div
id="desc"
className="wiki-editor"
style={{ display: !isConflict ? 'block' : 'none' }}
/>
<div className="wiki-title wiki-up">
<Button
icon="upload"
type="primary"
className="upload-btn"
disabled={isConflict}
onClick={this.onUpload}
>
更新
</Button>
<Button onClick={onCancel} className="upload-btn">
取消
</Button>
<Checkbox checked={notice} onChange={onEmailNotice}>
通知相关人员
</Checkbox>
</div>
</div>
);
}
}
export default WikiEditor;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'antd';
import { Link } from 'react-router-dom';
const WikiView = props => {
const { editorEable, onEditor, uid, username, editorTime, desc } = props;
return (
<div className="wiki-view-content">
<div className="wiki-title">
<Button icon="edit" onClick={onEditor} disabled={!editorEable}>
编辑
</Button>
{username && (
<div className="wiki-user">
{' '}
<Link className="user-name" to={`/user/profile/${uid || 11}`}>
{username}
</Link>{' '}
修改于 {editorTime}
</div>
)}
</div>
<div
className="tui-editor-contents"
dangerouslySetInnerHTML={{ __html: desc }}
/>
</div>
);
};
WikiView.propTypes = {
editorEable: PropTypes.bool,
onEditor: PropTypes.func,
uid: PropTypes.number,
username: PropTypes.string,
editorTime: PropTypes.string,
desc: PropTypes.string
};
export default WikiView;

View File

@@ -0,0 +1,255 @@
import React, { Component } from 'react';
import { message } from 'antd';
import { connect } from 'react-redux';
import axios from 'axios';
import PropTypes from 'prop-types';
import './index.scss';
import { timeago } from '../../../common/utils';
import { Link } from 'react-router-dom';
import WikiView from './View.js';
import WikiEditor from './Editor.js';
@connect(
state => {
return {
projectMsg: state.project.currProject
};
},
{}
)
class WikiPage extends Component {
constructor(props) {
super(props);
this.state = {
isEditor: false,
isUpload: true,
desc: '',
markdown: '',
notice: props.projectMsg.switch_notice,
status: 'INIT',
editUid: '',
editName: '',
curdata: null
};
}
static propTypes = {
match: PropTypes.object,
projectMsg: PropTypes.object
};
async componentDidMount() {
const currProjectId = this.props.match.params.id;
await this.handleData({ project_id: currProjectId });
this.handleConflict();
}
componentWillUnmount() {
// willUnmount
try {
if (this.state.status === 'CLOSE') {
this.WebSocket.send('end');
this.WebSocket.close();
}
} catch (e) {
return null;
}
}
// 结束编辑websocket
endWebSocket = () => {
try {
if (this.state.status === 'CLOSE') {
const sendEnd = () => {
this.WebSocket.send('end');
};
this.handleWebsocketAccidentClose(sendEnd);
}
} catch (e) {
return null;
}
};
// 处理多人编辑冲突问题
handleConflict = () => {
// console.log(location)
let domain = location.hostname + (location.port !== '' ? ':' + location.port : '');
let s;
//因后端 node 仅支持 ws 暂不支持 wss
let wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
s = new WebSocket(
wsProtocol +
'://' +
domain +
'/api/ws_plugin/wiki_desc/solve_conflict?id=' +
this.props.match.params.id
);
s.onopen = () => {
this.WebSocket = s;
s.send('start');
};
s.onmessage = e => {
let result = JSON.parse(e.data);
if (result.errno === 0) {
// 更新
if (result.data) {
this.setState({
// curdata: result.data,
desc: result.data.desc,
username: result.data.username,
uid: result.data.uid,
editorTime: timeago(result.data.up_time)
});
}
// 新建
this.setState({
isEditor: !this.state.isEditor,
status: 'CLOSE'
});
} else {
this.setState({
editUid: result.data.uid,
editName: result.data.username,
status: 'EDITOR'
});
}
};
s.onerror = () => {
this.setState({
status: 'CLOSE'
});
console.warn('websocket 连接失败,将导致多人编辑同一个接口冲突。');
};
};
// 点击编辑按钮 发送 websocket 获取数据
onEditor = () => {
// this.WebSocket.send('editor');
const sendEditor = () => {
this.WebSocket.send('editor');
};
this.handleWebsocketAccidentClose(sendEditor, status => {
// 如果websocket 启动不成功用户依旧可以对wiki 进行编辑
if (!status) {
this.setState({
isEditor: !this.state.isEditor
});
}
});
};
// 处理websocket 意外断开问题
handleWebsocketAccidentClose = (fn, callback) => {
// websocket 是否启动
if (this.WebSocket) {
// websocket 断开
if (this.WebSocket.readyState !== 1) {
message.error('websocket 链接失败,请重新刷新页面');
} else {
fn();
}
callback(true);
} else {
callback(false);
}
};
// 获取数据
handleData = async params => {
let result = await axios.get('/api/plugin/wiki_desc/get', { params });
if (result.data.errcode === 0) {
const data = result.data.data;
if (data) {
this.setState({
desc: data.desc,
markdown: data.markdown,
username: data.username,
uid: data.uid,
editorTime: timeago(data.up_time)
});
}
} else {
message.error(`请求数据失败: ${result.data.errmsg}`);
}
};
// 数据上传
onUpload = async (desc, markdown) => {
const currProjectId = this.props.match.params.id;
let option = {
project_id: currProjectId,
desc,
markdown,
email_notice: this.state.notice
};
let result = await axios.post('/api/plugin/wiki_desc/up', option);
if (result.data.errcode === 0) {
await this.handleData({ project_id: currProjectId });
this.setState({ isEditor: false });
} else {
message.error(`更新失败: ${result.data.errmsg}`);
}
this.endWebSocket();
// this.WebSocket.send('end');
};
// 取消编辑
onCancel = () => {
this.setState({ isEditor: false });
this.endWebSocket();
};
// 邮件通知
onEmailNotice = e => {
this.setState({
notice: e.target.checked
});
};
render() {
const { isEditor, username, editorTime, notice, uid, status, editUid, editName } = this.state;
const editorEable =
this.props.projectMsg.role === 'admin' ||
this.props.projectMsg.role === 'owner' ||
this.props.projectMsg.role === 'dev';
const isConflict = status === 'EDITOR';
return (
<div className="g-row">
<div className="m-panel wiki-content">
<div className="wiki-content">
{isConflict && (
<div className="wiki-conflict">
<Link to={`/user/profile/${editUid || uid}`}>
<b>{editName || username}</b>
</Link>
<span>正在编辑该wiki请稍后再试...</span>
</div>
)}
</div>
{!isEditor ? (
<WikiView
editorEable={editorEable}
onEditor={this.onEditor}
uid={uid}
username={username}
editorTime={editorTime}
desc={this.state.desc}
/>
) : (
<WikiEditor
isConflict={isConflict}
onUpload={this.onUpload}
onCancel={this.onCancel}
notice={notice}
onEmailNotice={this.onEmailNotice}
desc={this.state.desc}
/>
)}
</div>
</div>
);
}
}
export default WikiPage;

View File

@@ -0,0 +1,27 @@
.wiki-content {
.wiki-user {
padding-top: 8px;
}
.wiki-editor {
padding-top: 16px;
}
.upload-btn {
margin-right: 16px;
}
.wiki-conflict {
text-align: center;
font-size: 14px;
padding-top: 10px;
}
.wiki-up {
text-align: right;
padding-top: 16px;
}
.wiki-title {
padding-bottom: 16px;
}
}