更新:课程详情的初步代码

This commit is contained in:
2025-11-14 15:13:21 +08:00
parent e7e0597026
commit 21b03635a2
25 changed files with 4958 additions and 12 deletions

View 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>