index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. <template>
  2. <view>
  3. <view class="editor_toolbox" :style="{ top: `${statusBarHeight}px` }">
  4. <u-icon name="arrow-leftward" size="40rpx" color="#303133" @click="onSave"></u-icon>
  5. <!-- <i class="iconfont icon-save" @tap="onSave" /> -->
  6. <i class="iconfont icon-undo" data-method="undo" @tap="edit" />
  7. <i class="iconfont icon-redo" data-method="redo" @tap="edit" />
  8. <i class="iconfont icon-img" data-method="insertImg" @tap="edit" />
  9. <i class="iconfont icon-video" data-method="insertVideo" @tap="edit" />
  10. <i class="iconfont icon-link" data-method="insertLink" @tap="edit" />
  11. <i class="iconfont icon-text" data-method="insertText" @tap="edit" />
  12. <i class="iconfont icon-clear" @tap="clear" />
  13. </view>
  14. <view :style="{ paddingTop: `${statusBarHeight + 50}px` }">
  15. <mp-html ref="article" container-style="padding:20px" :content="content" domain="" :editable="editable" @remove="remove" />
  16. </view>
  17. <block v-if="modal">
  18. <view class="mask" />
  19. <view class="modal">
  20. <view class="modal_title">{{ modal.title }}</view>
  21. <input class="modal_input" :value="modal.value" maxlength="-1" auto-focus @input="modalInput" />
  22. <view class="modal_foot">
  23. <view class="modal_button" @tap="modalCancel">取消</view>
  24. <view class="modal_button" style="color:#576b95;border-left:1px solid rgba(0,0,0,.1)" @tap="modalConfirm">确定</view>
  25. </view>
  26. </view>
  27. </block>
  28. </view>
  29. </template>
  30. <script>
  31. import mpHtml from '@/components/rider-crud/components/mp-html/mp-html'
  32. import http from '@/http/api.js'
  33. // 上传图片方法
  34. function upload(src, type) {
  35. return new Promise((resolve, reject) => {
  36. console.log('上传', type === 'img' ? '图片' : '视频', ':', src)
  37. // resolve(src)
  38. const params = {
  39. // #ifdef MP-ALIPAY
  40. fileType: 'image/video/audio', // 仅支付宝小程序,且必填。
  41. // #endif
  42. filePath: src,
  43. name: 'file'
  44. }
  45. http.upload('/api/blade-resource/oss/endpoint/put-file', params).then(res => {
  46. resolve(res.data.link)
  47. })
  48. })
  49. }
  50. // 删除图片方法
  51. function remove(src) {
  52. console.log('删除图片:', src)
  53. // 实际使用时,删除线上资源
  54. }
  55. export default {
  56. data() {
  57. return {
  58. content: null,
  59. modal: null,
  60. editable: true,
  61. statusBarHeight: 0
  62. }
  63. },
  64. components: {
  65. mpHtml
  66. },
  67. onReady() {
  68. /**
  69. * @description 设置获取链接的方法
  70. * @param {String} type 链接的类型(img/video/audio/link)
  71. * @param {String} value 修改链接时,这里会传入旧值
  72. * @returns {Promise} 返回线上地址
  73. * type 为音视频时可以返回一个数组作为源地址
  74. * type 为 audio 时,可以返回一个 object,包含 src、name、author、poster 等字段
  75. */
  76. this.$refs.article.getSrc = (type, value) => {
  77. return new Promise((resolve, reject) => {
  78. if (type === 'img' || type === 'video') {
  79. uni.showActionSheet({
  80. itemList: ['本地选取', '远程链接'],
  81. success: res => {
  82. if (res.tapIndex === 0) {
  83. // 本地选取
  84. if (type === 'img') {
  85. uni.chooseImage({
  86. count: value === undefined ? 9 : 1, // 2.2.0 版本起插入图片时支持多张(修改图片链接时仅限一张)
  87. success: res => {
  88. // #ifdef MP-WEIXIN
  89. if (res.tempFilePaths.length == 1 && wx.editImage) {
  90. // 单张图片时进行编辑
  91. wx.editImage({
  92. src: res.tempFilePaths[0],
  93. complete: res2 => {
  94. uni.showLoading({
  95. title: '上传中'
  96. })
  97. upload(res2.tempFilePath || res.tempFilePaths[0], type).then(res => {
  98. uni.hideLoading()
  99. resolve(res)
  100. })
  101. }
  102. })
  103. } else {
  104. // #endif
  105. uni.showLoading({
  106. title: '上传中'
  107. })
  108. ;(async () => {
  109. const arr = []
  110. for (let item of res.tempFilePaths) {
  111. // 依次上传
  112. const src = await upload(item, type)
  113. arr.push(src)
  114. }
  115. return arr
  116. })().then(res => {
  117. uni.hideLoading()
  118. resolve(res)
  119. })
  120. // #ifdef MP-WEIXIN
  121. }
  122. // #endif
  123. },
  124. fail: reject
  125. })
  126. } else {
  127. uni.chooseVideo({
  128. success: res => {
  129. uni.showLoading({
  130. title: '上传中'
  131. })
  132. upload(res.tempFilePath, type).then(res => {
  133. uni.hideLoading()
  134. resolve(res)
  135. })
  136. },
  137. fail: reject
  138. })
  139. }
  140. } else {
  141. // 远程链接
  142. this.callback = {
  143. resolve,
  144. reject
  145. }
  146. this.$set(this, 'modal', {
  147. title: (type === 'img' ? '图片' : '视频') + '链接',
  148. value
  149. })
  150. }
  151. }
  152. })
  153. } else {
  154. this.callback = {
  155. resolve,
  156. reject
  157. }
  158. let title
  159. if (type === 'audio') {
  160. title = '音频链接'
  161. } else if (type === 'link') {
  162. title = '链接地址'
  163. }
  164. this.$set(this, 'modal', {
  165. title,
  166. value
  167. })
  168. }
  169. })
  170. }
  171. },
  172. onLoad(option) {
  173. let that = this
  174. that.content = option.data
  175. // 获取高度 - 小程序设配
  176. uni.getSystemInfo({
  177. success: function(res) {
  178. // #ifdef MP-WEIXIN
  179. that.statusBarHeight = Number(res.statusBarHeight) + Number(res.platform == 'ios' ? 44 : 48)
  180. // #endif
  181. // #ifdef APP-PLUS
  182. that.statusBarHeight = Number(res.statusBarHeight)
  183. // #endif
  184. }
  185. })
  186. },
  187. methods: {
  188. // 删除图片/视频/音频标签事件
  189. remove(e) {
  190. // 删除线上资源
  191. remove(e.src)
  192. },
  193. // 处理模态框
  194. modalInput(e) {
  195. this.value = e.detail.value
  196. },
  197. modalConfirm() {
  198. this.callback.resolve(this.value || this.modal.value || '')
  199. this.$set(this, 'modal', null)
  200. },
  201. modalCancel() {
  202. this.callback.reject()
  203. this.$set(this, 'modal', null)
  204. },
  205. // 调用编辑器接口
  206. edit(e) {
  207. this.$refs.article[e.currentTarget.dataset.method]()
  208. },
  209. // 清空编辑器内容
  210. clear() {
  211. uni.showModal({
  212. title: '确认',
  213. content: '确定清空内容吗?',
  214. success: res => {
  215. if (res.confirm) this.$refs.article.clear()
  216. }
  217. })
  218. },
  219. // 保存编辑器内容
  220. onSave() {
  221. setTimeout(() => {
  222. var content = this.$refs.article.getContent()
  223. uni.showModal({
  224. title: '提示',
  225. content: '是否保存当前内容返回上一级?',
  226. confirmText: '保存',
  227. cancelText: '不保存',
  228. success: res => {
  229. if (res.confirm) {
  230. uni.$emit('richBack', content)
  231. }
  232. this.editable = false
  233. uni.navigateBack({
  234. delta: 1
  235. })
  236. }
  237. })
  238. }, 50)
  239. }
  240. }
  241. }
  242. </script>
  243. <style>
  244. .editor_toolbox {
  245. position: fixed;
  246. top: 0;
  247. width: 100%;
  248. z-index: 999;
  249. background-color: #ededed;
  250. display: flex;
  251. align-items: center;
  252. padding: 5px;
  253. box-sizing: border-box;
  254. line-height: 1.6;
  255. }
  256. @font-face {
  257. font-family: 'iconfont';
  258. src: url('data:application/x-font-woff2;charset=utf-8;base64,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')
  259. format('woff2');
  260. }
  261. .iconfont {
  262. color: #303133;
  263. flex: 1;
  264. text-align: center;
  265. font-family: 'iconfont' !important;
  266. font-size: 22px;
  267. font-style: normal;
  268. -webkit-font-smoothing: antialiased;
  269. -moz-osx-font-smoothing: grayscale;
  270. }
  271. .icon-undo:before {
  272. content: '\e607';
  273. }
  274. .icon-redo:before {
  275. content: '\e606';
  276. }
  277. .icon-img:before {
  278. content: '\e6e2';
  279. }
  280. .icon-video:before {
  281. content: '\e798';
  282. }
  283. .icon-link:before {
  284. content: '\e60d';
  285. }
  286. .icon-text:before {
  287. content: '\e6ce';
  288. }
  289. .icon-clear:before {
  290. content: '\e637';
  291. }
  292. .icon-save:before {
  293. content: '\e501';
  294. }
  295. /* 模态框 */
  296. .modal {
  297. position: fixed;
  298. top: 50%;
  299. left: 16px;
  300. right: 16px;
  301. background-color: #fff;
  302. border-radius: 12px;
  303. transform: translateY(-50%);
  304. }
  305. .modal_title {
  306. padding: 32px 24px 16px;
  307. font-size: 17px;
  308. font-weight: 700;
  309. text-align: center;
  310. }
  311. .modal_input {
  312. display: block;
  313. padding: 5px;
  314. margin: 0 24px 32px 24px;
  315. font-size: 14px;
  316. border: 1px solid #dfe2e5;
  317. }
  318. .modal_foot {
  319. display: flex;
  320. line-height: 56px;
  321. font-weight: 700;
  322. border-top: 1px solid rgba(0, 0, 0, 0.1);
  323. }
  324. .modal_button {
  325. flex: 1;
  326. text-align: center;
  327. }
  328. /* 蒙版 */
  329. .mask {
  330. position: fixed;
  331. top: 0;
  332. right: 0;
  333. bottom: 0;
  334. left: 0;
  335. background-color: black;
  336. opacity: 0.5;
  337. }
  338. </style>