更新:课程详情的初步代码
This commit is contained in:
491
components/comment/CommentEditor.vue
Normal file
491
components/comment/CommentEditor.vue
Normal file
@@ -0,0 +1,491 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user