Files
taimed-international-app/components/comment/CommentEditor.vue

492 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<wd-popup
v-model="visible"
position="bottom"
lock-scroll
:close-on-click-modal="false"
@close="handleClose"
>
<view class="comment-editor">
<view class="editor-header">
<text class="title">{{ parentComment ? '回复留言' : '发布留言' }}</text>
<wd-icon name="close" @click="handleClose" />
</view>
<!-- 回复对象 -->
<view v-if="parentComment" class="reply-to">
<text>回复@{{ parentComment.user.name }} 的留言</text>
</view>
<!-- 富文本编辑器 -->
<view class="editor-container">
<view class="editor-wrapper">
<editor
id="editor"
class="ql-container"
placeholder="~ 和谐社会 友善发言 ~"
show-img-size
show-img-toolbar
show-img-resize
:read-only="readOnly"
@statuschange="onStatusChange"
@ready="onEditorReady"
/>
</view>
</view>
<!-- 工具栏 -->
<view class="tools-panel">
<view class="tool-item" @click="showEmojiPicker">
<image src="/static/biaoqing.png" class="tool-icon" mode="aspectFit" />
<text class="tool-text">表情</text>
</view>
<!-- <view class="tool-item" @click="chooseImage">
<wd-icon name="picture" size="18px" color="#a9a9a9" />
<text class="tool-text">图片</text>
</view> -->
</view>
<!-- 已上传的图片 -->
<view v-if="uploadedImages.length > 0" class="uploaded-images">
<view class="image-tip">
<text>最多可上传3张图片</text>
</view>
<view class="image-list">
<view
v-for="(img, index) in uploadedImages"
:key="index"
class="image-item"
>
<image :src="img.url" mode="aspectFill" class="preview-image" />
<view class="delete-btn" @click="deleteImage(index)">
<wd-icon name="close" size="16px" color="#fff" />
</view>
</view>
</view>
</view>
<!-- 表情选择器 -->
<view v-if="showEmoji" class="emoji-picker">
<bkhumor-emojiplus
@emotion="handleEmojiSelect"
:height="230"
:windowWidth="windowWidth"
/>
</view>
<!-- 操作按钮 -->
<view class="editor-actions">
<wd-button type="primary" block :loading="submitting" @click="handleSubmit"> </wd-button>
<!-- <wd-button type="info" block plain custom-style="margin-top: 20rpx" @click="handleClose"> </wd-button> -->
</view>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { IComment } from '@/types/comment'
import BkhumorEmojiplus from '@/components/bkhumor-emojiplus/index.vue'
interface Props {
show: boolean
parentComment?: IComment
type: 'course' | 'book'
}
const props = defineProps<Props>()
const emit = defineEmits<{
submit: [content: string, images: string[]]
close: []
}>()
const visible = computed({
get: () => props.show,
set: (val) => {
if (!val) emit('close')
}
})
const editorCtx = ref<any>(null)
const readOnly = ref(false)
const showEdit = ref(false)
const showTools = ref(false)
const showEmoji = ref(false)
const uploadedImages = ref<Array<{ url: string }>>([])
const submitting = ref(false)
const windowWidth = ref(0)
// 获取窗口宽度
windowWidth.value = uni.getSystemInfoSync().windowWidth
/**
* 编辑器准备完成
*/
const onEditorReady = () => {
uni.createSelectorQuery()
.select('#editor')
.context((res: any) => {
editorCtx.value = res.context
editorCtx.value.clear()
})
.exec()
}
/**
* 编辑器状态变化
*/
const onStatusChange = (e: any) => {
// 可以在这里处理编辑器状态变化
}
/**
* 切换工具栏
*/
const toggleTools = () => {
showTools.value = true
showEdit.value = true
uni.hideKeyboard()
}
/**
* 隐藏工具栏
*/
const hideTools = () => {
showTools.value = false
showEdit.value = false
showEmoji.value = false
}
/**
* 显示表情选择器
*/
const showEmojiPicker = () => {
showEmoji.value = !showEmoji.value
showTools.value = false
}
/**
* 选择表情
*/
const handleEmojiSelect = (emoji: any) => {
if (editorCtx.value) {
editorCtx.value.insertImage({
src: emoji.emotion,
alt: '表情',
success: () => {
console.log('插入表情成功')
}
})
}
}
/**
* 选择图片
*/
// const chooseImage = () => {
// if (uploadedImages.value.length >= 3) {
// uni.showToast({
// title: '最多只能上传3张图片',
// icon: 'none'
// })
// return
// }
// uni.chooseImage({
// count: 3 - uploadedImages.value.length,
// sizeType: ['compressed'],
// sourceType: ['album', 'camera'],
// success: (res) => {
// uploadImages(res.tempFilePaths)
// }
// })
// }
/**
* 上传图片
*/
const uploadImages = (filePaths: string[]) => {
filePaths.forEach((filePath) => {
uni.uploadFile({
url: uni.getStorageSync('baseURL') + 'oss/fileoss',
filePath,
name: 'file',
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.url) {
uploadedImages.value.push({ url: data.url })
}
} catch (error) {
console.error('解析上传结果失败:', error)
uni.showToast({
title: '上传失败',
icon: 'none'
})
}
},
fail: () => {
uni.showToast({
title: '上传失败',
icon: 'none'
})
}
})
})
}
/**
* 删除图片
*/
const deleteImage = (index: number) => {
uploadedImages.value.splice(index, 1)
}
/**
* 获取编辑器内容
*/
const getEditorContent = (): Promise<string> => {
return new Promise((resolve, reject) => {
if (!editorCtx.value) {
reject('编辑器未初始化')
return
}
editorCtx.value.getContents({
success: (res: any) => {
resolve(res.html || '')
},
fail: (error: any) => {
reject(error)
}
})
})
}
/**
* 提交评论
*/
const handleSubmit = async () => {
try {
submitting.value = true
// 获取编辑器内容
const content = await getEditorContent()
// 去除HTML标签检查是否为空
const textContent = content.replace(/<(?!img\s*\/?)[^>]*>/g, '').trim()
if (!textContent && uploadedImages.value.length === 0) {
uni.showToast({
title: '请输入内容',
icon: 'none'
})
return
}
// 获取图片URL列表
const images = uploadedImages.value.map(img => img.url)
// 提交
emit('submit', content, images)
// 清空编辑器和图片
if (editorCtx.value) {
editorCtx.value.clear()
}
uploadedImages.value = []
showTools.value = false
showEmoji.value = false
showEdit.value = false
} catch (error) {
console.error('提交评论失败:', error)
uni.showToast({
title: '提交失败',
icon: 'none'
})
} finally {
submitting.value = false
}
}
/**
* 关闭编辑器
*/
const handleClose = () => {
// 清空状态
if (editorCtx.value) {
editorCtx.value.clear()
}
uploadedImages.value = []
showTools.value = false
showEmoji.value = false
showEdit.value = false
emit('close')
}
// 监听显示状态,重置编辑器
watch(() => props.show, (newVal) => {
if (newVal) {
// 延迟初始化编辑器
setTimeout(() => {
onEditorReady()
}, 300)
}
})
</script>
<style lang="scss" scoped>
.comment-editor {
padding: 30rpx;
background-color: #fff;
max-height: 80vh;
overflow-y: auto;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
}
.reply-to {
padding: 20rpx;
margin-bottom: 20rpx;
background-color: #f7f8f9;
border-radius: 8rpx;
text {
font-size: 26rpx;
color: #666;
}
}
.editor-container {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
.editor-wrapper {
flex: 1;
.ql-container {
min-height: 100rpx;
max-height: 400rpx;
height: auto;
padding: 20rpx;
border: 1px solid #e5e5e5;
border-radius: 8rpx;
font-size: 28rpx;
line-height: 1.2;
box-sizing: border-box;
:deep(.ql-editor) img {
width: 20px;
}
}
}
.editor-tools {
height: 100rpx;
width: 100rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
border: 1px solid #e5e5e5;
border-radius: 8rpx;
cursor: pointer;
}
}
.tools-panel {
display: flex;
gap: 10px;
margin-bottom: 20rpx;
border-radius: 8rpx;
.tool-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
background-color: #f7f8f9;
width: 120rpx;
height: 120rpx;
border-radius: 10px;
.tool-icon {
width: 40rpx;
height: 40rpx;
}
.tool-text {
font-size: 24rpx;
color: #666;
}
}
}
.uploaded-images {
margin-bottom: 20rpx;
.image-tip {
margin-bottom: 15rpx;
text {
font-size: 24rpx;
color: #999;
}
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
.image-item {
position: relative;
width: 200rpx;
height: 200rpx;
.preview-image {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.delete-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
.emoji-picker {
margin-bottom: 20rpx;
}
.editor-actions {
margin-top: 30rpx;
}
</style>