feat: 皮肤设置2.0 (#426)
* fix: 修复windows上传文件路径反斜杠问题 * feat: 补全服务端skinConf定义 * feat: web端皮肤背景设置2.0 * feat: 删除console.log * feat: 皮肤设置内容结果增加背景设置以及应用皮肤设置方法抽离 --------- Co-authored-by: jiangchunfu <jiangchunfu@kaike.la> Co-authored-by: sudoooooo <zjbbabybaby@gmail.com>
This commit is contained in:
parent
4bc8fbc557
commit
4b8719ab9c
@ -135,6 +135,17 @@ export interface BaseConf {
|
||||
export interface SkinConf {
|
||||
skinColor: string;
|
||||
inputBgColor: string;
|
||||
backgroundConf: {
|
||||
color: string;
|
||||
type: string;
|
||||
image: string;
|
||||
};
|
||||
contentConf: {
|
||||
opacity: number;
|
||||
};
|
||||
themeConf: {
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BottomConf {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { join, dirname } from 'path';
|
||||
import { join, dirname, sep } from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { FileUploadHandler } from './uploadHandler.interface';
|
||||
@ -23,7 +23,9 @@ export class LocalHandler implements FileUploadHandler {
|
||||
const filePath = join(
|
||||
options?.pathPrefix ? options?.pathPrefix : '',
|
||||
filename,
|
||||
);
|
||||
)
|
||||
.split(sep)
|
||||
.join('/');
|
||||
const physicalPath = join(this.physicalRootPath, filePath);
|
||||
await fse.mkdir(dirname(physicalPath), { recursive: true });
|
||||
const writeStream = createWriteStream(physicalPath);
|
||||
|
@ -249,6 +249,8 @@ describe('DataStatisticController', () => {
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
|
@ -44,6 +44,17 @@ export const mockSensitiveResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
@ -327,6 +338,17 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
|
@ -176,7 +176,21 @@ describe('SurveyController', () => {
|
||||
endTime: '2034-01-23 21:59:05',
|
||||
},
|
||||
bottomConf: { logoImage: '/imgs/Logo.webp', logoImageWidth: '60%' },
|
||||
skinConf: { skinColor: '#4a4c5b', inputBgColor: '#ffffff' },
|
||||
skinConf: {
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
},
|
||||
submitConf: {},
|
||||
dataConf: {
|
||||
dataList: [],
|
||||
|
@ -50,7 +50,9 @@
|
||||
"skinColor": "#4a4c5b",
|
||||
"inputBgColor": "#ffffff",
|
||||
"backgroundConf": {
|
||||
"color": "#ffffff"
|
||||
"color": "#ffffff",
|
||||
"type": "color",
|
||||
"image": ""
|
||||
},
|
||||
"themeConf": {
|
||||
"color": "#ffa600"
|
||||
|
@ -44,6 +44,17 @@ export const mockResponseSchema: ResponseSchema = {
|
||||
logoImageWidth: '60%',
|
||||
},
|
||||
skinConf: {
|
||||
backgroundConf: {
|
||||
color: '#fff',
|
||||
type: 'color',
|
||||
image: '',
|
||||
},
|
||||
themeConf: {
|
||||
color: '#ffa600',
|
||||
},
|
||||
contentConf: {
|
||||
opacity: 100,
|
||||
},
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
|
BIN
web/public/imgs/icons/upload.png
Normal file
BIN
web/public/imgs/icons/upload.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 340 B |
21
web/src/common/utils/applySkinConfig.ts
Normal file
21
web/src/common/utils/applySkinConfig.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export default function (skinConfig: any) {
|
||||
const root = document.documentElement
|
||||
const { themeConf, backgroundConf, contentConf } = skinConfig
|
||||
|
||||
if (themeConf?.color) {
|
||||
// 设置主题颜色
|
||||
root.style.setProperty('--primary-color', themeConf?.color)
|
||||
}
|
||||
|
||||
// 设置背景
|
||||
const { color, type, image } = backgroundConf || {}
|
||||
root.style.setProperty(
|
||||
'--primary-background',
|
||||
type === 'image' ? `url(${image}) no-repeat center / cover` : color
|
||||
)
|
||||
|
||||
if (contentConf?.opacity.toString()) {
|
||||
// 设置全局透明度
|
||||
root.style.setProperty('--opacity', `${contentConf.opacity / 100}`)
|
||||
}
|
||||
}
|
@ -14,22 +14,34 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useEditStore } from '@/management/stores/edit'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/src/message.scss'
|
||||
import applySkinConfig from '@/common/utils/applySkinConfig'
|
||||
|
||||
import LeftMenu from '@/management/components/LeftMenu.vue'
|
||||
import CommonTemplate from './components/CommonTemplate.vue'
|
||||
import Navbar from './components/ModuleNavbar.vue'
|
||||
|
||||
const editStore = useEditStore()
|
||||
const { init, setSurveyId } = editStore
|
||||
const { init, setSurveyId, schema } = editStore
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
watch(
|
||||
() => schema.skinConf,
|
||||
(v) => {
|
||||
applySkinConfig(v)
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
const surveyId = route.params.id as string
|
||||
setSurveyId(surveyId)
|
||||
|
@ -30,12 +30,14 @@
|
||||
<i-ep-monitor />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`preview-panel ${previewTab == 1 ? 'phone' : 'pc'}`">
|
||||
<div
|
||||
:class="`preview-panel ${previewTab == 1 ? 'phone' : 'pc'}`"
|
||||
>
|
||||
<div class="wrapper">
|
||||
<div class="tips-wrapper">
|
||||
<i-ep-WarningFilled /> <span>用户预览模式,数据不保存!</span>
|
||||
</div>
|
||||
<div v-loading="loading" element-loading-text="加载中..." style="height: 100%">
|
||||
<div class="iframe-wrapper" v-loading="loading" element-loading-text="加载中...">
|
||||
<iframe
|
||||
v-loading="loading"
|
||||
id="iframe-preview"
|
||||
@ -125,12 +127,28 @@ const closedDialog = () => {
|
||||
&.pc {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #f7f7f7;
|
||||
box-shadow: 0px 2px 10px -2px rgba(82, 82, 102, 0.2);
|
||||
height: 726px;
|
||||
background: var(--primary-background);
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.tips-wrapper {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
width: 636px;
|
||||
height: 704px;
|
||||
flex: 1;
|
||||
margin-top: 20px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.phone {
|
||||
@ -148,6 +166,9 @@ const closedDialog = () => {
|
||||
padding-bottom: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.iframe-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
iframe {
|
||||
border-radius: 0px 0px 20px 20px;
|
||||
@ -156,6 +177,7 @@ const closedDialog = () => {
|
||||
}
|
||||
.tips-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
background: $primary-bg-color;
|
||||
color: $primary-color;
|
||||
|
@ -39,7 +39,7 @@ const moduleConfig = toRef(schema, 'submitConf')
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #f6f7f9;
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
||||
.result-page-wrap {
|
||||
@ -49,8 +49,6 @@ const moduleConfig = toRef(schema, 'submitConf')
|
||||
max-height: 812px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 0.3rem;
|
||||
.result-page {
|
||||
background: rgba(255, 255, 255, var(--opacity));
|
||||
display: flex;
|
||||
|
@ -73,25 +73,6 @@ export default defineComponent({
|
||||
pageEditOne
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
skinConf: {
|
||||
handler(newVal) {
|
||||
const { themeConf, backgroundConf, contentConf } = newVal
|
||||
const root = document.documentElement
|
||||
if (themeConf?.color) {
|
||||
root.style.setProperty('--primary-color', themeConf?.color) // 设置主题颜色
|
||||
}
|
||||
if (backgroundConf?.color) {
|
||||
root.style.setProperty('--primary-background-color', backgroundConf?.color) // 设置背景颜色
|
||||
}
|
||||
if (contentConf?.opacity.toString()) {
|
||||
root.style.setProperty('--opacity', contentConf?.opacity / 100) // 设置全局透明度
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -103,7 +84,7 @@ export default defineComponent({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #f6f7f9;
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
@ -137,7 +118,6 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: var(--primary-background-color);
|
||||
position: relative;
|
||||
|
||||
.mask {
|
||||
@ -150,9 +130,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 0 0.3rem;
|
||||
background: rgba(255, 255, 255, var(--opacity));
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,37 @@ export default [
|
||||
name: '背景',
|
||||
key: 'skinConf.backgroundConf',
|
||||
formConfigList: [
|
||||
{
|
||||
type: 'TabsSetter',
|
||||
key: 'type',
|
||||
tabList: [
|
||||
{
|
||||
label: '图片(<5M)',
|
||||
value: 'image',
|
||||
},
|
||||
{
|
||||
label: '颜色',
|
||||
value: 'color',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '背景图片',
|
||||
type: 'UploadSingleFile',
|
||||
accept: "image/*",
|
||||
limitSize: 5,// 单位MB
|
||||
key: 'image',
|
||||
relyFunc: (data) => {
|
||||
return data.type === 'image'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '背景颜色',
|
||||
type: 'ColorPicker',
|
||||
key: 'color'
|
||||
key: 'color',
|
||||
relyFunc: (data) => {
|
||||
return data.type === 'color'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
65
web/src/materials/setters/widgets/TabsSetter.vue
Normal file
65
web/src/materials/setters/widgets/TabsSetter.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="tabs-setter">
|
||||
<div class="tabs-header">
|
||||
<div
|
||||
v-for="item in tabList"
|
||||
:class="['tabs-header__item', { active: props.formConfig.value === item.value }]"
|
||||
:key="item.value"
|
||||
@click="handleTabClick(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
|
||||
interface IProps {
|
||||
formConfig: any
|
||||
}
|
||||
|
||||
interface IEmit {
|
||||
(ev: typeof FORM_CHANGE_EVENT_KEY, arg: { key: string; value: string }): void
|
||||
}
|
||||
|
||||
const props = defineProps<IProps>()
|
||||
const emit = defineEmits<IEmit>()
|
||||
|
||||
const tabList = computed(() => {
|
||||
return props.formConfig?.tabList || []
|
||||
})
|
||||
|
||||
function handleTabClick(item: any) {
|
||||
const key = props.formConfig.key
|
||||
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key, value: item.value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabs-setter {
|
||||
background: #f2f4f7;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
|
||||
.tabs-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
&__item {
|
||||
flex: 1;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
100
web/src/materials/setters/widgets/UploadSingleFile.vue
Normal file
100
web/src/materials/setters/widgets/UploadSingleFile.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<el-input size="small" v-model="inputValue">
|
||||
<template #append>
|
||||
<el-upload
|
||||
ref="upload"
|
||||
class="upload-img"
|
||||
action="/api/file/upload"
|
||||
:accept="formConfig.accept"
|
||||
:limit="1"
|
||||
:show-file-list="false"
|
||||
:data="{ channel: 'upload' }"
|
||||
:on-exceed="handleExceed"
|
||||
:headers="{
|
||||
Authorization: `Bearer ${token}`
|
||||
}"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:on-success="onSuccess"
|
||||
>
|
||||
<img src="/imgs/icons/upload.png" alt="上传图标" />
|
||||
</el-upload>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, genFileId } from 'element-plus'
|
||||
import { get as _get } from 'lodash-es'
|
||||
import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus'
|
||||
import { useUserStore } from '@/management/stores/user'
|
||||
|
||||
const upload = ref<UploadInstance>()
|
||||
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
|
||||
interface IProps {
|
||||
formConfig: any
|
||||
}
|
||||
|
||||
interface IEmit {
|
||||
(ev: typeof FORM_CHANGE_EVENT_KEY, arg: { key: string; value: string }): void
|
||||
}
|
||||
|
||||
const props = defineProps<IProps>()
|
||||
const emit = defineEmits<IEmit>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const token = _get(userStore, 'userInfo.token')
|
||||
const inputValue = ref(props.formConfig.value)
|
||||
|
||||
watch(inputValue, (newValue) => {
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key: props.formConfig.key, value: newValue })
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.formConfig.value,
|
||||
(newValue) => {
|
||||
inputValue.value = newValue
|
||||
}
|
||||
)
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
upload.value!.clearFiles()
|
||||
const file = files[0] as UploadRawFile
|
||||
file.uid = genFileId()
|
||||
upload.value!.handleStart(file)
|
||||
}
|
||||
|
||||
function onSuccess(response: any) {
|
||||
if (response?.data?.url) {
|
||||
const key = props.formConfig.key
|
||||
emit(FORM_CHANGE_EVENT_KEY, { key, value: response.data.url })
|
||||
}
|
||||
}
|
||||
|
||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
const { limitSize } = props.formConfig
|
||||
if (limitSize) {
|
||||
if (rawFile.size / 1024 / 1024 > limitSize) {
|
||||
ElMessage.error(`图片大小不得超过 ${limitSize}MB!`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
.upload-img {
|
||||
.el-upload {
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -6,31 +6,12 @@ import { watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { useSurveyStore } from './stores/survey'
|
||||
import applySkinConfig from '@/common/utils/applySkinConfig';
|
||||
|
||||
const { skinConf } = storeToRefs(useSurveyStore())
|
||||
|
||||
const updateSkinConfig = (value: any) => {
|
||||
const root = document.documentElement
|
||||
const { themeConf, backgroundConf, contentConf } = value
|
||||
|
||||
if (themeConf?.color) {
|
||||
// 设置主题颜色
|
||||
root.style.setProperty('--primary-color', themeConf?.color)
|
||||
}
|
||||
|
||||
if (backgroundConf?.color) {
|
||||
// 设置背景颜色
|
||||
root.style.setProperty('--primary-background-color', backgroundConf?.color)
|
||||
}
|
||||
|
||||
if (contentConf?.opacity.toString()) {
|
||||
// 设置全局透明度
|
||||
root.style.setProperty('--opacity', `${parseInt(contentConf.opacity) / 100}`)
|
||||
}
|
||||
}
|
||||
|
||||
watch(skinConf, (value) => {
|
||||
updateSkinConfig(value)
|
||||
applySkinConfig(value)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@ -54,4 +35,15 @@ html {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 750px) {
|
||||
body {
|
||||
padding-top: 40px;
|
||||
background: var(--primary-background);
|
||||
}
|
||||
#app {
|
||||
border-radius: 8px 8px 0 0;
|
||||
box-shadow: var(--el-box-shadow);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -175,13 +175,11 @@ const handleSubmit = () => {
|
||||
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
background-color: var(--primary-background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
margin: 0 0.3rem;
|
||||
background: rgba(255, 255, 255, var(--opacity));
|
||||
border-radius: 8px 8px 0 0;
|
||||
height: 100%;
|
||||
|
Loading…
Reference in New Issue
Block a user