edit.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. <template>
  2. <div class="w-main docs-edit">
  3. <v-title>{{$L('文档编辑')}}-{{$L('轻量级的团队在线协作')}}</v-title>
  4. <div class="edit-box">
  5. <div class="edit-header">
  6. <div class="header-menu active" @click="handleClick('back')"><Icon type="md-arrow-back" /></div>
  7. <div class="header-menu" @click="handleClick('menu')"><Icon type="md-menu" /></div>
  8. <!--<div class="header-menu" @click="handleClick('share')"><Icon type="md-share" /></div>
  9. <div class="header-menu" @click="handleClick('view')"><Icon type="md-eye" /></div>-->
  10. <div class="header-menu" @click="handleClick('history')"><Icon type="md-time" /></div>
  11. <Poptip class="header-menu synch">
  12. <Icon type="md-contacts" :title="$L('正在协作会员')"/><em v-if="synchUsers.length > 0">{{synchUsers.length}}</em>
  13. <ul class="synch-lists" slot="content">
  14. <li class="title">{{$L('正在协作会员')}}:</li>
  15. <li v-for="item in synchUsersS">
  16. <img class="synch-userimg" :src="item.userimg"/>
  17. <user-view class="synch-username" placement="right" :username="item.username"/>
  18. </li>
  19. </ul>
  20. </Poptip>
  21. <div class="header-title">{{docDetail.title}}</div>
  22. <div v-if="docDetail.type=='document'" class="header-hint">
  23. <ButtonGroup size="small" shape="circle">
  24. <Button :type="`${docContent.type!='md'?'primary':'default'}`" @click="$set(docContent, 'type', 'text')">{{$L('文本编辑器')}}</Button>
  25. <Button :type="`${docContent.type=='md'?'primary':'default'}`" @click="$set(docContent, 'type', 'md')">{{$L('MD编辑器')}}</Button>
  26. </ButtonGroup>
  27. </div>
  28. <div v-else-if="docDetail.type=='mind'" class="header-hint">{{$L('选中节点,按enter键添加子节点,tab键添加同级节点')}}</div>
  29. <Button :disabled="(disabledBtn || loadIng > 0) && hid == 0" class="header-button" size="small" type="primary" @click="handleClick('save')">{{$L('保存')}}</Button>
  30. </div>
  31. <div class="docs-body">
  32. <template v-if="docDetail.type=='document'">
  33. <MDEditor v-if="docContent.type=='md'" class="body-text" v-model="docContent.content" height="100%"></MDEditor>
  34. <TEditor v-else class="body-text" v-model="docContent.content" height="100%"></TEditor>
  35. </template>
  36. <minder v-else-if="docDetail.type=='mind'" class="body-mind" v-model="docContent"></minder>
  37. <sheet v-else-if="docDetail.type=='sheet'" class="body-sheet" v-model="docContent.content"></sheet>
  38. <flow v-else-if="docDetail.type=='flow'" class="body-flow" v-model="docContent.content"></flow>
  39. </div>
  40. </div>
  41. <WDrawer v-model="docDrawerShow" maxWidth="450">
  42. <Tabs v-if="docDrawerShow" v-model="docDrawerTab">
  43. <TabPane :label="$L('知识库目录')" name="menu">
  44. <nested-draggable :lists="sectionLists" :readonly="true" :activeid="sid" @change="handleSection"></nested-draggable>
  45. <div v-if="sectionLists.length == 0" style="color:#888;padding:32px;text-align:center">{{sectionNoDataText}}</div>
  46. </TabPane>
  47. <TabPane :label="$L('文档历史版本')" name="history">
  48. <Table class="tableFill" :columns="historyColumns" :data="historyLists" :no-data-text="historyNoDataText" size="small" stripe></Table>
  49. </TabPane>
  50. </Tabs>
  51. </WDrawer>
  52. </div>
  53. </template>
  54. <style lang="scss">
  55. .docs-edit {
  56. .body-text {
  57. .mdeditor-box {
  58. position: relative;
  59. width: 100%;
  60. .markdown {
  61. position: absolute;
  62. top: 0;
  63. left: 0;
  64. bottom: 0;
  65. right: 0;
  66. overflow: auto;
  67. transform: translateZ(0);
  68. &.border {
  69. border: 0 !important;
  70. }
  71. }
  72. }
  73. .teditor-loadedstyle {
  74. .tox-tinymce {
  75. border: 0;
  76. border-radius: 0;
  77. }
  78. .tox-mbtn {
  79. height: 28px;
  80. }
  81. .tox-menubar,
  82. .tox-toolbar-overlord {
  83. padding: 0 12%;
  84. background: #f9f9f9;
  85. }
  86. .tox-toolbar__overflow,
  87. .tox-toolbar__primary {
  88. background: none !important;
  89. border-top: 1px solid #eaeaea !important;
  90. }
  91. .tox-toolbar-overlord {
  92. border-bottom: 1px solid #E9E9E9 !important;
  93. }
  94. .tox-toolbar__group:not(:last-of-type) {
  95. border-right: 1px solid #eaeaea !important;
  96. }
  97. .tox-sidebar-wrap {
  98. margin: 22px 12%;
  99. border: 1px solid #e8e8e8;
  100. border-radius: 2px;
  101. box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.08);
  102. .tox-edit-area {
  103. border-top: 0;
  104. }
  105. }
  106. .tox-statusbar {
  107. border-top: 1px solid #E9E9E9;
  108. .tox-statusbar__resize-handle {
  109. display: none;
  110. }
  111. }
  112. }
  113. }
  114. .body-sheet {
  115. box-sizing: content-box;
  116. * {
  117. box-sizing: content-box;
  118. }
  119. }
  120. }
  121. </style>
  122. <style lang="scss" scoped>
  123. .docs-edit {
  124. .edit-box {
  125. display: flex;
  126. flex-direction: column;
  127. position: absolute;
  128. width: 100%;
  129. height: 100%;
  130. .edit-header {
  131. display: flex;
  132. flex-direction: row;
  133. align-items: center;
  134. width: 100%;
  135. height: 38px;
  136. background-color: #ffffff;
  137. box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.1);
  138. position: relative;
  139. z-index: 9;
  140. .header-menu {
  141. width: 50px;
  142. height: 100%;
  143. text-align: center;
  144. display: flex;
  145. align-items: center;
  146. justify-content: center;
  147. margin-right: 3px;
  148. cursor: pointer;
  149. color: #777777;
  150. position: relative;
  151. .ivu-icon {
  152. font-size: 16px;
  153. }
  154. &.synch {
  155. em {
  156. padding-left: 2px;
  157. }
  158. }
  159. &:hover,
  160. &.active {
  161. color: #fff;
  162. background: #059DFD;
  163. }
  164. .synch-lists {
  165. max-height: 500px;
  166. overflow: auto;
  167. li {
  168. display: flex;
  169. align-items: center;
  170. padding: 6px 0;
  171. border-bottom: 1px dashed #eeeeee;
  172. &.title {
  173. font-size: 14px;
  174. font-weight: 600;
  175. color: #333333;
  176. }
  177. .synch-userimg {
  178. width: 24px;
  179. height: 24px;
  180. border-radius: 50%;
  181. }
  182. .synch-username {
  183. padding-left: 8px;
  184. font-size: 14px;
  185. color: #555555;
  186. }
  187. }
  188. }
  189. }
  190. .header-title {
  191. flex: 1;
  192. color: #333333;
  193. border-left: 1px solid #ddd;
  194. margin-left: 5px;
  195. padding-left: 24px;
  196. padding-right: 24px;
  197. font-size: 16px;
  198. white-space: nowrap;
  199. }
  200. .header-hint {
  201. padding-right: 22px;
  202. font-size: 12px;
  203. color: #666;
  204. white-space: nowrap;
  205. .ivu-btn {
  206. font-size: 12px;
  207. padding: 0 10px;
  208. }
  209. }
  210. .header-button {
  211. font-size: 12px;
  212. margin-right: 12px;
  213. }
  214. }
  215. .docs-body {
  216. flex: 1;
  217. width: 100%;
  218. position: relative;
  219. .body-text {
  220. display: flex;
  221. width: 100%;
  222. height: 100%;
  223. .teditor-loadedstyle {
  224. height: 100%;
  225. }
  226. }
  227. }
  228. }
  229. }
  230. </style>
  231. <script>
  232. import Vue from 'vue'
  233. import minder from '../../components/docs/minder'
  234. Vue.use(minder)
  235. const MDEditor = resolve => require(['../../components/MDEditor/index'], resolve);
  236. const TEditor = resolve => require(['../../components/TEditor'], resolve);
  237. const Sheet = resolve => require(['../../components/docs/sheet/index'], resolve);
  238. const Flow = resolve => require(['../../components/docs/flow/index'], resolve);
  239. const NestedDraggable = resolve => require(['../../components/docs/NestedDraggable'], resolve);
  240. const WDrawer = resolve => require(['../../components/iview/WDrawer'], resolve);
  241. export default {
  242. components: {WDrawer, Flow, Sheet, MDEditor, TEditor, NestedDraggable},
  243. data () {
  244. return {
  245. loadIng: 0,
  246. sid: 0,
  247. hid: 0,
  248. docDetail: { },
  249. docContent: { },
  250. bakContent: null,
  251. docDrawerShow: false,
  252. docDrawerTab: '',
  253. sectionLists: [],
  254. sectionNoDataText: "",
  255. historyColumns: [],
  256. historyLists: [],
  257. historyNoDataText: "",
  258. userInfo: {},
  259. routeName: '',
  260. synergyNum: 0,
  261. synchUsers: [],
  262. }
  263. },
  264. created() {
  265. this.historyColumns = [{
  266. "title": this.$L("存档日期"),
  267. "minWidth": 160,
  268. "maxWidth": 200,
  269. render: (h, params) => {
  270. return h('span', $A.formatDate("Y-m-d H:i:s", params.row.indate));
  271. }
  272. }, {
  273. "title": this.$L("操作员"),
  274. "key": 'username',
  275. "minWidth": 80,
  276. "maxWidth": 130,
  277. render: (h, params) => {
  278. return h('UserView', {
  279. props: {
  280. username: params.row.username
  281. }
  282. });
  283. }
  284. }, {
  285. "title": " ",
  286. "key": 'action',
  287. "width": 80,
  288. "align": 'center',
  289. render: (h, params) => {
  290. if (this.hid == params.row.id || (this.hid == 0 && params.index == 0)) {
  291. return h('Icon', {
  292. props: { type: 'md-checkmark' },
  293. style: { marginRight: '6px', fontSize: '16px', color: '#FF5722' },
  294. });
  295. }
  296. return h('Button', {
  297. props: {
  298. type: 'text',
  299. size: 'small'
  300. },
  301. style: {
  302. fontSize: '12px'
  303. },
  304. on: {
  305. click: () => {
  306. let data = {sid: this.getSid() + "-" + params.row.id, other: this.$route.params.other}
  307. if (params.index == 0) {
  308. data.sid = this.getSid();
  309. }
  310. this.goForward({name: 'docs-edit', params: data }, true);
  311. this.refreshSid();
  312. this.docDrawerShow = false;
  313. }
  314. }
  315. }, this.$L('还原'));
  316. }
  317. }];
  318. },
  319. mounted() {
  320. this.routeName = this.$route.name;
  321. this.userInfo = $A.getUserInfo((res, isLogin) => {
  322. if (this.userInfo.id != res.id) {
  323. this.userInfo = res;
  324. }
  325. }, false);
  326. //
  327. $A.WSOB.setOnMsgListener("chat/index", ['docs'], (msgDetail) => {
  328. if (this.routeName !== this.$route.name) {
  329. return;
  330. }
  331. let body = msgDetail.body;
  332. if (body.sid != this.sid) {
  333. return;
  334. }
  335. if (body.type === 'users') {
  336. this.synchUsers = body.lists;
  337. this.synchUsers.splice(this.synchUsers.length);
  338. } else if (body.type === 'update') {
  339. this.$Modal.confirm({
  340. title: this.$L("更新提示"),
  341. content: this.$L('团队成员(%)更新了内容,<br/>更新时间:%。<br/><br/>点击【确定】加载最新内容。', body.nickname, $A.formatDate("Y-m-d H:i:s", body.time)),
  342. onOk: () => {
  343. this.refreshDetail();
  344. }
  345. });
  346. }
  347. });
  348. },
  349. activated() {
  350. this.refreshSid();
  351. this.synergy(true);
  352. },
  353. deactivated() {
  354. this.synergy(false);
  355. this.docDrawerShow = false;
  356. if ($A.getToken() === false) {
  357. this.sid = 0;
  358. }
  359. },
  360. watch: {
  361. sid(val) {
  362. if (!val) {
  363. this.goBack();
  364. return;
  365. }
  366. this.hid = $A.runNum($A.strExists(val, '-') ? $A.getMiddle(val, "-", null) : 0);
  367. this.refreshDetail();
  368. },
  369. docDrawerTab(act) {
  370. switch (act) {
  371. case "menu":
  372. if (!this.sectionNoDataText) {
  373. this.sectionNoDataText = this.$L("数据加载中.....");
  374. let bookid = this.docDetail.bookid;
  375. $A.aAjax({
  376. url: 'docs/section/lists',
  377. data: {
  378. bookid: bookid
  379. },
  380. error: () => {
  381. if (bookid != this.docDetail.bookid) {
  382. return;
  383. }
  384. this.sectionNoDataText = this.$L("数据加载失败!");
  385. },
  386. success: (res) => {
  387. if (bookid != this.docDetail.bookid) {
  388. return;
  389. }
  390. if (res.ret === 1) {
  391. this.sectionLists = res.data;
  392. this.sectionNoDataText = this.$L("没有相关的数据");
  393. }else{
  394. this.sectionLists = [];
  395. this.sectionNoDataText = res.msg;
  396. }
  397. }
  398. });
  399. }
  400. break;
  401. case "history":
  402. if (!this.historyNoDataText) {
  403. this.historyNoDataText = this.$L("数据加载中.....");
  404. let sid = this.getSid();
  405. $A.aAjax({
  406. url: 'docs/section/history',
  407. data: {
  408. id: sid,
  409. pagesize: 50
  410. },
  411. error: () => {
  412. if (sid != this.getSid()) {
  413. return;
  414. }
  415. this.historyNoDataText = this.$L("数据加载失败!");
  416. },
  417. success: (res) => {
  418. if (sid != this.getSid()) {
  419. return;
  420. }
  421. if (res.ret === 1) {
  422. this.historyLists = res.data;
  423. this.historyNoDataText = this.$L("没有相关的数据");
  424. }else{
  425. this.historyLists = [];
  426. this.historyNoDataText = res.msg;
  427. }
  428. }
  429. });
  430. }
  431. break;
  432. }
  433. }
  434. },
  435. computed: {
  436. disabledBtn() {
  437. return this.bakContent == $A.jsonStringify(this.docContent);
  438. },
  439. synchUsersS() {
  440. let temp = Math.round(new Date().getTime() / 1000);
  441. return this.synchUsers.filter(item => {
  442. return item.indate + 10 > temp;
  443. });
  444. }
  445. },
  446. methods: {
  447. synergy(enter) {
  448. if (enter === false) {
  449. $A.WSOB.sendTo('docs', {
  450. type: 'quit',
  451. sid: this.sid,
  452. username: this.userInfo.username,
  453. });
  454. } else {
  455. if (this.routeName !== this.$route.name) {
  456. let tmpNum = this.synergyNum;
  457. setTimeout(() => {
  458. if (tmpNum === this.synergyNum) {
  459. this.synergyNum++;
  460. this.synergy();
  461. }
  462. }, 10000);
  463. } else {
  464. $A.WSOB.sendTo('docs', null, {
  465. type: enter === true ? 'enter' : 'refresh',
  466. sid: this.sid,
  467. username: this.userInfo.username,
  468. userimg: this.userInfo.userimg,
  469. indate: Math.round(new Date().getTime() / 1000),
  470. }, (res) => {
  471. this.synchUsers = res.status === 1 ? res.message : [];
  472. let tmpNum = this.synergyNum;
  473. setTimeout(() => {
  474. if (tmpNum === this.synergyNum) {
  475. this.synergyNum++;
  476. this.synergy();
  477. }
  478. }, 10000);
  479. });
  480. }
  481. }
  482. },
  483. refreshSid() {
  484. this.sid = this.$route.params.sid;
  485. if (typeof this.$route.params.other === "object") {
  486. this.$set(this.docDetail, 'title', $A.getObject(this.$route.params.other, 'title'))
  487. }
  488. },
  489. getSid() {
  490. return $A.runNum($A.getMiddle(this.sid, null, '-'));
  491. },
  492. refreshDetail() {
  493. this.docDetail = { };
  494. this.docContent = { };
  495. this.bakContent = null;
  496. this.getDetail();
  497. },
  498. getDetail() {
  499. this.loadIng++;
  500. $A.aAjax({
  501. url: 'docs/section/content',
  502. data: {
  503. id: this.sid,
  504. },
  505. complete: () => {
  506. this.loadIng--;
  507. },
  508. error: () => {
  509. this.goBack();
  510. alert(this.$L('网络繁忙,请稍后再试!'));
  511. },
  512. success: (res) => {
  513. if (res.ret === 1) {
  514. this.docDetail = res.data;
  515. this.docContent = $A.jsonParse(res.data.content);
  516. this.bakContent = $A.jsonStringify(this.docContent);
  517. } else {
  518. this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
  519. this.goBack();
  520. }
  521. }
  522. });
  523. },
  524. handleSection(act, detail) {
  525. if (act === 'open') {
  526. this.goForward({name: 'docs-edit', params: {sid: detail.id, other: detail || {}}}, true);
  527. this.refreshSid();
  528. this.docDrawerShow = false;
  529. }
  530. },
  531. handleClick(act) {
  532. switch (act) {
  533. case "back":
  534. if (this.bakContent == $A.jsonStringify(this.docContent) && this.hid == 0) {
  535. this.goBack();
  536. return;
  537. }
  538. this.$Modal.confirm({
  539. title: this.$L('温馨提示'),
  540. content: this.$L('是否放弃修改的内容返回?'),
  541. loading: true,
  542. cancelText: this.$L('放弃保存'),
  543. onCancel: () => {
  544. this.goBack();
  545. },
  546. okText: this.$L('保存并返回'),
  547. onOk: () => {
  548. this.bakContent = $A.jsonStringify(this.docContent);
  549. $A.aAjax({
  550. url: 'docs/section/save?id=' + this.getSid(),
  551. method: 'post',
  552. data: {
  553. D: Object.assign(this.docDetail, {content: this.bakContent})
  554. },
  555. error: () => {
  556. this.$Modal.remove();
  557. alert(this.$L('网络繁忙,请稍后再试!'));
  558. },
  559. success: (res) => {
  560. this.$Modal.remove();
  561. this.goBack();
  562. setTimeout(() => {
  563. if (res.ret === 1) {
  564. this.$Message.success(res.msg);
  565. this.historyNoDataText = '';
  566. } else {
  567. this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
  568. }
  569. }, 350);
  570. }
  571. });
  572. }
  573. });
  574. break;
  575. case "save":
  576. this.bakContent = $A.jsonStringify(this.docContent);
  577. $A.aAjax({
  578. url: 'docs/section/save?id=' + this.getSid(),
  579. method: 'post',
  580. data: {
  581. D: Object.assign(this.docDetail, {content: this.bakContent})
  582. },
  583. error: () => {
  584. alert(this.$L('网络繁忙,保存失败!'));
  585. },
  586. success: (res) => {
  587. if (res.ret === 1) {
  588. this.$Message.success(res.msg);
  589. this.historyNoDataText = '';
  590. } else {
  591. this.$Modal.error({title: this.$L('温馨提示'), content: res.msg});
  592. }
  593. }
  594. });
  595. break;
  596. case "menu":
  597. case "history":
  598. this.docDrawerTab = act;
  599. this.docDrawerShow = true
  600. break;
  601. case "share":
  602. case "view":
  603. this.$Message.info(this.$L("敬请期待!"));
  604. break;
  605. }
  606. }
  607. },
  608. }
  609. </script>