feat: 题目与选项支持图片 (#291)

* feat: 题目与选项支持图片

* fix: 修复使用本地存储时文件访问路径不正确的问题

* fix: 图片编辑表单无法输入

* chore: 添加上传文件夹到gitignore

* fix: 两个#app的问题
This commit is contained in:
nil 2024-07-17 19:21:09 -07:00 committed by sudoooooo
parent 3388a15462
commit 358e822660
14 changed files with 212 additions and 65 deletions

3
.gitignore vendored
View File

@ -25,3 +25,6 @@ pnpm-debug.log*
*.sw?
.history
# 默认的上传文件夹
userUpload

View File

@ -52,6 +52,12 @@ http {
proxy_pass http://127.0.0.1:3000;
}
# 静态文件的默认存储文件夹
# 文件夹的配置在 server/src/modules/file/config/index.ts SERVER_LOCAL_CONFIG.FILE_KEY_PREFIX
location /userUpload {
proxy_pass http://127.0.0.1:3000;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 20M;
}

View File

@ -1,24 +1,9 @@
<template>
<div class="editor-wrapper border">
<Toolbar
:class="['toolbar', props.staticToolBar ? 'static-toolbar' : 'dynamic-toolbar']"
ref="toolbar"
v-show="showToolbar"
:editor="editorRef"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
class="editor"
ref="editor"
:modelValue="curValue"
:defaultConfig="editorConfig"
@onCreated="onCreated"
@onChange="onChange"
@onBlur="onBlur"
@onFocus="onFocus"
:mode="mode"
/>
<Toolbar :class="['toolbar', props.staticToolBar ? 'static-toolbar' : 'dynamic-toolbar']" ref="toolbar"
v-show="showToolbar" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
<Editor class="editor" ref="editor" :modelValue="curValue" :defaultConfig="editorConfig" @onCreated="onCreated"
@onChange="onChange" @onBlur="onBlur" @onFocus="onFocus" :mode="mode" />
</div>
</template>
@ -26,28 +11,62 @@
import '@wangeditor/editor/dist/css/style.css'
import './styles/reset-wangeditor.scss'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { ref, shallowRef, onBeforeMount, watch } from 'vue'
import { ref, shallowRef, onBeforeMount, watch, computed } from 'vue'
import { useStore } from 'vuex'
import { get as _get } from 'lodash-es'
const emit = defineEmits(['input', 'onFocus', 'change', 'blur', 'created'])
const model = defineModel()
const props = defineProps(['staticToolBar'])
const props = defineProps({
staticToolBar: { default: false, required: false },
needUploadImage: { default: false, required: false }
})
const curValue = ref('')
const editorRef = shallowRef()
const showToolbar = ref(props.staticToolBar || false)
const showToolbar = ref(props.staticToolBar)
const mode = 'simple'
const toolbarConfig = {
toolbarKeys: [
'color', //
'bgColor', //
'bold',
'insertLink' //
]
const toolbarConfig = computed(() => {
const config = {
toolbarKeys: [
'color', //
'bgColor', //
'bold',
'insertLink', //
]
}
if (props.needUploadImage) {
config.toolbarKeys.push('uploadImage')
}
return config
})
const editorConfig = {
MENU_CONF: {}
}
const editorConfig = {}
const store = useStore()
const token = _get(store, 'state.user.userInfo.token')
editorConfig.MENU_CONF['uploadImage'] = {
allowedFileTypes: ['image/jpeg', 'image/png'],
server: '/api/file/upload',
fieldName: 'file',
meta: {
//! channelchannel
channel: 'upload'
},
headers: {
Authorization: `Bearer ${token}`
},
customInsert(res, insertFn) {
const url = res.data.url
insertFn(url, '', '')
},
}
const setHtml = (newHtml) => {
const editor = editorRef.value
@ -114,6 +133,7 @@ onBeforeMount(() => {
.static-toolbar {
border-bottom: 1px solid #dedede;
}
.dynamic-toolbar {
position: absolute;
left: 0;

View File

@ -27,6 +27,16 @@ const isVideo = (html) => {
return html.indexOf('<video') > -1
}
export const cleanRichTextWithMediaTag = (text) => {
if (!text) {
return text === 0 ? 0 : ''
}
const html = transformHtmlTag(text).replace(/<img([\w\W]+?)\/>/g,'[图片]').replace(/<video.*\/video>/g,'[视频]')
const content = html.replace(/<[^<>]+>/g, '').replace(/&nbsp;/g, '')
return content
}
export const cleanRichText = (text) => {
if (!text) {
return text === 0 ? 0 : ''

View File

@ -1,7 +1,5 @@
<template>
<div id="app">
<router-view></router-view>
</div>
<router-view></router-view>
</template>
<script>
@ -14,6 +12,7 @@ export default {
@import url('./styles/icon.scss');
@import url('../materials/questions/common/css/icon.scss');
@import url('./styles/reset.scss');
@import url('./styles/common.scss');
html {
font-size: 50px;

View File

@ -13,28 +13,30 @@
v-for="item in props.tableData.listHead"
:key="item.field"
:prop="item.field"
:label="cleanRichText(item.title)"
:label="item.title"
minWidth="200"
>
<template #header="scope">
<div
class="table-row-cell"
@mouseover="onPopoverRefOver(scope, 'head')"
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
>
<span>
{{ scope.column.label.replace(/&nbsp;/g, '') }}
<div class="table-row-cell">
<span
class="table-row-head"
@mouseover="onPopoverRefOver(scope, 'head')"
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
v-html="item.title"
>
</span>
</div>
</template>
<template #default="scope">
<div
class="table-row-cell"
@mouseover="onPopoverRefOver(scope, 'content')"
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
>
<span>
{{ getContent(scope.row[scope.column.property]) }}
<div>
<span
class="table-row-cell"
@mouseover="onPopoverRefOver(scope, 'content')"
@click="onPreviewImage"
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
v-html="getContent(scope.row[scope.column.property])"
>
</span>
</div>
</template>
@ -48,15 +50,18 @@
width="400"
trigger="hover"
virtual-triggering
:content="popoverContent"
>
<div v-html="popoverContent"></div>
</el-popover>
<ImagePreview :url="previewImageUrl" v-model:visible="showPreviewImage"/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { cleanRichText } from '@/common/xss'
import ImagePreview from './ImagePreview.vue'
const props = defineProps({
tableData: {
@ -74,8 +79,8 @@ const popoverRefMap = ref({})
const popoverVirtualRef = ref()
const popoverContent = ref('')
const getContent = (value) => {
const content = cleanRichText(value)
const getContent = (content) => {
// const content = cleanRichText(value)
return content === 0 ? 0 : content || '未知'
}
const setPopoverContent = (content) => {
@ -93,6 +98,16 @@ const onPopoverRefOver = (scope, type) => {
}
setPopoverContent(popoverContent)
}
const previewImageUrl = ref('')
const showPreviewImage = ref(false)
const onPreviewImage = (e) => {
if (e.target.tagName === 'IMG') {
previewImageUrl.value = e.target.src
showPreviewImage.value = true
}
console.log(e.target.src)
}
</script>
<style lang="scss" scoped>
@ -121,17 +136,25 @@ const onPopoverRefOver = (scope, type) => {
}
.table-row-cell {
max-width: 100%;
display: inline-block;
white-space: nowrap;
/* 禁止自动换行 */
overflow: hidden;
/* 超出部分隐藏 */
text-overflow: ellipsis;
/* 显示省略号 */
white-space: nowrap; /* 禁止自动换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 显示省略号 */
:deep(img) {
height: 23px;
width: auto;
}
:deep(p) {
display: flex;
align-items: center;
}
}
}
:deep(.el-table td.el-table__cell div) {
font-size: 13px;
}
</style>
<style>
.el-popover p image {
max-width: 100%;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<Teleport to="body">
<Transition>
<div class="image-preview" v-show="visible">
<div class="close-btn" @click="visible = false"> <i-ep-close /> </div>
<div class="image-con">
<img :src="props.url" class="image-item" />
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
const props = defineProps<{
url: string
}>()
const visible = defineModel('visible')
</script>
<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.2s ease-out;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.image-preview {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba($color: #000000, $alpha: .4);
z-index: 2024;
display: flex;
align-items: center;
justify-content: center;
.image-con {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.image-item {
max-width: 80%;
max-height: 70%;
}
}
.close-btn {
position: absolute;
right: 20px;
top: 20px;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
background-color: #d0d0d0;
}
}
</style>

View File

@ -3,6 +3,7 @@
v-model="renderData"
handle=".question-wrapper.isSelected"
filter=".question-wrapper.isSelected .question.isSelected"
:preventOnFilter="false"
:group="DND_GROUP"
:onEnd="checkEnd"
:move="checkMove"

View File

@ -57,7 +57,7 @@
import { computed, inject, ref, type ComputedRef } from 'vue'
import { ConditionNode, RuleNode } from '@/common/logicEngine/RuleBuild'
import { CHOICES } from '@/common/typeEnum'
import { cleanRichText } from '@/common/xss'
import { cleanRichTextWithMediaTag } from '@/common/xss'
const renderData = inject<ComputedRef<Array<any>>>('renderData') || ref([])
const props = defineProps({
index: {
@ -88,7 +88,7 @@ const fieldList = computed(() => {
.filter((question: any) => CHOICES.includes(question.type))
.map((item: any) => {
return {
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichText(item.title)}`,
label: `${item.showIndex ? item.indexNumber + '.' : ''} ${cleanRichTextWithMediaTag(item.title)}`,
value: item.field
}
})
@ -102,7 +102,7 @@ const getRelyOptions = computed(() => {
return (
currentQuestion?.options.map((item: any) => {
return {
label: cleanRichText(item.text),
label: cleanRichTextWithMediaTag(item.text),
value: item.hash
}
}) || []

View File

@ -0,0 +1,8 @@
// 富文本标题选项中的预览弹窗的图片宽度
.el-popover {
p {
img {
max-width: 100%;
}
}
}

View File

@ -12,6 +12,7 @@
<span class="drag-handle qicon qicon-tuodong"></span>
<div class="input-box">
<RichEditor
:needUploadImage="true"
:modelValue="element.text"
@change="(value) => handleChange(index, value)"
/>

View File

@ -69,6 +69,7 @@ export default defineComponent({
<RichEditor
class="rich-editor"
modelValue={filterXSS(this.title)}
needUploadImage={true}
onChange={this.handleChange}
onCreated={(editor) => {
editor?.focus()

View File

@ -1,7 +1,5 @@
<template>
<div id="app">
<router-view></router-view>
</div>
<router-view></router-view>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue'

View File

@ -114,6 +114,11 @@ export default defineConfig({
'/api': {
target: 'http://127.0.0.1:3000',
changeOrigin: true
},
// 静态文件的默认存储文件夹
'/userUpload': {
target: 'http://127.0.0.1:3000',
changeOrigin: true
}
}
},