richTextEditor.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. <template>
  2. <view class="">
  3. </view>
  4. </template>
  5. <script setup>
  6. import {
  7. onLoad
  8. } from "@dcloudio/uni-app";
  9. </script>
  10. <style lang="scss" scoped>
  11. </style>
  12. // Editor.vue
  13. <template>
  14. <view class="container">
  15. <view class="editor-wrapper">
  16. <editor id="editor" ref="editorRef" class="ql-container" :placeholder="placeholder" @statuschange="onStatusChange"
  17. :show-img-resize="true" @ready="onEditorReady" @input="getCtx"></editor>
  18. </view>
  19. <view class="wrapper">
  20. <view class="toolbar" @tap="formatToolbar">
  21. <view :class="formats.header === 1 ? 'ql-active' : ''" class="iconfont icon-format-header-1" data-name="header"
  22. :data-value="1"></view>
  23. <view :class="formats.header === 2 ? 'ql-active' : ''" class="iconfont icon-format-header-2" data-name="header"
  24. :data-value="2"></view>
  25. <view :class="formats.bold ? 'ql-active' : ''" class="iconfont icon-zitijiacu" data-name="bold"></view>
  26. <view :class="formats.italic ? 'ql-active' : ''" class="iconfont icon-zitixieti" data-name="italic">
  27. </view>
  28. <view :class="formats.underline ? 'ql-active' : ''" class="iconfont icon-zitixiahuaxian" data-name="underline">
  29. </view>
  30. <view :class="formats.strike ? 'ql-active' : ''" class="iconfont icon-zitishanchuxian" data-name="strike">
  31. </view>
  32. <view :class="formats.align === 'left' ? 'ql-active' : ''" class="iconfont icon-zuoduiqi" data-name="align"
  33. data-value="left"></view>
  34. <view :class="formats.align === 'center' ? 'ql-active' : ''" class="iconfont icon-juzhongduiqi"
  35. data-name="align" data-value="center"></view>
  36. <view :class="formats.align === 'right' ? 'ql-active' : ''" class="iconfont icon-youduiqi" data-name="align"
  37. data-value="right"></view>
  38. <view :class="formats.align === 'justify' ? 'ql-active' : ''" class="iconfont icon-zuoyouduiqi"
  39. data-name="align" data-value="justify"></view>
  40. <view :class="formats.lineHeight ? 'ql-active' : ''" class="iconfont icon-line-height" data-name="lineHeight"
  41. data-value="2"></view>
  42. <view :class="formats.letterSpacing ? 'ql-active' : ''" class="iconfont icon-Character-Spacing"
  43. data-name="letterSpacing" data-value="2em"></view>
  44. <view :class="formats.marginTop ? 'ql-active' : ''" class="iconfont icon-722bianjiqi_duanqianju"
  45. data-name="marginTop" data-value="20px"></view>
  46. <view :class="formats.previewarginBottom ? 'ql-active' : ''" class="iconfont icon-723bianjiqi_duanhouju"
  47. data-name="marginBottom" data-value="20px"></view>
  48. <!-- <view class="iconfont icon-clearedformat" @tap="removeFormat"></view> -->
  49. <view :class="formats.fontFamily ? 'ql-active' : ''" class="iconfont icon-font" data-name="fontFamily"
  50. data-value="Pacifico"></view>
  51. <view :class="formats.fontSize === '24px' ? 'ql-active' : ''" class="iconfont icon-fontsize"
  52. data-name="fontSize" data-value="24px"></view>
  53. <view :class="formats.color === '#0000ff' ? 'ql-active' : ''" class="iconfont icon-text_color" data-name="color"
  54. data-value="#0000ff"></view>
  55. <view :class="formats.backgroundColor === '#00ff00' ? 'ql-active' : ''" class="iconfont icon-fontbgcolor"
  56. data-name="backgroundColor" data-value="#00ff00"></view>
  57. <view class="iconfont icon-date" @tap="insertDate"></view>
  58. <view class="iconfont icon--checklist" data-name="list" data-value="check"></view>
  59. <view :class="formats.list === 'ordered' ? 'ql-active' : ''" class="iconfont icon-youxupailie" data-name="list"
  60. data-value="ordered"></view>
  61. <view :class="formats.list === 'bullet' ? 'ql-active' : ''" class="iconfont icon-wuxupailie" data-name="list"
  62. data-value="bullet"></view>
  63. <view class="iconfont icon-undo" @tap="undo"></view>
  64. <view class="iconfont icon-redo" @tap="redo"></view>
  65. <view class="iconfont icon-outdent" data-name="indent" data-value="-1"></view>
  66. <view class="iconfont icon-indent" data-name="indent" data-value="+1"></view>
  67. <view class="iconfont icon-fengexian" @tap="insertDivider"></view>
  68. <view class="iconfont icon-charutupian" @tap="insertImage"></view>
  69. <view :class="formats.script === 'sub' ? 'ql-active' : ''" class="iconfont icon-zitixiabiao" data-name="script"
  70. data-value="sub"></view>
  71. <view :class="formats.script === 'super' ? 'ql-active' : ''" class="iconfont icon-zitishangbiao"
  72. data-name="script" data-value="super"></view>
  73. <view class="iconfont icon-shanchu" @tap="clear"></view>
  74. <view :class="formats.direction === 'rtl' ? 'ql-active' : ''" class="iconfont icon-direction-rtl"
  75. data-name="direction" data-value="rtl"></view>
  76. </view>
  77. </view>
  78. </view>
  79. </template>
  80. <script setup>
  81. import {
  82. ref,
  83. getCurrentInstance,
  84. nextTick
  85. } from 'vue';
  86. const instance = getCurrentInstance(); // 组件实例
  87. const props = defineProps({
  88. value: {
  89. type: String,
  90. default: 'ces'
  91. }
  92. })
  93. const emit = defineEmits(['update:value'])
  94. // 只读
  95. const readOnly = ref(false)
  96. // 提示词
  97. const placeholder = ref('开始输入...')
  98. // 富文本内容
  99. const richText = ref('')
  100. // 底部工具栏
  101. const formats = ref({}
  102. as any)
  103. // 图片上传 - 接口
  104. const serverUrl = ref('/resource/oss/upload')
  105. // editor编辑器
  106. const editorCtx = ref < any > (null)
  107. // 只读改变
  108. const readOnlyChange = () => {
  109. readOnly.value = !readOnly.value;
  110. }
  111. // 编辑器初始化完成时触发
  112. const onEditorReady = () => {
  113. // 富文本节点渲染完成
  114. const htmls = props.value
  115. const contents = JSON.stringify(htmls)
  116. const query = uni.createSelectorQuery().in(instance);
  117. query.select("#editor").context((res: any) => {
  118. editorCtx.value = res.context;
  119. editorCtx.value?.setContents({
  120. html: htmls,
  121. delta: contents
  122. })
  123. }).exec();
  124. }
  125. // 编辑器内容改变时触发,detail = {html, text, delta}
  126. const getCtx = (e) => {
  127. richText.value = e.detail.html;
  128. // console.log('richText.value', richText.value);
  129. emit('update:value', richText.value)
  130. // this.$emit('input', e.detail.html);
  131. }
  132. // 撤销
  133. const undo = () => {
  134. editorCtx.value.undo();
  135. }
  136. // 重做
  137. const redo = () => {
  138. editorCtx.value.redo();
  139. }
  140. // 工具栏点击触发
  141. const formatToolbar = (e: any) => {
  142. const {
  143. name,
  144. value
  145. } = e.target.dataset;
  146. if (!name) return;
  147. editorCtx.value.format(name, value);
  148. }
  149. // 通过 Context 方法改变编辑器内样式时触发,返回选区已设置的样式
  150. const onStatusChange = (e: any) => {
  151. console.log(e);
  152. const format = e.detail;
  153. formats.value = format;
  154. }
  155. // 插入分割线
  156. const insertDivider = () => {
  157. editorCtx.value.insertDivider({
  158. success: function() {
  159. console.log('insert divider success');
  160. }
  161. });
  162. }
  163. // 清空
  164. const clear = () => {
  165. editorCtx.value.clear({
  166. success: function() {
  167. console.log('clear success');
  168. }
  169. });
  170. }
  171. // 重置格式
  172. const removeFormat = () => {
  173. editorCtx.value.removeFormat();
  174. }
  175. // 插入当前日期
  176. const insertDate = () => {
  177. const date = new Date();
  178. const formatDate = `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
  179. editorCtx.value.insertText({
  180. text: formatDate
  181. });
  182. }
  183. // 插入图片
  184. const insertImage = () => {
  185. // 选择图片
  186. uni.chooseImage({
  187. count: 1,
  188. sizeType: ['original', 'compressed'],
  189. sourceType: ['album', 'camera'],
  190. success: async (res: any) => {
  191. uni.uploadFile({
  192. url: serverUrl.value, //请求的图片上传接口,这里是基准地址加上传接口
  193. filePath: res.tempFilePaths[0],
  194. name: 'file',
  195. formData: {
  196. file: res.tempFiles[0],
  197. },
  198. success: res => {
  199. // 装换
  200. const data = JSON.parse(res.data)?.data;
  201. if (data?.url) {
  202. editorCtx.value?.insertImage({
  203. width: '98%', //设置宽度为100%防止宽度溢出手机屏幕
  204. height: 'auto',
  205. src: data.url,
  206. alt: data.fileName,
  207. success: function() {
  208. console.log('insert image success');
  209. }
  210. });
  211. } else {
  212. uni.showToast({
  213. title: '上传失败,请重试!',
  214. icon: 'error',
  215. })
  216. }
  217. },
  218. fail: err => {
  219. uni.showToast({
  220. title: '上传失败,请重试!',
  221. icon: 'error',
  222. })
  223. }
  224. });
  225. }
  226. });
  227. }
  228. </script>
  229. <style lang="scss" scoped>
  230. @import './editor-icon.css';
  231. .container {
  232. height: 100%;
  233. .editor-wrapper {
  234. height: 100%;
  235. }
  236. .wrapper {
  237. /* height: 100%; */
  238. position: fixed;
  239. left: 0;
  240. bottom: 0;
  241. width: 100%;
  242. }
  243. .iconfont {
  244. padding: 16rpx;
  245. /* width: 60rpx; */
  246. /* height: 60rpx; */
  247. cursor: pointer;
  248. font-size: 40rpx;
  249. }
  250. .toolbar {
  251. box-sizing: border-box;
  252. border-bottom: 0;
  253. font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
  254. height: 74rpx;
  255. display: flex;
  256. align-items: center;
  257. flex-wrap: nowrap;
  258. overflow-x: scroll;
  259. margin: 0 20rpx;
  260. background-color: #fff;
  261. /* width: 100%; */
  262. }
  263. .ql-container {
  264. box-sizing: border-box;
  265. /* padding: 0 32rpx; */
  266. width: 100%;
  267. min-height: 30vh;
  268. height: 100%;
  269. margin-top: 20px;
  270. font-size: 16px;
  271. line-height: 1.5;
  272. overflow: scroll;
  273. }
  274. .ql-active {
  275. color: #a1d5ba;
  276. }
  277. }
  278. </style>