feat: 题目与选项支持图片 (#291)
* feat: 题目与选项支持图片 * fix: 修复使用本地存储时文件访问路径不正确的问题 * fix: 图片编辑表单无法输入 * chore: 添加上传文件夹到gitignore * fix: 两个#app的问题
This commit is contained in:
parent
3388a15462
commit
358e822660
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,3 +25,6 @@ pnpm-debug.log*
|
||||
*.sw?
|
||||
|
||||
.history
|
||||
|
||||
# 默认的上传文件夹
|
||||
userUpload
|
||||
|
@ -51,6 +51,12 @@ http {
|
||||
location /api {
|
||||
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;
|
||||
|
@ -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: {
|
||||
//! 此处的channel需要跟上传接口内配置的channel一致
|
||||
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;
|
||||
|
@ -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(/ /g, '')
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export const cleanRichText = (text) => {
|
||||
if (!text) {
|
||||
return text === 0 ? 0 : ''
|
||||
|
@ -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;
|
||||
|
@ -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(/ /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>
|
||||
|
@ -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>
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}) || []
|
||||
|
8
web/src/management/styles/common.scss
Normal file
8
web/src/management/styles/common.scss
Normal file
@ -0,0 +1,8 @@
|
||||
// 富文本标题、选项中的预览弹窗的图片宽度
|
||||
.el-popover {
|
||||
p {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)"
|
||||
/>
|
||||
|
@ -69,6 +69,7 @@ export default defineComponent({
|
||||
<RichEditor
|
||||
class="rich-editor"
|
||||
modelValue={filterXSS(this.title)}
|
||||
needUploadImage={true}
|
||||
onChange={this.handleChange}
|
||||
onCreated={(editor) => {
|
||||
editor?.focus()
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user