refactor:统一B,C端渲染组件,将其抽离到物料区 (#184)

* feat:抽离B,C端通用组件

* refactor: 通用组件统一渲染

* 兼容修复问题 (+1 squashed commit)
Squashed commits:
[8d168ef] refactor: 替换统一渲染组件

* refactor:统一B,C端渲染组件,将其抽离到物料区
This commit is contained in:
chaorenluo 2024-05-29 21:59:13 +08:00 committed by GitHub
parent b18677e709
commit 4115ff9847
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 825 additions and 800 deletions

View File

@ -1,85 +0,0 @@
<template>
<div class="container">
<div class="question-logo" @click="onSelect">
<img
v-if="logoImg !== ''"
:style="{ width: logoConf.logoImageWidth }"
class="bottom-logo"
:src="logoImg"
/>
<div class="logo-placeholder-wrapper" v-else>
<div class="logo-placeholder">LOGO</div>
<div class="no-logo-tip">若不配置logo该图片将不会在问卷中展示</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'LogoPreview',
props: {
logoConf: {
type: Object,
default: () => {}
},
isSelected: Boolean
},
data() {
return {}
},
methods: {
onSelect() {
this.$emit('select')
}
},
computed: {
logoImg() {
const { logoImage } = this.logoConf
return logoImage
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.container {
display: flex;
justify-content: center;
}
.question-logo {
max-width: 300px;
text-align: center;
padding: 0 0 0.6rem;
margin-top: -0.2rem;
cursor: pointer;
}
.logo-placeholder-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.logo-placeholder {
width: 200px;
height: 50px;
font-size: 40px;
color: #d8d8d8;
letter-spacing: 0;
line-height: 36px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #d8d8d8;
}
.no-logo-tip {
font-size: 13px;
line-height: 13px;
opacity: 0.5;
margin-top: 13px;
color: #92949d;
}
}
</style>

View File

@ -1,75 +0,0 @@
<template>
<div class="title-wrapper" @click="handleClick()">
<div class="main-title" :class="{ active: isSelected }">
<RichEditor
:modelValue="bannerConf?.titleConfig?.mainTitle"
@input="onTitleInput"
></RichEditor>
</div>
</div>
</template>
<script>
import RichEditor from '@/common/Editor/RichEditor.vue'
export default {
name: 'mainTitlePreview',
data() {
return {}
},
props: {
preview: {
type: Boolean,
default: false
},
bannerConf: {
type: Object
},
isSelected: {
type: Boolean
}
},
computed: {},
methods: {
handleClick() {
this.$emit('select')
},
onTitleInput(val) {
if (!this.isSelected) {
return
}
this.$emit('change', {
key: 'titleConfig.mainTitle',
value: val
})
}
},
components: {
RichEditor
}
}
</script>
<style lang="scss" scoped>
.title-wrapper {
padding: 15px;
}
.main-title {
border: 1px solid transparent;
&.active {
border: 1px solid #e3e4e6;
background-color: #f6f7f9;
box-shadow: 0 0 5px #dedede;
:deep(.w-e-text-container) {
background-color: #f6f7f9;
}
}
}
.main-title:hover {
border: 1px dashed #eee;
}
</style>

View File

@ -1,42 +0,0 @@
<template>
<div class="submit-wrapper" @click="onClick" :class="{ isSelected: isSelected }">
<el-button class="submit-btn" type="primary">{{ submitConf.submitTitle }}</el-button>
</div>
</template>
<script>
export default {
name: 'SubmitButton',
data() {
return {}
},
props: {
submitConf: Object,
isSelected: Boolean,
skinConf: {
type: Object,
required: true
}
},
methods: {
onClick() {
this.$emit('select')
}
}
}
</script>
<style lang="scss" scoped>
.submit-wrapper {
padding: 25px;
text-align: center;
.submit-btn {
color: white;
border: none;
width: 100%;
height: 44px;
background-color: var(--primary-color);
}
}
</style>

View File

@ -4,6 +4,7 @@
<div class="box content" ref="box">
<MainTitle
:bannerConf="bannerConf"
:readonly="false"
:is-selected="currentEditOne === 'mainTitle'"
@select="onSelectEditOne('mainTitle')"
@change="handleChange"
@ -18,6 +19,7 @@
/>
<SubmitButton
:submit-conf="submitConf"
:readonly="false"
:skin-conf="skinConf"
:is-selected="currentEditOne === 'submit'"
@select="onSelectEditOne('submit')"
@ -27,48 +29,74 @@
</div>
</template>
<script>
<script setup>
import { computed, ref, watch } from 'vue'
import communalLoader from '@materials/communals/communalLoader.js'
import MaterialGroup from '@/management/pages/edit/components/MaterialGroup.vue'
import MainTitle from '@/management/pages/edit/components/MainTitle.vue'
import SubmitButton from '@/management/pages/edit/components/SubmitButton.vue'
import { mapState, mapGetters } from 'vuex'
import { get as _get } from 'lodash-es'
import { useStore } from 'vuex'
export default {
name: 'PreviewPanel',
components: {
MainTitle,
SubmitButton,
MaterialGroup
},
data() {
const MainTitle = communalLoader.loadComponent('MainTitle')
const SubmitButton = communalLoader.loadComponent('SubmitButton')
const store = useStore()
const mainOperation = ref(null)
const materialGroup = ref(null)
const bannerConf = computed(() => store.state.edit.schema.bannerConf)
const submitConf = computed(() => store.state.edit.schema.submitConf)
const skinConf = computed(() => store.state.edit.schema.skinConf)
const questionDataList = computed(() => store.state.edit.schema.questionDataList)
const currentEditOne = computed(() => store.state.edit.currentEditOne)
const currentEditKey = computed(() => store.getters['edit/currentEditKey'])
const autoScrollData = computed(() => {
return {
isAnimating: false
currentEditOne: currentEditOne.value,
len: questionDataList.value.length
}
},
computed: {
...mapState({
bannerConf: (state) => _get(state, 'edit.schema.bannerConf'),
submitConf: (state) => _get(state, 'edit.schema.submitConf'),
skinConf: (state) => _get(state, 'edit.schema.skinConf'),
bottomConf: (state) => _get(state, 'edit.schema.bottomConf'),
questionDataList: (state) => _get(state, 'edit.schema.questionDataList'),
currentEditOne: (state) => _get(state, 'edit.currentEditOne')
}),
...mapGetters({
currentEditKey: 'edit/currentEditKey'
}),
autoScrollData() {
return {
currentEditOne: this.currentEditOne,
len: this.questionDataList.length
})
const onSelectEditOne = async (currentEditOne) => {
store.commit('edit/setCurrentEditOne', currentEditOne)
}
const handleChange = (data) => {
if (currentEditOne.value === null) {
return
}
const { key, value } = data
const resultKey = `${currentEditKey.value}.${key}`
store.dispatch('edit/changeSchema', { key: resultKey, value })
}
const onMainClick = (e) => {
if (e.target === mainOperation.value) {
store.commit('edit/setCurrentEditOne', null)
}
}
},
watch: {
skinConf: {
handler(skinConf) {
const { themeConf, backgroundConf, contentConf } = skinConf
const onQuestionOperation = (data) => {
switch (data.type) {
case 'move':
store.dispatch('edit/moveQuestion', {
index: data.index,
range: data.range
})
break
case 'delete':
store.dispatch('edit/deleteQuestion', { index: data.index })
break
case 'copy':
store.dispatch('edit/copyQuestion', { index: data.index })
break
default:
break
}
}
watch(
skinConf,
(newVal) => {
const { themeConf, backgroundConf, contentConf } = newVal
const root = document.documentElement
if (themeConf?.color) {
root.style.setProperty('--primary-color', themeConf?.color) //
@ -80,16 +108,19 @@ export default {
root.style.setProperty('--opacity', contentConf?.opacity / 100) //
}
},
immediate: true, //
{
immediate: true,
deep: true
},
autoScrollData(newVal) {
}
)
watch(autoScrollData, (newVal) => {
const { currentEditOne } = newVal
if (typeof currentEditOne === 'number') {
setTimeout(() => {
const field = this.questionDataList?.[currentEditOne]?.field
const field = questionDataList.value?.[currentEditOne]?.field
if (field) {
const questionModule = this.$refs.materialGroup.getQuestionRefByField(field)
const questionModule = materialGroup.value?.getQuestionRefByField(field)
if (questionModule && questionModule.$el) {
questionModule.$el.scrollIntoView({
behavior: 'smooth'
@ -98,68 +129,7 @@ export default {
}
}, 0)
}
}
},
methods: {
animate(dom, property, targetValue) {
const origin = dom[property]
const subVal = targetValue - origin
const flag = subVal < 0 ? -1 : 1
const step = flag * 50
const totalCount = Math.floor(subVal / step) + 1
let runCount = 0
const run = () => {
dom[property] += step
runCount++
if (runCount < totalCount) {
requestAnimationFrame(run)
} else {
this.isAnimating = false
}
}
requestAnimationFrame(run)
},
async onSelectEditOne(currentEditOne) {
this.$store.commit('edit/setCurrentEditOne', currentEditOne)
},
handleChange(data) {
if (this.currentEditOne === null) {
return
}
const { key, value } = data
const resultKey = `${this.currentEditKey}.${key}`
this.$store.dispatch('edit/changeSchema', { key: resultKey, value })
},
onMainClick(e) {
if (e.target === this.$refs.mainOperation) {
this.$store.commit('edit/setCurrentEditOne', null)
}
},
onQuestionOperation(data) {
switch (data.type) {
case 'move':
this.$store.dispatch('edit/moveQuestion', {
index: data.index,
range: data.range
})
break
case 'delete':
this.$store.dispatch('edit/deleteQuestion', { index: data.index })
break
case 'copy':
this.$store.dispatch('edit/copyQuestion', { index: data.index })
break
default:
break
}
}
}
}
</script>
<style lang="scss" scoped>
@ -198,6 +168,7 @@ export default {
.content {
background-color: #fff;
padding-top: 40px;
}
}
</style>

View File

@ -1,122 +0,0 @@
<template>
<div class="banner-wrap" @click="handleClick('banner')">
<div class="banner" v-if="bgImage">
<img :src="bgImage" />
</div>
<div v-else :class="{ 'empty-banner': true, 'banner-active': isSelected }">
<p>点击配置头图</p>
<p>若不配置头图将不在问卷中展示</p>
</div>
<div class="banner" v-if="bannerConf.bannerConfig.videoLink">
<div class="video">
<video
class="custom-video"
:poster="bannerConf.bannerConfigpostImg"
preload="auto"
controls
:src="bannerConf.bannerConfig.videoLink"
></video>
</div>
</div>
</div>
</template>
<script>
import { get as _get } from 'lodash-es'
export default {
name: 'BannerContent',
data() {
return {}
},
props: {
bannerConf: {
type: Object,
default: () => {}
},
isSelected: {
type: Boolean
}
},
computed: {
bgImage() {
return _get(this.bannerConf, 'bannerConfig.bgImage', '')
}
},
methods: {
handleClick() {
this.$emit('select')
}
},
components: {}
}
</script>
<style lang="scss" scoped>
.banner-preview {
width: 100%;
}
.banner-wrap {
width: 100%;
.banner {
width: 100%;
display: flex;
justify-content: center;
img {
width: 100%;
}
.custom-video {
margin: 0 auto;
width: 100%;
display: block;
}
}
.empty-banner {
height: 120px;
border: 1px dashed #e3e4e8;
p {
text-align: center;
color: #c8c9cd;
font-size: 16px;
&:first-child {
font-size: 24px;
color: #92949d;
margin: 20px 0 24px 0;
}
}
&.banner-active {
background-color: #f2f4f7;
box-shadow: 0 0 5px #e3e4e8;
}
}
}
.title-wrapper {
padding: 15px;
}
.main-title {
border: 1px solid transparent;
&.active {
border: 1px solid #e3e4e6;
background-color: #f6f7f9;
box-shadow: 0 0 5px #dedede;
:deep(.w-e-text-container) {
background-color: #f6f7f9;
}
}
}
.main-title:hover {
border: 1px dashed #eee;
}
</style>

View File

@ -3,62 +3,70 @@
<div class="operation-wrapper">
<div class="box" ref="box">
<div class="mask"></div>
<BannerContent :bannerConf="bannerConf" />
<HeaderContent :bannerConf="bannerConf" :readonly="false" />
<div class="content">
<MainTitle :isSelected="false" :bannerConf="bannerConf" />
<MainTitle :isSelected="false" :bannerConf="bannerConf" :readonly="false" />
<MaterialGroup :questionDataList="questionDataList" ref="MaterialGroup" />
<SubmitButton
:submit-conf="submitConf"
:skin-conf="skinConf"
:readonly="false"
:is-selected="currentEditOne === 'submit'"
/>
<LogoPreview :logo-conf="bottomConf" :is-selected="currentEditOne === 'logo'" />
<LogoIcon
:logo-conf="bottomConf"
:readonly="false"
:is-selected="currentEditOne === 'logo'"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { computed, defineComponent } from 'vue'
import MaterialGroup from '@/management/pages/edit/components/MaterialGroup.vue'
import BannerContent from '../components/BannerContent.vue'
import MainTitle from '@/management/pages/edit/components/MainTitle.vue'
import SubmitButton from '@/management/pages/edit/components/SubmitButton.vue'
import LogoPreview from '@/management/pages/edit/components/LogoPreview.vue'
import { mapState, mapGetters } from 'vuex'
import { get as _get } from 'lodash-es'
import { useStore } from 'vuex'
import communalLoader from '@materials/communals/communalLoader.js'
export default {
name: 'PreviewPanel',
const HeaderContent = ()=>communalLoader.loadComponent('HeaderContent')
const MainTitle = ()=>communalLoader.loadComponent('MainTitle')
const SubmitButton = ()=>communalLoader.loadComponent('SubmitButton')
const LogoIcon = ()=>communalLoader.loadComponent('LogoIcon')
export default defineComponent({
components: {
BannerContent,
MainTitle,
SubmitButton,
LogoPreview,
MaterialGroup
MaterialGroup,
HeaderContent:HeaderContent(),
MainTitle:MainTitle(),
SubmitButton:SubmitButton(),
LogoIcon:LogoIcon()
},
data() {
setup() {
const store = useStore()
const bannerConf = computed(() => store.state.edit.schema.bannerConf)
const submitConf = computed(() => store.state.edit.schema.submitConf)
const bottomConf = computed(() => store.state.edit.schema.bottomConf)
const skinConf = computed(() => store.state.edit.schema.skinConf)
const questionDataList = computed(() => store.state.edit.schema.questionDataList)
const currentEditOne = computed(() => store.state.edit.currentEditOne)
const currentEditKey = computed(() => store.getters['edit/currentEditKey'])
return {
isAnimating: false
bannerConf,
submitConf,
bottomConf,
skinConf,
questionDataList,
currentEditOne,
currentEditKey
}
},
computed: {
...mapState({
bannerConf: (state) => _get(state, 'edit.schema.bannerConf'),
submitConf: (state) => _get(state, 'edit.schema.submitConf'),
bottomConf: (state) => _get(state, 'edit.schema.bottomConf'),
skinConf: (state) => _get(state, 'edit.schema.skinConf'),
questionDataList: (state) => _get(state, 'edit.schema.questionDataList'),
currentEditOne: (state) => _get(state, 'edit.currentEditOne')
}),
...mapGetters({
currentEditKey: 'edit/currentEditKey'
})
},
watch: {
skinConf: {
handler(skinConf) {
const { themeConf, backgroundConf, contentConf } = skinConf
handler(newVal) {
const { themeConf, backgroundConf, contentConf } = newVal
const root = document.documentElement
if (themeConf?.color) {
root.style.setProperty('--primary-color', themeConf?.color) //
@ -70,36 +78,11 @@ export default {
root.style.setProperty('--opacity', contentConf?.opacity / 100) //
}
},
immediate: true, //
immediate: true,
deep: true
}
},
methods: {
animate(dom, property, targetValue) {
const origin = dom[property]
const subVal = targetValue - origin
const flag = subVal < 0 ? -1 : 1
const step = flag * 50
const totalCount = Math.floor(subVal / step) + 1
let runCount = 0
const run = () => {
dom[property] += step
runCount++
if (runCount < totalCount) {
requestAnimationFrame(run)
} else {
this.isAnimating = false
}
}
requestAnimationFrame(run)
}
}
}
})
</script>
<style lang="scss" scoped>
@ -140,6 +123,7 @@ export default {
.box {
background-color: var(--primary-background-color);
position: relative;
.mask {
position: absolute;
top: 0;
@ -148,6 +132,7 @@ export default {
right: 0;
z-index: 999;
}
.content {
margin: 0 0.3rem;
background: rgba(255, 255, 255, var(--opacity));

View File

@ -0,0 +1,11 @@
// 对链接做一个兼容转换支持用户不配置http开头或者配置 // 开头
export const formatLink = (url) => {
url = url.trim()
if (!url) {
return url
}
if (url.startsWith('http') || url.startsWith('//')) {
return url
}
return `http://${url}`
}

View File

@ -0,0 +1,15 @@
import { defineAsyncComponent } from 'vue'
export class CommunalLoader {
loadComponent(path) {
return defineAsyncComponent({
loader: () => import(`./widgets/${path}/index.jsx`),
delay: 200,
timeout: 3000
})
}
}
const communalLoader = new CommunalLoader()
export default communalLoader

View File

@ -0,0 +1,82 @@
import { defineComponent, computed } from 'vue'
import { get as _get } from 'lodash-es'
import { formatLink } from '@materials/communals/common/utils'
export default defineComponent({
name: 'HeaderContent',
props: {
bannerConf: {
type: Object,
default: () => {}
},
readonly: {
type: Boolean,
default: false
},
isSelected: {
type: Boolean,
default: false
}
},
setup(props) {
const bgImage = computed(() => {
return _get(props.bannerConf, 'bannerConfig.bgImage', '')
})
const onBannerClick = () => {
const allow = _get(props.bannerConf, 'bannerConfig.bgImageAllowJump', false)
const jumpLink = _get(props.bannerConf, 'bannerConfig.bgImageJumpLink', '')
if (!allow || !jumpLink) {
return
}
window.open(formatLink(jumpLink))
}
const bannerRender = () => {
let attribute = {
class: 'banner-img'
}
if (props.readonly) {
const allow = _get(props.bannerConf, 'bannerConfig.bgImageAllowJump', false)
attribute = {
class: `${attribute.class} ${allow ? 'pointer' : ''}`,
onClick: onBannerClick
}
}
if (bgImage.value) {
return (
<div class="banner">
<img src={bgImage.value} {...attribute} />
</div>
)
}
if (!bgImage.value && !props.readonly) {
let classStr = 'empty-banner'
if (props.isSelected) {
classStr += 'banner-active'
}
return (
<div class={classStr}>
<p>点击配置头图</p>
<p>若不配置头图将不在问卷中展示</p>
</div>
)
}
return ''
}
return {
bgImage,
// emptyBannerRender,
bannerRender
}
},
render() {
return (
<div class="header-banner-wrap">
{this.bannerRender()}
{/* {this.emptyBannerRender()} */}
</div>
)
}
})

View File

@ -0,0 +1,66 @@
import { defineComponent, computed } from 'vue'
export default defineComponent({
name: 'HeaderVideo',
props: {
bannerConf: {
type: Object,
default: () => {}
},
readonly: {
type: Boolean,
default: false
}
},
setup(props) {
const attributeVideo = computed(() => {
const { bannerConf, readonly } = props
let data = {
id: 'video',
preload: 'auto',
style: readonly ? 'margin: 0 auto; width: 100%; display: block;' : '',
poster: bannerConf?.bannerConfig?.postImg,
controls: '',
class: readonly ? '' : 'custom-video'
}
return data
})
const play = () => {
const video = document.getElementById('video')
if (!video) return
document.querySelector('.play-icon').style.display = 'none'
document.querySelector('.video-modal').style.display = 'none'
video.play()
}
return {
attributeVideo,
props,
play
}
},
render() {
const { readonly } = this.props
return (
<div class="header-video-warp">
<div class="video">
<video {...this.attributeVideo} >
<source src={this.bannerConf?.bannerConfig?.videoLink} type="video/mp4" />
</video>
{readonly ? (
<>
<div
class="video-modal"
style={`background-image:url(${this.attributeVideo.poster})`}
></div>
<div class="iconfont icon-kaishi play-icon" onClick={this.play}></div>
</>
) : (
''
)}
</div>
</div>
)
}
})

View File

@ -0,0 +1,47 @@
import { defineComponent } from 'vue'
import './index.scss'
import HeaderBanner from './Components/HeaderBanner'
import HeaderVideo from './Components/HeaderVideo'
export default defineComponent({
name: 'HeaderContent',
props: {
bannerConf: {
type: Object,
default: () => {}
},
readonly: {
type: Boolean,
default: false
},
isSelected: {
type: Boolean,
default: false
}
},
emits: ['select'],
setup(props, { emit }) {
const handleClick = () => {
if (props.readonly) return
emit('select')
}
return {
handleClick,
props
}
},
render() {
const { bannerConf, readonly, isSelected } = this.props
return (
<div class="header-content-warp" onClick={this.handleClick}>
<HeaderBanner bannerConf={bannerConf} readonly={readonly} isSelected={isSelected} />
{bannerConf?.bannerConfig?.videoLink ? (
<HeaderVideo bannerConf={bannerConf} readonly={readonly} />
) : (
''
)}
</div>
)
}
})

View File

@ -0,0 +1,69 @@
.header-content-warp {
width: 100%;
.header-banner-wrap {
.banner {
width: 100%;
display: flex;
justify-content: center;
}
.banner-img {
width: 100%;
&.pointer {
cursor: pointer;
}
}
.empty-banner {
height: 120px;
border: 1px dashed #e3e4e8;
p {
text-align: center;
color: #c8c9cd;
font-size: 16px;
&:first-child {
font-size: 24px;
color: #92949d;
margin: 20px 0 24px 0;
}
}
&.banner-active {
background-color: #f2f4f7;
box-shadow: 0 0 5px #e3e4e8;
}
}
}
.header-video-warp {
.video {
width: 100%;
height: 100%;
position: relative;
}
.video-modal {
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
z-index: 100;
}
.play-icon {
position: absolute;
background-repeat: no-repeat;
background-position: center;
font-size: 1.8rem;
left: 50%;
top: 49%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
z-index: 101;
}
.custom-video {
margin: 0 auto;
width: 100%;
display: block;
}
}
}

View File

@ -0,0 +1,67 @@
import { defineComponent, computed } from 'vue'
import './index.scss'
export default defineComponent({
name: 'LogoIcon',
props: {
logoConf: {
type: Object,
default: () => ({
logoImageWidth: Number,
logoImage: String
})
},
readonly: {
type: Boolean,
default: false
}
},
emits: ['select'],
setup(props, { emit }) {
const logoImage = computed(() => {
return props.logoConf?.logoImage
})
const logoImageWidth = computed(() => {
return props.logoConf?.logoImageWidth
})
const onSelect = () => {
if (props.readonly) return
emit('select')
}
const noLogoRender = () => {
if (!props.readonly) {
return (
<div class="logo-placeholder-wrapper">
<div class="logo-placeholder">LOGO</div>
<div class="no-logo-tip">若不配置logo该图片将不会在问卷中展示</div>
</div>
)
}
}
return {
logoImage,
logoImageWidth,
props,
onSelect,
noLogoRender
}
},
render() {
return (
<div class="logo-icon-warp" onClick={this.onSelect}>
<div class="question-logo">
{this.logoImage ? (
<img src={this.logoImage} style={{width: this.logoImageWidth}} />
) : (
this.noLogoRender()
)}
</div>
</div>
)
}
})

View File

@ -0,0 +1,43 @@
.logo-icon-warp {
display: flex;
justify-content: center;
.logo-wrapper {
text-align: center;
font-size: 0;
padding: 0.1rem 0 0.5rem;
}
.question-logo {
max-width: 300px;
text-align: center;
padding: 0 0 0.6rem;
margin-top: -0.2rem;
cursor: pointer;
}
.logo-placeholder-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.logo-placeholder {
width: 200px;
height: 50px;
font-size: 40px;
color: #d8d8d8;
letter-spacing: 0;
line-height: 36px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #d8d8d8;
}
.no-logo-tip {
font-size: 13px;
line-height: 13px;
opacity: 0.5;
margin-top: 13px;
color: #92949d;
}
}
}

View File

@ -0,0 +1,103 @@
import { defineComponent, computed,shallowRef,defineAsyncComponent} from 'vue'
import '@/render/styles/variable.scss'
import './index.scss'
export default defineComponent({
name: 'MainTitle',
props: {
bannerConf: {
type: Object,
default: () => {}
},
readonly: {
type: Boolean,
default: false
},
isSelected: {
type: Boolean,
default: false
}
},
emits: ['select', 'change'],
setup(props, { emit }) {
const titleClass = computed(() => {
let classStr = ''
if (!props.readonly) {
classStr = `main-title ${props.isSelected ? 'active' : ''}`
} else {
classStr = 'titlePanel'
}
return classStr
})
const isTitleHide = computed(() => {
if (props.readonly && !mainTitle.value) {
return false
}
return true
})
const mainTitle = computed(() => {
return props.bannerConf.titleConfig?.mainTitle
})
const handleClick = () => {
if (props.readonly) return
emit('select')
}
const onTitleInput = (val) => {
if (!props.isSelected) {
return
}
emit('change', {
key: 'titleConfig.mainTitle',
value: val
})
}
const richEditorView = shallowRef(null)
if (!props.readonly) {
richEditorView.value = defineAsyncComponent(
() => import('@/common/Editor/RichEditor.vue')
)
}
return {
props,
titleClass,
isTitleHide,
mainTitle,
richEditorView,
handleClick,
onTitleInput
}
},
render() {
const { readonly,mainTitle,onTitleInput,richEditorView} = this;
return (
<div
class={['main-title-warp', !readonly ? 'pd15' : '']}
onClick={this.handleClick}
>
{this.isTitleHide ? (
<div class={this.titleClass}>
{!readonly ? (
<richEditorView
modelValue={mainTitle}
onInput={onTitleInput}
placeholder="请输入标题"
class="mainTitle"
/>
) : (
<div class="mainTitle" v-html={mainTitle}></div>
)}
</div>
) : (
''
)}
</div>
)
}
})

View File

@ -0,0 +1,63 @@
.main-title-warp {
.mainTitle {
font-size: 0.28rem;
line-height: 0.4rem;
color: $title-color;
ol {
list-style: decimal;
}
ul {
list-style: disc;
}
img {
width: 100%;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
line-height: 0.6rem;
font-size: 0.28rem;
color: $title-color-deep;
margin-bottom: 0.35rem;
}
p {
margin-bottom: 0;
}
}
.title-wrapper {
padding: 15px;
}
.main-title {
border: 1px solid transparent;
&.active {
border: 1px solid #e3e4e6;
background-color: #f6f7f9;
box-shadow: 0 0 5px #dedede;
:deep(.w-e-text-container) {
background-color: #f6f7f9;
}
}
}
.main-title:hover {
border: 1px dashed #eee;
}
&.pd15 {
padding: 15px;
}
.titlePanel {
position: relative;
width: 100%;
padding: 0.4rem 0.4rem;
box-sizing: border-box;
}
}

View File

@ -0,0 +1,56 @@
import { defineComponent } from 'vue'
import '@/render/styles/variable.scss'
import './index.scss'
export default defineComponent({
name: 'SubmitButton',
props: {
submitConf: Object,
skinConf: {
type: Object,
default: () => ({})
},
readonly: Boolean,
validate: Function,
renderData: Array
},
emits: ['submit', 'select'],
setup(props, { emit }) {
const submit = (e) => {
if (!props.readonly) return
const validate = props.validate
if (e) {
e.preventDefault()
validate((valid) => {
if (valid) {
emit('submit')
}
})
}
}
const handleClick = () => {
if (props.readonly) return
emit('select')
}
return {
props,
submit,
handleClick
}
},
render() {
const { submitConf } = this.props
return (
<div
class={['submit-warp', 'preview-submit_wrapper']}
onClick={this.handleClick}
>
<button class="submit-btn" type="primary" onClick={this.submit}>
{submitConf.submitTitle}
</button>
</div>
)
}
})

View File

@ -0,0 +1,27 @@
.submit-warp {
&.question-submit_wrapper {
margin: 0 0.4rem;
font-size: 0;
position: relative;
}
&.preview-submit_wrapper {
padding: 25px;
text-align: center;
}
.submit-btn {
position: relative;
display: inline-block;
width: 100%;
padding: 0.25rem 0;
font-size: 0.36rem;
line-height: 0.5rem;
font-weight: 500;
text-align: center;
color: #fff;
background: var(--primary-color);
border-radius: 0.08rem;
margin: 0.4rem 0;
cursor: pointer;
border: none;
}
}

View File

@ -9,7 +9,7 @@
"
>
</Component>
<LogoIcon v-if="!['successPage', 'indexPage'].includes(store.state.router)" />
<LogoIcon v-if="!['successPage', 'indexPage'].includes(store.state.router)" :logo-conf="logoConf" :readonly="true" />
</div>
</template>
<script setup lang="ts">
@ -24,12 +24,15 @@ import IndexPage from './pages/IndexPage.vue'
import ErrorPage from './pages/ErrorPage.vue'
import SuccessPage from './pages/SuccessPage.vue'
import AlertDialog from './components/AlertDialog.vue'
import LogoIcon from './components/LogoIcon.vue'
// @ts-ignore
import communalLoader from '@materials/communals/communalLoader.js'
import { get as _get, upperFirst } from 'lodash-es'
import { initRuleEngine } from '@/render/hooks/useRuleEngine.js'
const LogoIcon = communalLoader.loadComponent('LogoIcon')
const store = useStore()
const logoConf = computed(() => store.state?.bottomConf || {})
const skinConf = computed(() => _get(store, 'state.skinConf', {}))
const components = {
EmptyPage,

View File

@ -1,123 +0,0 @@
<template>
<div class="question-header">
<div class="banner" v-if="bannerConf.bannerConfig && bannerConf.bannerConfig.bgImage">
<img
class="banner-img"
:src="bannerConf.bannerConfig.bgImage"
:class="{ pointer: bannerConf.bannerConfig.bgImageAllowJump }"
@click="handleBannerClick"
/>
</div>
<div class="banner" v-if="bannerConf.bannerConfig && bannerConf.bannerConfig.videoLink">
<div class="video">
<video
ref="videoRef"
controls
style="margin: 0 auto; width: 100%; display: block"
preload="auto"
:poster="bannerConf.bannerConfig.postImg"
>
<source :src="bannerConf.bannerConfig.videoLink" type="video/mp4" />
</video>
<div
class="video-modal"
:style="{
backgroundImage:
bannerConf.bannerConfig.postImg && `url(${bannerConf.bannerConfig.postImg})`,
display: displayModel
}"
></div>
<div
class="iconfont icon-kaishi play-icon"
:style="{ display: displayModel }"
@click="handlePlay()"
></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useStore } from 'vuex'
import { get as _get } from 'lodash-es'
import { formatLink } from '../utils/index.js'
const store = useStore()
const bannerConf = computed<any>(() => _get(store, 'state.bannerConf', {}))
const handleBannerClick = () => {
const allow = _get(bannerConf.value, 'bannerConfig.bgImageAllowJump', false)
const jumpLink = _get(bannerConf.value, 'bannerConfig.bgImageJumpLink', '')
if (!allow || !jumpLink) {
return
}
window.open(formatLink(jumpLink))
}
const videoRef = ref<HTMLVideoElement | null>(null)
const displayModel = ref('block')
const handlePlay = () => {
if (bannerConf.value.bannerConfig && bannerConf.value.bannerConfig.videoLink) {
videoRef.value?.play()
displayModel.value = 'none'
}
}
</script>
<style lang="scss" scoped>
.question-header {
.banner,
.titlePanel {
position: relative;
width: 100%;
}
.banner {
display: flex;
.banner-img {
width: 100%;
&.pointer {
cursor: pointer;
}
}
}
.titlePanel {
padding: 0.4rem 0.4rem;
box-sizing: border-box;
}
.video {
width: 100%;
height: 100%;
}
.video-modal {
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
z-index: 100;
}
.play-icon {
position: absolute;
background-repeat: no-repeat;
background-position: center;
font-size: 1.8rem;
left: 50%;
top: 49%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
z-index: 101;
}
}
</style>

View File

@ -1,29 +0,0 @@
<template>
<div class="container">
<div v-if="logoImage" class="logo-wrapper">
<img :style="{ width: !isMobile ? '20%' : logoImageWidth || '20%' }" :src="logoImage" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const logoImage = computed(() => store.state?.bottomConf?.logoImage)
const logoImageWidth = computed(() => store.state?.bottomConf?.logoImageWidth)
const isMobile = computed(() => store.state?.isMobile)
</script>
<style lang="scss" scoped>
.container {
display: flex;
justify-content: center;
}
.logo-wrapper {
text-align: center;
font-size: 0;
padding: 0.1rem 0 0.5rem;
}
</style>

View File

@ -1,60 +0,0 @@
<template>
<div class="question-header">
<div class="titlePanel" v-if="bannerConf.titleConfig && bannerConf.titleConfig.mainTitle">
<div
class="mainTitle"
v-if="bannerConf.titleConfig.mainTitle"
v-html="bannerConf.titleConfig.mainTitle"
></div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const bannerConf = computed(() => store.state?.bannerConf || {})
</script>
<style lang="scss" scoped>
@import '@/render/styles/variable.scss';
.question-header {
.titlePanel {
position: relative;
width: 100%;
padding: 0.4rem 0.4rem;
box-sizing: border-box;
}
}
.mainTitle {
font-size: 0.28rem;
line-height: 0.4rem;
color: $title-color;
ol {
list-style: decimal;
}
ul {
list-style: disc;
}
img {
width: 100%;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
line-height: 0.6rem;
color: $title-color-deep;
margin-bottom: 0.35rem;
}
p {
margin-bottom: 0;
}
}
</style>

View File

@ -1,62 +0,0 @@
<template>
<div class="question-submit_wrapper">
<button class="question-submit-btn" @click="handleSubmit">
{{ submitConf.submitTitle }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useStore } from 'vuex'
interface Props {
validate: (fn: (valid: boolean) => void) => void
renderData?: Array<any>
}
interface Emit {
(ev: 'submit'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const store = useStore()
const submitConf = computed(() => store.state?.submitConf || {})
const handleSubmit = (e: Event) => {
const validate = props.validate
if (e) {
e.preventDefault()
validate((valid) => {
if (valid) {
emit('submit')
}
})
}
}
</script>
<style lang="scss" scoped>
@import '@/render/styles/variable.scss';
.question-submit_wrapper {
margin: 0 0.4rem;
font-size: 0;
position: relative;
.question-submit-btn {
position: relative;
display: inline-block;
width: 100%;
padding: 0.25rem 0;
font-size: 0.36rem;
line-height: 0.5rem;
font-weight: 500;
text-align: center;
color: #fff;
background: var(--primary-color);
border-radius: 0.08rem;
margin: 0.4rem 0;
cursor: pointer;
}
}
</style>

View File

@ -10,7 +10,6 @@ export const useShowInput = (questionKey) => {
if (curRange.isShowInput) {
const rangeKey = `${questionKey}_${key}`
othersValue[rangeKey] = formValues[rangeKey]
;(curRange.othersKey = rangeKey), (curRange.othersValue = formValues[rangeKey])
if (!questionVal.toString().includes(key) && formValues[rangeKey]) {
// 如果分值被未被选中且对应的填写更多有值,则清空填写更多

View File

@ -2,12 +2,18 @@
<div class="index">
<ProgressBar />
<div class="wrapper" ref="boxRef">
<HeaderSetter></HeaderSetter>
<HeaderContent :bannerConf="bannerConf" :readonly="true" />
<div class="content">
<MainTitle></MainTitle>
<MainTitle :bannerConf="bannerConf" :readonly="true"></MainTitle>
<MainRenderer ref="mainRef"></MainRenderer>
<Submit :validate="validate" :renderData="renderData" @submit="handleSubmit"></Submit>
<LogoIcon />
<SubmitButton
:validate="validate"
:submitConf="submitConf"
:readonly="true"
:renderData="renderData"
@submit="handleSubmit"
></SubmitButton>
<LogoIcon :logo-conf="logoConf" :readonly="true" />
</div>
</div>
</div>
@ -15,15 +21,12 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useStore } from 'vuex'
import HeaderSetter from '../components/HeaderSetter.vue'
import MainTitle from '../components/MainTitle.vue'
import Submit from '../components/SubmitSetter.vue'
// @ts-ignore
import communalLoader from '@materials/communals/communalLoader.js'
import MainRenderer from '../components/MainRenderer.vue'
import AlertDialog from '../components/AlertDialog.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import ProgressBar from '../components/ProgressBar.vue'
import LogoIcon from '../components/LogoIcon.vue'
import { submitForm } from '../api/survey'
import encrypt from '../utils/encrypt'
@ -40,6 +43,11 @@ withDefaults(defineProps<Props>(), {
isMobile: false
})
const HeaderContent = communalLoader.loadComponent('HeaderContent')
const MainTitle = communalLoader.loadComponent('MainTitle')
const SubmitButton = communalLoader.loadComponent('SubmitButton')
const LogoIcon = communalLoader.loadComponent('LogoIcon')
const mainRef = ref<any>()
const boxRef = ref<HTMLElement>()
@ -48,7 +56,10 @@ const confirm = useCommandComponent(ConfirmDialog)
const store = useStore()
const bannerConf = computed(() => store.state?.bannerConf || {})
const renderData = computed(() => store.getters.renderData)
const submitConf = computed(() => store.state?.submitConf || {})
const logoConf = computed(() => store.state?.bottomConf || {})
const validate = (cbk: (v: boolean) => void) => {
const index = 0
@ -58,12 +69,12 @@ const validate = (cbk: (v: boolean) => void) => {
const normalizationRequestBody = () => {
const enterTime = store.state.enterTime
const encryptInfo = store.state.encryptInfo
const formValues = store.state.formValues
const formModel = store.getters.formModel
const surveyPath = store.state.surveyPath
const result: any = {
surveyPath,
data: JSON.stringify(formValues),
data: JSON.stringify(formModel),
difTime: Date.now() - enterTime,
clientTime: Date.now()
}
@ -126,11 +137,13 @@ const handleSubmit = () => {
<style scoped lang="scss">
.index {
min-height: 100%;
.wrapper {
min-height: 100%;
background-color: var(--primary-background-color);
display: flex;
flex-direction: column;
.content {
flex: 1;
margin: 0 0.3rem;

View File

@ -5,17 +5,20 @@
<img src="/imgs/icons/success.webp" />
<div class="msg" v-html="successMsg"></div>
</div>
<LogoIcon />
<LogoIcon :logo-conf="logoConf" :readonly="true" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useStore } from 'vuex'
import LogoIcon from '../components/LogoIcon.vue'
// @ts-ignore
import communalLoader from '@materials/communals/communalLoader.js'
const LogoIcon = communalLoader.loadComponent('LogoIcon')
const store = useStore()
const logoConf = computed(() => store.state?.bottomConf || {})
const successMsg = computed(() => {
const msgContent = store.state?.submitConf?.msgContent || {}
return msgContent?.msg_200 || '提交成功'