journal-edit.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. <template>
  2. <view class="app-page bg-white pa-12">
  3. <!-- 工具栏区域 -->
  4. <view class="tool-bar-wrap e-fixed bg-white pt-12 pb-16 border-b-1">
  5. <view class="tool-bar flex flex-center text-grey-darken-2">
  6. <text class="halohtmlicon icon-undo" data-method="undo" @click="fnOnToolBarEdit"></text>
  7. <text class="halohtmlicon icon-redo" data-method="redo" @click="fnOnToolBarEdit"></text>
  8. <text class="halohtmlicon icon-img" data-method="insertImg" @click="fnOnToolBarEdit"></text>
  9. <text class="halohtmlicon icon-video" data-method="insertVideo" @click="fnOnToolBarEdit"></text>
  10. <text class="halohtmlicon icon-link" data-method="insertLink" @click="fnOnToolBarEdit"></text>
  11. <text class="halohtmlicon icon-text" data-method="insertText" @click="fnOnToolBarEdit"></text>
  12. <text class="halohtmlicon icon-line" data-method="insertHtml" data-param="<hr style='margin:10px 0;'/>" @click="fnOnToolBarEdit"></text>
  13. </view>
  14. <view class="tool-bar flex flex-center text-grey-darken-2 mt-16">
  15. <text class="halohtmlicon icon-heading" @click="fnOnInsertHead"></text>
  16. <text
  17. class="halohtmlicon icon-quote"
  18. data-method="insertHtml"
  19. data-param="<blockquote style='padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5'>引用</blockquote>"
  20. @click="fnOnToolBarEdit"
  21. ></text>
  22. <text class="halohtmlicon icon-table" @click="fnOnInsertTable"></text>
  23. <text class="halohtmlicon icon-code" @click="fnOnInsertCode"></text>
  24. <text class="halohtmlicon icon-emoji" data-type="emoji" data-title="插入表情" @click="fnOnOpenDialog"></text>
  25. <text class="halohtmlicon icon-template" data-type="template" data-title="插入模板" @click="fnOnOpenDialog"></text>
  26. <text v-if="editable" class="flex-1 text-align-center iconfont icon-eye" style="font-size: 44rpx;" @click="fnOnPreview()"></text>
  27. <text v-else class="halohtmlicon icon-edit" @click="fnOnPreview()"></text>
  28. </view>
  29. </view>
  30. <!-- 编辑区域 -->
  31. <view class="edit-wrap bg-white round-3">
  32. <mp-html
  33. class="evan-markdown"
  34. lazy-load
  35. ref="markdown"
  36. :editable="editable"
  37. :domain="markdownConfig.domain"
  38. :loading-img="markdownConfig.loadingGif"
  39. :scroll-table="true"
  40. selectable="force"
  41. :tag-style="markdownConfig.tagStyle"
  42. :container-style="markdownConfig.containStyle"
  43. :content="form.content"
  44. :markdown="true"
  45. :showLineNumber="true"
  46. :showLanguageName="true"
  47. :copyByLongPress="true"
  48. />
  49. </view>
  50. <view class="fixed-bottom bg-white pa-24 ">
  51. <view class="ml-32 mr-32 border-b-1 pb-12 flex-between">
  52. <text class="text-size-n ">是否公开</text>
  53. <view><tm-switch color="light-blue" v-model="status" :text="['是', '否']"></tm-switch></view>
  54. </view>
  55. <view class="btn-wrap flex flex-center mt-24">
  56. <tm-button theme="light-blue" :width="300" :height="76" block @click="fnSave()">立即保存</tm-button>
  57. <tm-button class="ml-24" theme="red" :width="300" :height="76" block @click="fnClear()">清空内容</tm-button>
  58. </view>
  59. </view>
  60. <!-- 附件选择文件 -->
  61. <attachment-select
  62. v-if="attachmentsSelect.show"
  63. :title="attachmentsSelect.title"
  64. @on-select="fnOnAttachmentsSelect"
  65. @on-close="attachmentsSelect.show = false"
  66. ></attachment-select>
  67. <!-- 弹窗 -->
  68. <block v-if="modal">
  69. <view class="mask" />
  70. <view class="modal">
  71. <view class="modal_title">{{ modal.title }}</view>
  72. <view class="flex flex-col flex-center" v-if="modal.type == 'table'">
  73. <tm-input required title="输入行数" input-type="number" v-model="modal.rows"></tm-input>
  74. <tm-input required title="输入列数" input-type="number" v-model="modal.cols"></tm-input>
  75. </view>
  76. <block v-else><input class="modal_input" :value="modal.value" maxlength="-1" auto-focus @input="modalInput" /></block>
  77. <view class="modal_foot">
  78. <view class="modal_button" @tap="modalCancel">取消</view>
  79. <view class="modal_button" style="color:#576b95;border-left:1px solid rgba(0,0,0,.1)" @tap="modalConfirm">确定</view>
  80. </view>
  81. </view>
  82. </block>
  83. <!-- 底部弹窗 -->
  84. <tm-poup v-model="dialog.show" position="bottom" height="auto" @change="fnOnCloseDialog()">
  85. <text class="poup-close" @click="fnOnCloseDialog(false)">×</text>
  86. <view class="poup-head pa-24 pb-0 text-align-center text-weight-b ">{{ dialog.title }}</view>
  87. <view class="poup-body pa-36">
  88. <!-- 表情区域 -->
  89. <block v-if="dialog.type == 'emoji'">
  90. <view class="flex" v-for="(item, index) in emojis" :key="index">
  91. <view class="flex-1 mt-6 mb-6 text-size-xl" v-for="(emoji, index) in item" :key="emoji" :data-emoji="emoji" @click="fnOnInsertEmoji">{{ emoji }}</view>
  92. </view>
  93. </block>
  94. <!-- 模板区域 -->
  95. <block v-else-if="dialog.type == 'template'">
  96. <block v-for="(template, index) in templates" :key="index">
  97. <rich-text :nodes="template" data-method="insertHtml" :data-param="template" @click="fnOnToolBarEdit"></rich-text>
  98. </block>
  99. </block>
  100. </view>
  101. </tm-poup>
  102. </view>
  103. </template>
  104. <script>
  105. import { getAdminAccessToken } from '@/utils/auth.js';
  106. import MarkdownConfig from '@/common/markdown/markdown.config.js';
  107. import mpHtml from '@/components/mp-html/components/mp-html/mp-html.vue';
  108. import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
  109. import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue';
  110. import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
  111. import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
  112. import tmFlotbutton from '@/tm-vuetify/components/tm-flotbutton/tm-flotbutton.vue';
  113. import tmSwitch from '@/tm-vuetify/components/tm-switch/tm-switch.vue';
  114. import tmPoup from '@/tm-vuetify/components/tm-poup/tm-poup.vue';
  115. import tmInput from '@/tm-vuetify/components/tm-input/tm-input.vue';
  116. import attachmentSelect from '@/components/attachment-select/attachment-select.vue';
  117. export default {
  118. components: {
  119. mpHtml,
  120. tmSkeleton,
  121. tmButton,
  122. tmEmpty,
  123. tmTranslate,
  124. tmFlotbutton,
  125. tmSwitch,
  126. tmPoup,
  127. tmInput,
  128. attachmentSelect
  129. },
  130. data() {
  131. return {
  132. loading: 'loading',
  133. markdownConfig: MarkdownConfig,
  134. queryParams: {
  135. size: 10,
  136. page: 0
  137. },
  138. status: true,
  139. form: {
  140. content: '',
  141. keepRaw: true,
  142. sourceContent: '',
  143. type: 'PUBLIC'
  144. },
  145. modal: null,
  146. editable: true,
  147. attachmentsSelect: {
  148. title: '选择附件',
  149. show: false
  150. },
  151. dialog: {
  152. show: false,
  153. type: ''
  154. },
  155. // 用于插入的 emoji 表情
  156. emojis: [
  157. ['😄', '😷', '😂', '😝', '😳', '😱', '😔', '😒', '😉'],
  158. ['😎', '😭', '😍', '😘', '🤔', '😕', '🙃', '🤑', '😲'],
  159. ['🙄', '😤', '😴', '🤓', '😡', '😑', '😮', '🤒', '🤮']
  160. ],
  161. // 用于插入的 html 模板
  162. templates: [
  163. '<section style="text-align: center; margin: 0px auto;"><section style="border-radius: 4px; border: 1px solid #757576; display: inline-block; padding: 5px 20px;"><span style="font-size: 18px; color: #595959;">标题</span></section></section>',
  164. '<div style="width: 100%; box-sizing: border-box; border-radius: 5px; background-color: #f6f6f6; padding: 10px; margin: 10px 0"><div>卡片</div><div style="font-size: 12px; color: gray">正文</div></div>',
  165. '<div style="border: 1px solid gray; box-shadow: 3px 3px 0px #cfcfce; padding: 10px; margin: 10px 0">段落</div>'
  166. ]
  167. };
  168. },
  169. computed: {
  170. journalInfo() {
  171. return uni.$tm.vx.getters().getJournalInfo;
  172. }
  173. },
  174. onLoad() {
  175. if (this.journalInfo) {
  176. this.fnSetPageTitle('编辑日记');
  177. this.form = Object.assign({}, this.form, this.journalInfo);
  178. this.status = this.form.type == 'PUBLIC' ? true : false;
  179. } else {
  180. this.fnSetPageTitle('新增日记');
  181. this.form.content = uni.getStorageSync('html-edit');
  182. }
  183. },
  184. onReady() {
  185. /**
  186. * @description 设置获取链接的方法
  187. * @param {String} type 链接的类型(img/video/audio/link)
  188. * @param {String} value 修改链接时,这里会传入旧值
  189. * @returns {Promise} 返回线上地址
  190. * type 为音视频时可以返回一个数组作为源地址
  191. * type 为 audio 时,可以返回一个 object,包含 src、name、author、poster 等字段
  192. */
  193. this.$refs.markdown.getSrc = (type, value) => {
  194. return new Promise((resolve, reject) => {
  195. this.checkEditable()
  196. .then(res => {
  197. if (type === 'img' || type === 'video') {
  198. uni.showActionSheet({
  199. itemList: ['本地选取', '附件选取'],
  200. success: res => {
  201. if (res.tapIndex === 0) {
  202. // 本地选取
  203. if (type === 'img') {
  204. uni.chooseImage({
  205. count: value === undefined ? 9 : 1,
  206. success: res => {
  207. // #ifdef MP-WEIXIN
  208. if (res.tempFilePaths.length == 1 && wx.editImage) {
  209. // 单张图片时进行编辑
  210. wx.editImage({
  211. src: res.tempFilePaths[0],
  212. complete: res2 => {
  213. uni.showLoading({
  214. title: '上传中'
  215. });
  216. this.fnFileUpload(res2.tempFilePath || res.tempFilePaths[0], type).then(res => {
  217. uni.hideLoading();
  218. resolve(res);
  219. });
  220. }
  221. });
  222. } else {
  223. // #endif
  224. uni.showLoading({
  225. title: '上传中'
  226. });
  227. (async () => {
  228. const arr = [];
  229. for (let item of res.tempFilePaths) {
  230. // 依次上传
  231. const src = await this.fnFileUpload(item, type);
  232. arr.push(src);
  233. }
  234. return arr;
  235. })().then(res => {
  236. uni.hideLoading();
  237. resolve(res);
  238. });
  239. // #ifdef MP-WEIXIN
  240. }
  241. // #endif
  242. },
  243. fail: reject
  244. });
  245. } else {
  246. uni.chooseVideo({
  247. success: res => {
  248. uni.showLoading({
  249. title: '上传中'
  250. });
  251. this.fnFileUpload(res.tempFilePath, type).then(res => {
  252. uni.hideLoading();
  253. resolve(res);
  254. });
  255. },
  256. fail: reject
  257. });
  258. }
  259. } else {
  260. // 远程链接
  261. this.callback = {
  262. resolve,
  263. reject
  264. };
  265. this.attachmentsSelect.title = type === 'img' ? '选取图片' : '选取视频';
  266. this.attachmentsSelect.show = true;
  267. }
  268. }
  269. });
  270. } else {
  271. this.callback = {
  272. resolve,
  273. reject
  274. };
  275. let title;
  276. if (type === 'audio') {
  277. title = '音频链接';
  278. } else if (type === 'link') {
  279. title = '链接地址';
  280. }
  281. this.$set(this, 'modal', {
  282. title,
  283. type: 'link',
  284. value
  285. });
  286. }
  287. })
  288. .catch(err => {});
  289. });
  290. };
  291. },
  292. methods: {
  293. // 检查是否可编辑
  294. checkEditable() {
  295. return new Promise((resolve, reject) => {
  296. if (this.editable) {
  297. resolve();
  298. } else {
  299. uni.showModal({
  300. title: '提示',
  301. content: '需要继续编辑吗?',
  302. success: res => {
  303. if (res.confirm) {
  304. this.editable = true;
  305. resolve();
  306. } else {
  307. reject();
  308. }
  309. }
  310. });
  311. }
  312. });
  313. },
  314. // 调用编辑操作
  315. fnOnToolBarEdit(e) {
  316. this.$refs.markdown[e.currentTarget.dataset.method](e.currentTarget.dataset.param);
  317. },
  318. // 监听附件选择
  319. fnOnAttachmentsSelect(file) {
  320. this.attachmentsSelect.show = false;
  321. this.callback.resolve(file.path);
  322. },
  323. // 处理模态框
  324. modalInput(e) {
  325. this.value = e.detail.value;
  326. },
  327. modalConfirm() {
  328. if (this.modal.type == 'table') {
  329. if (this.modal.rows <= 0) {
  330. return uni.$tm.toast('行数必须大于0');
  331. }
  332. if (this.modal.cols <= 0) {
  333. return uni.$tm.toast('列数必须大于0');
  334. }
  335. }
  336. this.callback.resolve(this.value || this.modal.value || '');
  337. this.$set(this, 'modal', null);
  338. },
  339. modalCancel() {
  340. this.callback.reject();
  341. this.$set(this, 'modal', null);
  342. },
  343. // 上传图片方法
  344. fnFileUpload(src, type) {
  345. return new Promise((resolve, reject) => {
  346. uni.uploadFile({
  347. filePath: src,
  348. header: {
  349. 'admin-authorization': getAdminAccessToken()
  350. },
  351. url: this.$baseApiUrl + '/api/admin/attachments/upload',
  352. name: 'file',
  353. success: upladRes => {
  354. const _uploadRes = JSON.parse(upladRes.data);
  355. if (_uploadRes.status == 200) {
  356. resolve(_uploadRes.data.path);
  357. } else {
  358. uni.$tm.toast(_uploadRes.message);
  359. reject();
  360. }
  361. },
  362. fail: err => {
  363. uni.$tm.toast(err.message);
  364. reject();
  365. }
  366. });
  367. });
  368. },
  369. // 插入 head 系列标签
  370. fnOnInsertHead() {
  371. this.checkEditable()
  372. .then(() => {
  373. const _hList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
  374. wx.showActionSheet({
  375. itemList: _hList,
  376. success: res => {
  377. let tagName = _hList[res.tapIndex];
  378. this.$refs.markdown.insertHtml(`<${tagName}>标题</${tagName}>`);
  379. }
  380. });
  381. })
  382. .catch(() => {});
  383. },
  384. // 插入表格
  385. fnOnInsertTable() {
  386. this.checkEditable()
  387. .then(() => {
  388. this.$set(this, 'modal', {
  389. title: '插入表格',
  390. type: 'table',
  391. rows: 1,
  392. cols: 1,
  393. value: this.value
  394. });
  395. this.callback = {
  396. resolve: () => {
  397. this.$refs.markdown.insertTable(this.modal.rows, this.modal.cols);
  398. },
  399. reject: () => {}
  400. };
  401. })
  402. .catch(() => {});
  403. },
  404. // 保存插入表格
  405. fnOnSaveInsertTable() {
  406. this.callback.resolve(this.value || this.modal.value || '');
  407. },
  408. // 插入代码
  409. fnOnInsertCode() {
  410. this.checkEditable()
  411. .then(() => {
  412. uni.showActionSheet({
  413. itemList: ['html', 'css', 'javascript', 'json'],
  414. success: res => {
  415. const lan = ['html', 'css', 'javascript', 'json'][res.tapIndex];
  416. this.$refs.markdown.insertHtml(`<pre><code class="language-${lan}">${lan} code</code></pre>`);
  417. }
  418. });
  419. })
  420. .catch(() => {});
  421. },
  422. // 插入 emoji
  423. fnOnInsertEmoji(e) {
  424. this.$refs.markdown.insertHtml(e.currentTarget.dataset.emoji);
  425. this.fnOnCloseDialog();
  426. },
  427. // 处理底部弹窗
  428. fnOnOpenDialog(e) {
  429. this.checkEditable()
  430. .then(() => {
  431. this.dialog.type = e.currentTarget.dataset.type;
  432. this.dialog.title = e.currentTarget.dataset.title;
  433. this.dialog.show = true;
  434. })
  435. .catch(() => {});
  436. },
  437. fnOnCloseDialog(e) {
  438. if (e == false) {
  439. this.dialog.show = false;
  440. this.dialog.type = '';
  441. }
  442. },
  443. fnOnPreview() {
  444. this.editable = !this.editable;
  445. if (this.editable) {
  446. uni.$tm.toast('您已进入编辑模式!');
  447. } else {
  448. uni.$tm.toast('您已进入预览模式!');
  449. let _content = this.$refs.markdown.getContent();
  450. if (_content === '<p></p>') {
  451. _content = '';
  452. }
  453. this.form.content = _content;
  454. uni.setStorageSync('html-edit', _content);
  455. }
  456. },
  457. fnSave() {
  458. uni.showLoading({
  459. mask: true,
  460. title: '保存中...'
  461. });
  462. const _content = this.$refs.markdown.getContent();
  463. if (!_content.trim()) {
  464. return uni.$tm.toast('请输入内容!');
  465. }
  466. this.form.type = this.status ? 'PUBLIC' : 'INTIMATE';
  467. this.form.content = _content;
  468. this.form.sourceContent = this.$refs.markdown.getText();
  469. if (this.journalInfo) {
  470. this.$httpApi.admin
  471. .updateJournalsById(this.form.id, this.form)
  472. .then(res => {
  473. if (res.status == 200) {
  474. uni.$tm.toast('保存成功!');
  475. uni.setStorageSync('html-edit', '');
  476. setTimeout(() => {
  477. uni.$emit('journals_refresh');
  478. }, 1000);
  479. } else {
  480. uni.$tm.toast('保存失败,请重试!');
  481. }
  482. })
  483. .catch(err => {
  484. uni.$tm.toast('保存失败,请重试!');
  485. });
  486. } else {
  487. this.$httpApi.admin
  488. .createJournals(this.form)
  489. .then(res => {
  490. if (res.status == 200) {
  491. uni.$tm.toast('保存成功!');
  492. uni.setStorageSync('html-edit', '');
  493. setTimeout(() => {
  494. uni.$emit('journals_refresh');
  495. }, 1000);
  496. } else {
  497. uni.$tm.toast('保存失败,请重试!');
  498. }
  499. })
  500. .catch(err => {
  501. uni.$tm.toast('保存失败,请重试!');
  502. });
  503. }
  504. },
  505. fnClear() {
  506. uni.$eShowModal({
  507. title: '提示',
  508. content: '确定清空当前内容吗?',
  509. showCancel: true,
  510. cancelText: '否',
  511. cancelColor: '#999999',
  512. confirmText: '是',
  513. confirmColor: '#03a9f4'
  514. })
  515. .then(res => {
  516. this.$refs.markdown.clear();
  517. this.fnToTopPage();
  518. })
  519. .catch(() => {});
  520. }
  521. }
  522. };
  523. </script>
  524. <style lang="scss" scoped>
  525. .app-page {
  526. width: 100vw;
  527. min-height: 100vh;
  528. box-sizing: border-box;
  529. padding-bottom: 236rpx;
  530. }
  531. .fixed-bottom {
  532. position: fixed;
  533. left: 0;
  534. bottom: 0;
  535. right: 0;
  536. box-sizing: border-box;
  537. box-shadow: 0rpx -4rpx 24rpx rgba(0, 0, 0, 0.05);
  538. border-radius: 24rpx 24rpx 0 0;
  539. }
  540. .edit-wrap {
  541. box-sizing: border-box;
  542. padding-top: 150rpx;
  543. }
  544. .modal {
  545. width: 80vw;
  546. position: fixed;
  547. top: 50%;
  548. left: 50%;
  549. transform: translate(-50%, -50%);
  550. background-color: #fff;
  551. border-radius: 24rpx;
  552. }
  553. .modal_title {
  554. padding-top: 42rpx;
  555. padding-bottom: 32rpx;
  556. font-size: 34rpx;
  557. font-weight: 700;
  558. text-align: center;
  559. }
  560. .modal_input {
  561. display: block;
  562. padding: 12rpx 12rpx;
  563. margin: 24rpx;
  564. margin-top: 0;
  565. font-size: 30rpx;
  566. border: 2rpx solid #dfe2e5;
  567. border-radius: 6rpx;
  568. }
  569. .modal_foot {
  570. display: flex;
  571. line-height: 100rpx;
  572. font-weight: bold;
  573. border-top: 2rpx solid rgba(0, 0, 0, 0.1);
  574. }
  575. .modal_button {
  576. flex: 1;
  577. text-align: center;
  578. }
  579. /* 蒙版 */
  580. .mask {
  581. position: fixed;
  582. top: 0;
  583. right: 0;
  584. bottom: 0;
  585. left: 0;
  586. background-color: black;
  587. opacity: 0.5;
  588. }
  589. .poup-close {
  590. position: absolute;
  591. right: 30rpx;
  592. top: 8rpx;
  593. font-size: 50rpx;
  594. }
  595. </style>