feat: vue3 (#103)

This commit is contained in:
Weiguo Wang 2024-05-09 20:34:24 +08:00 committed by sudoooooo
parent 2bca93bf12
commit 6771b831e5
280 changed files with 8111 additions and 10342 deletions

15
web/.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

View File

@ -1,20 +0,0 @@
module.exports = {
root: false,
env: {
node: true,
},
extends: [
'plugin:vue/essential',
'eslint:recommended',
'plugin:prettier/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
semi: ['error', 'always'],
},
};

1
web/.gitignore vendored
View File

@ -14,6 +14,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
package-lock.json package-lock.json
pnpm-lock.yaml
# Editor directories and files # Editor directories and files
.idea .idea

View File

@ -1,5 +0,0 @@
module.exports = {
singleQuote: true, // 使用单引号
semi: true, // 不使用分号
// trailingComma: 'all', // 在对象和数组末尾加上逗号
};

8
web/.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

10
web/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

View File

@ -1,11 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
'@vue/babel-preset-jsx',
{
injectH: false,
},
],
],
};

63
web/components.d.ts vendored Normal file
View File

@ -0,0 +1,63 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
'IEp-[]': typeof import('~icons/ep/[]')['default']
'IEp-[test]': typeof import('~icons/ep/[test]')['default']
'IEp-]': typeof import('~icons/ep/]')['default']
IEpBottom: typeof import('~icons/ep/bottom')['default']
IEpCheck: typeof import('~icons/ep/check')['default']
IEpCirclePlus: typeof import('~icons/ep/circle-plus')['default']
IEpClose: typeof import('~icons/ep/close')['default']
IEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
IEpLoading: typeof import('~icons/ep/loading')['default']
IEpQuestionFilled: typeof import('~icons/ep/question-filled')['default']
IEpRank: typeof import('~icons/ep/rank')['default']
IEpRemove: typeof import('~icons/ep/remove')['default']
IEpSearch: typeof import('~icons/ep/search')['default']
IEpSort: typeof import('~icons/ep/sort')['default']
IEpSortDown: typeof import('~icons/ep/sort-down')['default']
IEpSortUp: typeof import('~icons/ep/sort-up')['default']
IEpTop: typeof import('~icons/ep/top')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
vPopover: typeof import('element-plus/es')['ElPopoverDirective']
}
}

8
web/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module "vuex" {
export * from "vuex/types/index.d.ts";
export * from "vuex/types/helpers.d.ts";
export * from "vuex/types/logger.d.ts";
export * from "vuex/types/vue.d.ts";
}

View File

@ -2,55 +2,57 @@
"name": "web", "name": "web",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vite",
"build": "vue-cli-service build", "dev": "vite",
"report": "vue-cli-service build --report", "build": "run-p type-check \"build-only {@}\" --",
"lint": "vue-cli-service lint", "preview": "vite preview",
"lintfix": "eslint --fix ." "build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
"@vue/babel-preset-jsx": "^1.4.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"axios": "^1.4.0", "axios": "^1.4.0",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"element-ui": "^2.15.13", "element-plus": "^2.7.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"vue": "^2.7.14", "vue": "^3.4.15",
"vue-router": "^3.5.1", "vue-router": "^4.2.5",
"vuedraggable": "^2.24.3", "vuedraggable": "^4.1.0",
"vuex": "^3.6.2", "vuex": "^4.0.2",
"xss": "^1.0.14" "xss": "^1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.16", "@iconify-json/ep": "^1.1.15",
"@babel/eslint-parser": "^7.12.16", "@rushstack/eslint-patch": "^1.10.2",
"@vue/cli-plugin-babel": "~5.0.0", "@tsconfig/node20": "^20.1.2",
"@vue/cli-plugin-eslint": "~5.0.0", "@types/node": "^20.11.19",
"@vue/cli-plugin-router": "~5.0.0", "@vitejs/plugin-vue": "^5.0.3",
"@vue/cli-plugin-vuex": "~5.0.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/cli-service": "~5.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"eslint": "^7.32.0", "@vue/eslint-config-typescript": "^12.0.0",
"eslint-config-prettier": "^8.3.0", "@vue/tsconfig": "^0.5.1",
"eslint-plugin-prettier": "^4.0.0", "eslint": "^8.49.0",
"eslint-plugin-vue": "^8.7.1", "eslint-plugin-vue": "^9.17.0",
"less-loader": "^11.1.3", "npm-run-all2": "^6.1.1",
"postcss-import": "^15.1.0", "prettier": "^3.0.3",
"postcss-url": "^10.1.3", "sass": "^1.72.0",
"prettier": "^2.4.1", "typescript": "~5.3.0",
"sass": "^1.32.7", "unplugin-auto-import": "^0.17.5",
"sass-loader": "^12.0.0", "unplugin-icons": "^0.18.5",
"speed-measure-webpack-plugin": "^1.5.0", "unplugin-vue-components": "^0.26.0",
"style-resources-loader": "^1.5.0", "vite": "^5.1.4",
"vue-style-loader": "^4.1.3", "vite-plugin-virtual-mpa": "^1.11.0",
"vue-template-compiler": "^2.7.14" "vue-tsc": "^1.8.27"
}, },
"engines": { "engines": {
"node": ">=14.21.0", "node": ">=14.21.0",

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>imgs/favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>imgs/favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
<script>
(function () {
function resetRemUnit() {
var PC_W = 750;
var docEl = window.document.documentElement;
var width = docEl.getBoundingClientRect().width || 375;
if (!(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent)) {
width = width < PC_W ? width : PC_W;
docEl.className += ' ispc-html';
}
var f = Math.min(width / 7.5, 50);
docEl.style.fontSize = f + 'px';
var d = window.document.createElement('div');
d.style.width = '1rem';
d.style.display = "none";
var head = window.document.getElementsByTagName('head')[0];
head.appendChild(d);
var realf = parseFloat(window.getComputedStyle(d, null).getPropertyValue('width'));
if (f !== realf) {
docEl.style.fontSize = f * (f / realf) + 'px';
}
}
resetRemUnit();
window.addEventListener('resize', resetRemUnit);
})();
</script>
<style></style>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -1,43 +1,43 @@
<template> <template>
<div class="editor-v2"> <div class="editor-v2">
<RichEditor :value="realData" @input="handleChange" @blur="handleBlur" /> <RichEditor :modelValue="realData" @input="handleChange" @blur="handleBlur" />
</div> </div>
</template> </template>
<script> <script>
import RichEditor from './RichEditor'; import RichEditor from './RichEditor.vue'
export default { export default {
components: { components: {
RichEditor, RichEditor
// ReadOnly, // ReadOnly,
}, },
props: { props: {
realData: { realData: {
type: String, type: String,
default: () => '', default: () => ''
}, },
questionDataList: { questionDataList: {
type: Array, type: Array,
default: () => [], default: () => []
}, }
}, },
data() { data() {
return {}; return {}
}, },
computed: {}, computed: {},
watch: {}, watch: {},
destroyed() { unmounted() {
this.$emit('onDestroy'); this.$emit('onDestroy')
}, },
methods: { methods: {
handleChange(v) { handleChange(v) {
this.$emit('change', v); this.$emit('change', v)
}, },
handleBlur(v) { handleBlur(v) {
this.$emit('blur', v); this.$emit('blur', v)
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.editor-v2 { .editor-v2 {

View File

@ -4,33 +4,33 @@
</div> </div>
</template> </template>
<script> <script>
import { filterXSS } from '@/common/xss'; import { filterXSS } from '@/common/xss'
export default { export default {
name: 'ReadOnly', name: 'ReadOnly',
props: { props: {
realData: { realData: {
type: String, type: String,
default: () => '', default: () => ''
}, },
viewData: { viewData: {
type: String, type: String,
default: () => '', default: () => ''
}, },
tag: { tag: {
tyle: String, tyle: String,
default: () => '', default: () => ''
}, },
border: { border: {
tyle: Boolean, tyle: Boolean,
default: () => false, default: () => false
}, },
defaultStyle: { defaultStyle: {
tyle: Boolean, tyle: Boolean,
default: () => false, default: () => false
}, }
}, },
data() { data() {
return {}; return {}
}, },
computed: { computed: {
tagHtml() { tagHtml() {
@ -47,38 +47,38 @@ export default {
border-radius: 0 0.06rem; border-radius: 0 0.06rem;
background: rgba(250,136,26,0.1); background: rgba(250,136,26,0.1);
">${this.tag}</span>` ">${this.tag}</span>`
: ''; : ''
}, },
getHtml() { getHtml() {
const title = filterXSS(this.viewData); const title = filterXSS(this.viewData)
if (!this.tag) return title; if (!this.tag) return title
let html = this.isRichText(title) ? title : `<p>${title}</p>`; let html = this.isRichText(title) ? title : `<p>${title}</p>`
const index = html.lastIndexOf('</p>'); const index = html.lastIndexOf('</p>')
if (this.viewData.indexOf(this.tagHtml) < 0) { if (this.viewData.indexOf(this.tagHtml) < 0) {
html = html.slice(0, index) + this.tagHtml + html.slice(index); html = html.slice(0, index) + this.tagHtml + html.slice(index)
} }
return html; return html
}, },
getStyle() { getStyle() {
let style = ''; let style = ''
if (this.border) { if (this.border) {
style += 'border:1px solid #c8c9cd;padding:10px;'; style += 'border:1px solid #c8c9cd;padding:10px;'
} }
if (this.defaultStyle) { if (this.defaultStyle) {
style += 'color: #6e707c;font-size: 12px;'; style += 'color: #6e707c;font-size: 12px;'
} }
return style; return style
}, }
}, },
destroyed() { unmounted() {
this.$emit('onDestroy'); this.$emit('onDestroy')
}, },
methods: { methods: {
isRichText(str) { isRichText(str) {
return /^<p[\s\S]*>[\s\S]*<\/p>$/.test(str); return /^<p[\s\S]*>[\s\S]*<\/p>$/.test(str)
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.read-only { .read-only {

View File

@ -1,126 +1,119 @@
<template> <template>
<div class="editor-wrapper border"> <div class="editor-wrapper border">
<div class="toolbar" ref="toolbar" v-show="showToolbar"></div> <Toolbar
<div class="editor" ref="editor"></div> :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> </div>
</template> </template>
<script> <script setup>
import { createEditor, createToolbar } from '@wangeditor/editor'; 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'
export default { const emit = defineEmits(['input', 'onFocus', 'change', 'blur'])
name: 'richEditor', const model = defineModel()
data() { const props = defineProps(['staticToolBar'])
return {
curValue: '', const curValue = ref('')
editor: null, const editorRef = shallowRef()
showToolbar: false, const showToolbar = ref(props.staticToolBar || false)
};
}, const mode = 'simple'
props: ['value'], // value v-model
mounted() { const toolbarConfig = {
this.create(); toolbarKeys: [
}, 'color', //
watch: { 'bgColor', //
value: { 'bold',
immediate: true, 'insertLink' //
handler(newVal) { ]
const isEqual = newVal === this.curValue; }
if (isEqual) return; //
const editorConfig = {}
const setHtml = (newHtml) => {
const editor = editorRef.value
if (editor == null) return
editor.setHtml(newHtml)
}
const onCreated = (editor) => {
editorRef.value = editor
if (model.value) {
setHtml(model.value)
}
}
const onChange = (editor) => {
const editorHtml = editor.getHtml()
curValue.value = editorHtml // html
emit('input', editorHtml) // v-model
}
const onFocus = (editor) => {
emit('onFocus', editor)
setToolbarStatus(true)
}
const onBlur = (editor) => {
const editorHtml = editor.getHtml()
curValue.value = editorHtml // html
emit('change', editorHtml)
emit('blur', editor)
setToolbarStatus(false)
}
const setToolbarStatus = (status) => {
if (props.staticToolBar) return
showToolbar.value = status
}
watch(
() => model.value,
(newVal) => {
const isEqual = newVal === curValue.value
if (isEqual) return //
// HTML
this.setHtml(newVal);
},
},
},
methods: {
// HTML // HTML
setHtml(newHtml) { setHtml(newVal)
const editor = this.editor;
if (editor === null) return;
editor.setHtml(newHtml);
},
// editor
create() {
if (this.$refs.editor === null) return;
createEditor({
selector: this.$refs.editor,
html: this.defaultHtml || this.value || '',
config: {
onCreated: (editor) => {
this.editor = Object.seal(editor);
if (this.value) {
this.setHtml(this.value);
}
this.$refs.toolbar &&
createToolbar({
editor,
selector: this.$refs.toolbar,
config: {
toolbarKeys: [
'color', //
'bgColor', //
'bold',
// 'insertImage', //
// 'video',
// 'fontSize', //
// 'justify', //
'insertLink', //
// 'clean',
],
},
mode: 'simple',
});
},
onChange: (editor) => {
const editorHtml = editor.getHtml();
this.curValue = editorHtml; // html
this.$emit('input', editorHtml); // v-model
},
onDestroyed: (editor) => {
this.$emit('onDestroyed', editor);
editor.destroy();
},
onFocus: (editor) => {
this.$emit('onFocus', editor);
this.showToolbar = true;
},
onBlur: (editor) => {
const editorHtml = editor.getHtml();
this.curValue = editorHtml; // html
this.$emit('change', editorHtml);
this.$emit('blur', editor);
this.showToolbar = false;
},
// customPaste: (editor, event) => {
// let res;
// this.$emit('customPaste', editor, event, (val) => {
// res = val;
// });
// return res;
// },
},
content: [],
mode: 'simple',
});
},
}, },
}; {
// immediate: true
}
)
onBeforeMount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url('@wangeditor/editor/dist/css/style.css');
@import url('@/management/styles/reset-wangeditor.scss');
.editor-wrapper { .editor-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
// min-height: 45px; // min-height: 45px;
} }
.toolbar { .static-toolbar {
border-bottom: 1px solid #dedede;
}
.dynamic-toolbar {
position: absolute; position: absolute;
left: 0; left: 0;
top: -44px; top: -44px;

View File

@ -0,0 +1,19 @@
:root {
--w-e-textarea-bg-color: transparent;
}
.w-e-text-container * {
font-size: inherit;
}
.w-e-text-container [data-slate-editor] h1,
.w-e-text-container [data-slate-editor] h2,
.w-e-text-container [data-slate-editor] h3,
.w-e-text-container [data-slate-editor] h4,
.w-e-text-container [data-slate-editor] h5 {
margin: 0;
}
.w-e-text-container [data-slate-editor] p {
margin: 0;
}

View File

@ -1,62 +1,62 @@
import xss from 'xss'; import xss from 'xss'
const myxss = new xss.FilterXSS({ const myxss = new xss.FilterXSS({
onIgnoreTagAttr(tag, name, value) { onIgnoreTagAttr(tag, name, value) {
if (name === 'style' || name === 'class') { if (name === 'style' || name === 'class') {
return `${name}="${value}"`; return `${name}="${value}"`
} }
return undefined; return undefined
}, },
onIgnoreTag(tag, html) { onIgnoreTag(tag, html) {
// <xxx>过滤为空,否则不过滤为空 // <xxx>过滤为空,否则不过滤为空
var re1 = new RegExp('<.+?>', 'g'); var re1 = new RegExp('<.+?>', 'g')
if (re1.test(html)) { if (re1.test(html)) {
return ''; return ''
} else { } else {
return html; return html
} }
}, }
}); })
const isImg = (html) => { const isImg = (html) => {
html = html + ''; html = html + ''
return html.indexOf('<img') > -1; return html.indexOf('<img') > -1
}; }
const isVideo = (html) => { const isVideo = (html) => {
html = html + ''; html = html + ''
return html.indexOf('<video') > -1; return html.indexOf('<video') > -1
}; }
export const cleanRichText = (text) => { export const cleanRichText = (text) => {
if (!text) { if (!text) {
return text === 0 ? 0 : ''; return text === 0 ? 0 : ''
} }
const html = transformHtmlTag(text); const html = transformHtmlTag(text)
const content = html.replace(/<[^<>]+>/g, '').replace(/&nbsp;/g, ''); const content = html.replace(/<[^<>]+>/g, '').replace(/&nbsp;/g, '')
if (content) return content; if (content) return content
if (isImg(html)) return '图片'; if (isImg(html)) return '图片'
if (isVideo(html)) return '视频'; if (isVideo(html)) return '视频'
return '文本'; return '文本'
}; }
export function escapeHtml(html) { export function escapeHtml(html) {
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;'); return html.replace(/</g, '&lt;').replace(/>/g, '&gt;')
} }
export const transformHtmlTag = (html) => { export const transformHtmlTag = (html) => {
if (!html) return ''; if (!html) return ''
if (typeof html !== 'string') return html + ''; if (typeof html !== 'string') return html + ''
return html return html
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&amp;') .replace(html ? /&(?!#?\w+;)/g : /&/g, '&amp;')
.replace(/&lt;/g, '<') .replace(/&lt;/g, '<')
.replace(/&gt;/g, '>') .replace(/&gt;/g, '>')
.replace(/&quot;/g, '"') .replace(/&quot;/g, '"')
.replace(/&#39;/g, "'") .replace(/&#39;/g, "'")
.replace(/\\\n/g, '\\n'); .replace(/\\\n/g, '\\n')
//.replace(/&nbsp;/g, "") //.replace(/&nbsp;/g, "")
}; }
const filterXSSClone = myxss.process.bind(myxss); const filterXSSClone = myxss.process.bind(myxss)
export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html)); export const filterXSS = (html) => filterXSSClone(transformHtmlTag(html))
export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html)); export const escapeFilterXSS = (html) => escapeHtml(filterXSS(html))

View File

@ -3,11 +3,13 @@
<router-view></router-view> <router-view></router-view>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'App', name: 'App'
}; }
</script> </script>
<style lang="scss"> <style lang="scss">
@import url('./styles/icon.scss'); @import url('./styles/icon.scss');
@import url('../materials/questions/common/css/icon.scss'); @import url('../materials/questions/common/css/icon.scss');

View File

@ -1,10 +1,10 @@
import axios from './base'; import axios from './base'
export const getRecycleList = (data) => { export const getRecycleList = (data) => {
return axios.get('/survey/dataStatistic/dataTable', { return axios.get('/survey/dataStatistic/dataTable', {
params: { params: {
pageSize: 10, pageSize: 10,
...data, ...data
}, }
}); })
}; }

View File

@ -1,9 +1,9 @@
import axios from './base'; import axios from './base'
export const register = (data) => { export const register = (data) => {
return axios.post('/auth/register', data); return axios.post('/auth/register', data)
}; }
export const login = (data) => { export const login = (data) => {
return axios.post('/auth/login', data); return axios.post('/auth/login', data)
}; }

View File

@ -1,49 +1,50 @@
import axios from 'axios'; import axios from 'axios'
import store from '@/management/store/index'; import store from '@/management/store/index'
import router from '@/management/router/index'; import router from '@/management/router/index'
import { get as _get } from 'lodash-es'; import { get as _get } from 'lodash-es'
const instance = axios.create({
baseURL: '/api',
timeout: 10000,
});
instance.interceptors.response.use(
(response) => {
if (response.status !== 200) {
throw new Error('http请求出错');
}
const res = response.data;
if (res.code === 403) {
router.replace({
name: 'login',
});
return res;
} else {
return res;
}
},
(err) => {
throw new Error(err);
}
);
instance.interceptors.request.use((config) => {
const hasLogined = _get(store, 'state.user.hasLogined');
const token = _get(store, 'state.user.userInfo.token');
if (hasLogined && token) {
if (!config.headers) {
config.headers = {};
}
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export default instance;
export const CODE_MAP = { export const CODE_MAP = {
SUCCESS: 200, SUCCESS: 200,
ERROR: 500, ERROR: 500,
NOTAUTH: 403, NO_AUTH: 403,
}; ERR_AUTH: 1001
}
const instance = axios.create({
baseURL: '/api',
timeout: 10000
})
instance.interceptors.response.use(
(response) => {
if (response.status !== 200) {
throw new Error('http请求出错')
}
const res = response.data
if (res.code === CODE_MAP.NO_AUTH || res.code === CODE_MAP.ERR_AUTH) {
router.replace({
name: 'login'
})
return res
} else {
return res
}
},
(err) => {
throw new Error(err)
}
)
instance.interceptors.request.use((config) => {
const hasLogined = _get(store, 'state.user.hasLogined')
const token = _get(store, 'state.user.userInfo.token')
if (hasLogined && token) {
if (!config.headers) {
config.headers = {}
}
config.headers.Authorization = `Bearer ${token}`
}
return config
})
export default instance

View File

@ -1,5 +1,5 @@
import axios from './base'; import axios from './base'
export const refreshCaptcha = ({ captchaId }) => { export const refreshCaptcha = ({ captchaId }) => {
return axios.post('/auth/captcha', { captchaId }); return axios.post('/auth/captcha', { captchaId })
}; }

View File

@ -1,5 +1,5 @@
import axios from './base'; import axios from './base'
export const getBannerData = () => { export const getBannerData = () => {
return axios.get('/survey/getBannerData'); return axios.get('/survey/getBannerData')
}; }

View File

@ -1,4 +1,4 @@
import axios from './base'; import axios from './base'
export const getSurveyList = ({ curPage, filter, order }) => { export const getSurveyList = ({ curPage, filter, order }) => {
return axios.get('/survey/getList', { return axios.get('/survey/getList', {
@ -6,48 +6,48 @@ export const getSurveyList = ({ curPage, filter, order }) => {
pageSize: 10, pageSize: 10,
curPage, curPage,
filter, filter,
order, order
}, }
}); })
}; }
export const getSurveyById = (id) => { export const getSurveyById = (id) => {
return axios.get('/survey/getSurvey', { return axios.get('/survey/getSurvey', {
params: { params: {
surveyId: id, surveyId: id
}, }
}); })
}; }
export const saveSurvey = ({ surveyId, configData }) => { export const saveSurvey = ({ surveyId, configData }) => {
return axios.post('/survey/updateConf', { surveyId, configData }); return axios.post('/survey/updateConf', { surveyId, configData })
}; }
export const publishSurvey = ({ surveyId }) => { export const publishSurvey = ({ surveyId }) => {
return axios.post('/survey/publishSurvey', { return axios.post('/survey/publishSurvey', {
surveyId, surveyId
}); })
}; }
export const createSurvey = (data) => { export const createSurvey = (data) => {
return axios.post('/survey/createSurvey', data); return axios.post('/survey/createSurvey', data)
}; }
export const getSurveyHistory = ({ surveyId, historyType }) => { export const getSurveyHistory = ({ surveyId, historyType }) => {
return axios.get('/surveyHisotry/getList', { return axios.get('/surveyHisotry/getList', {
params: { params: {
surveyId, surveyId,
historyType, historyType
}, }
}); })
}; }
export const deleteSurvey = (surveyId) => { export const deleteSurvey = (surveyId) => {
return axios.post('/survey/deleteSurvey', { return axios.post('/survey/deleteSurvey', {
surveyId, surveyId
}); })
}; }
export const updateSurvey = (data) => { export const updateSurvey = (data) => {
return axios.post('/survey/updateMeta', data); return axios.post('/survey/updateMeta', data)
}; }

View File

@ -2,23 +2,23 @@
<div class="default-empty-root"> <div class="default-empty-root">
<img class="img" :src="data.img" /> <img class="img" :src="data.img" />
<div class="title">{{ data.title }}</div> <div class="title">{{ data.title }}</div>
<div class="desc" v-html="data.desc" /> <div class="desc" v-html="data.desc"></div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'Empty', name: 'EmptyModule',
props: { props: {
data: { data: {
type: Object, type: Object,
required: true, required: true
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" scoped>
.default-empty-root { .default-empty-root {
width: 350px; width: 350px;
margin: 200px auto; margin: 200px auto;

View File

@ -1,28 +1,22 @@
<template> <template>
<div class="nav"> <div class="nav">
<logo></logo> <LogoIcon />
<router-link <RouterLink v-for="(tab, index) in tabList" :key="index" class="tab-btn" :to="tab.to" replace>
v-for="(tab, index) in tabList"
:key="index"
class="tab-btn"
:to="tab.to"
replace
>
<div class="icon"> <div class="icon">
<i class="iconfont" :class="tab.icon"></i> <i class="iconfont" :class="tab.icon"></i>
</div> </div>
<p>{{ tab.text }}</p> <p>{{ tab.text }}</p>
</router-link> </RouterLink>
</div> </div>
</template> </template>
<script> <script>
import logo from './logo.vue'; import LogoIcon from './LogoIcon.vue'
export default { export default {
name: 'leftMenu', name: 'LeftMenu',
components: { components: {
logo, LogoIcon
}, },
data() { data() {
return { return {
@ -31,27 +25,27 @@ export default {
text: '编辑问卷', text: '编辑问卷',
icon: 'icon-bianji', icon: 'icon-bianji',
to: { to: {
name: 'QuestionEditIndex', name: 'QuestionEditIndex'
}, }
}, },
{ {
text: '投放问卷', text: '投放问卷',
icon: 'icon-toufang', icon: 'icon-toufang',
to: { to: {
name: 'publishResultPage', name: 'publishResultPage'
}, }
}, },
{ {
text: '数据统计', text: '数据统计',
icon: 'icon-shujutongji', icon: 'icon-shujutongji',
to: { to: {
name: 'analysisPage', name: 'analysisPage'
}, }
}, }
], ]
}; }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -3,18 +3,20 @@
<img src="/imgs/s-logo.webp" /> <img src="/imgs/s-logo.webp" />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'logoIcon', name: 'LogoIcon',
methods: { methods: {
toHomePage() { toHomePage() {
this.$router.push({ this.$router.push({
name: 'survey', name: 'survey'
}); })
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.navbar-main-logo { .navbar-main-logo {
width: 80px; width: 80px;

View File

@ -41,14 +41,14 @@ export const defaultQuestionConfig = {
text: '选项1', text: '选项1',
others: false, others: false,
othersKey: '', othersKey: '',
placeholderDesc: '', placeholderDesc: ''
}, },
{ {
text: '选项2', text: '选项2',
others: false, others: false,
othersKey: '', othersKey: '',
placeholderDesc: '', placeholderDesc: ''
}, }
], ],
star: 5, star: 5,
optionOrigin: '', optionOrigin: '',
@ -57,21 +57,21 @@ export const defaultQuestionConfig = {
numberRange: { numberRange: {
min: { min: {
placeholder: '0', placeholder: '0',
value: 0, value: 0
}, },
max: { max: {
placeholder: '1000', placeholder: '1000',
value: 1000, value: 1000
}, }
}, },
textRange: { textRange: {
min: { min: {
placeholder: '0', placeholder: '0',
value: 0, value: 0
}, },
max: { max: {
placeholder: '500', placeholder: '500',
value: 500, value: 500
}, }
}, }
}; }

View File

@ -4,84 +4,75 @@ const menuItems = {
snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp', snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp',
path: 'InputModule', path: 'InputModule',
icon: 'tixing-danhangshuru', icon: 'tixing-danhangshuru',
title: '单行输入框', title: '单行输入框'
}, },
textarea: { textarea: {
type: 'textarea', type: 'textarea',
snapshot: '/imgs/question-type-snapshot/11iAo3ca0u1657702225416.webp', snapshot: '/imgs/question-type-snapshot/11iAo3ca0u1657702225416.webp',
path: 'TextareaModule', path: 'TextareaModule',
icon: 'tixing-duohangshuru', icon: 'tixing-duohangshuru',
title: '多行输入框', title: '多行输入框'
}, },
radio: { radio: {
type: 'radio', type: 'radio',
snapshot: '/imgs/question-type-snapshot/TgeRDfURJZ1657702220602.webp', snapshot: '/imgs/question-type-snapshot/TgeRDfURJZ1657702220602.webp',
icon: 'tixing-danxuan', icon: 'tixing-danxuan',
path: 'RadioModule', path: 'RadioModule',
title: '单项选择', title: '单项选择'
}, },
checkbox: { checkbox: {
type: 'checkbox', type: 'checkbox',
path: 'CheckboxModule', path: 'CheckboxModule',
snapshot: '/imgs/question-type-snapshot/Md2YmzBBpV1657702223744.webp', snapshot: '/imgs/question-type-snapshot/Md2YmzBBpV1657702223744.webp',
icon: 'tixing-duoxuan', icon: 'tixing-duoxuan',
title: '多项选择', title: '多项选择'
}, },
'binary-choice': { 'binary-choice': {
type: 'binary-choice', type: 'binary-choice',
snapshot: '/imgs/question-type-snapshot/blW8U1ckzd1657702223023.webp', snapshot: '/imgs/question-type-snapshot/blW8U1ckzd1657702223023.webp',
path: 'BinaryChoiceModule', path: 'BinaryChoiceModule',
icon: 'tixing-panduanti', icon: 'tixing-panduanti',
title: '判断题', title: '判断题'
}, },
'radio-star': { 'radio-star': {
type: 'radio-star', type: 'radio-star',
snapshot: '/imgs/question-type-snapshot/7CU6tn4XqT1657702221208.webp', snapshot: '/imgs/question-type-snapshot/7CU6tn4XqT1657702221208.webp',
path: 'StarModule', path: 'StarModule',
icon: 'tixing-pingfen', icon: 'tixing-pingfen',
title: '评分', title: '评分'
}, },
'radio-nps': { 'radio-nps': {
type: 'radio-nps', type: 'radio-nps',
path: 'NpsModule', path: 'NpsModule',
snapshot: '/imgs/question-type-snapshot/radio-nps.webp', snapshot: '/imgs/question-type-snapshot/radio-nps.webp',
icon: 'NPSpingfen', icon: 'NPSpingfen',
title: 'nps评分', title: 'nps评分'
}, },
vote: { vote: {
type: 'vote', type: 'vote',
path: 'VoteModule', path: 'VoteModule',
snapshot: '/imgs/question-type-snapshot/nGTscsZlwn1657702222857.webp', snapshot: '/imgs/question-type-snapshot/nGTscsZlwn1657702222857.webp',
icon: 'tixing-toupiao', icon: 'tixing-toupiao',
title: '投票', title: '投票'
}, }
}; }
const menuGroup = [ const menuGroup = [
{ {
title: '输入类题型', title: '输入类题型',
questionList: ['text', 'textarea'], questionList: ['text', 'textarea']
}, },
{ {
title: '选择类题型', title: '选择类题型',
questionList: [ questionList: ['radio', 'checkbox', 'binary-choice', 'radio-star', 'radio-nps', 'vote']
'radio', }
'checkbox', ]
'binary-choice',
'radio-star',
'radio-nps',
'vote',
],
},
];
const menu = menuGroup.map((group) => { const menu = menuGroup.map((group) => {
group.questionList = group.questionList.map( group.questionList = group.questionList.map((question) => menuItems[question])
(question) => menuItems[question] return group
); })
return group;
});
export const questionTypeList = Object.values(menuItems); export const questionTypeList = Object.values(menuItems)
export default menu; export default menu

View File

@ -1,41 +1,35 @@
export default [ export default [
{ {
label: '顶部图片地址', label: '顶部图片地址',
type: 'Input', type: 'InputSetter',
key: 'bgImage', key: 'bgImage',
inline: true,
direction: 'horizon',
labelStyle: { width: '120px' } labelStyle: { width: '120px' }
}, },
{ {
label: '顶部视频地址', label: '顶部视频地址',
type: 'Input', type: 'InputSetter',
key: 'videoLink', key: 'videoLink',
direction: 'horizon',
labelStyle: { width: '120px' } labelStyle: { width: '120px' }
}, },
{ {
label: '视频海报地址', label: '视频海报地址',
type: 'Input', type: 'InputSetter',
key: 'postImg', key: 'postImg',
direction: 'horizon',
labelStyle: { width: '120px' } labelStyle: { width: '120px' }
}, },
{ {
label: '图片支持点击', label: '图片支持点击',
type: 'CustomedSwitch', type: 'CustomedSwitch',
direction: 'horizon',
labelStyle: { width: '120px' }, labelStyle: { width: '120px' },
key: 'bgImageAllowJump', key: 'bgImageAllowJump'
}, },
{ {
label: '跳转链接', label: '跳转链接',
type: 'Input', type: 'InputSetter',
direction: 'horizon',
labelStyle: { width: '120px' }, labelStyle: { width: '120px' },
key: 'bgImageJumpLink', key: 'bgImageJumpLink',
relyFunc: (data) => { relyFunc: (data) => {
return !!data?.bgImageAllowJump; return !!data?.bgImageAllowJump
}, }
}, }
]; ]

View File

@ -1,10 +1,9 @@
export default [ export default [
{ {
label: '自定义Logo', label: '自定义Logo',
type: 'Input', type: 'InputSetter',
key: 'logoImage', key: 'logoImage',
tip: '默认尺寸200px*50px', tip: '默认尺寸200px*50px',
direction: 'horizon',
labelStyle: { width: '120px' } labelStyle: { width: '120px' }
}, },
{ {
@ -12,7 +11,6 @@ export default [
type: 'InputPercent', type: 'InputPercent',
key: 'logoImageWidth', key: 'logoImageWidth',
tip: '填写宽度百分比例如30%', tip: '填写宽度百分比例如30%',
direction: 'horizon',
labelStyle: { width: '120px' } labelStyle: { width: '120px' }
}, }
]; ]

View File

@ -1,46 +1,48 @@
import bannerConfig from "./bannerConfig" import bannerConfig from './bannerConfig'
import logoConfig from "./logoConfig" import logoConfig from './logoConfig'
export default [ export default [
{ {
name: '头图', name: '头图',
key: 'bannerConf.bannerConfig', key: 'bannerConf.bannerConfig',
formConfigList: bannerConfig formConfigList: bannerConfig
}, },
{ {
name: '背景', name: '背景',
key: 'skinConf.backgroundConf', key: 'skinConf.backgroundConf',
formConfigList: [{ formConfigList: [
direction: 'space_between', {
label: '背景颜色', label: '背景颜色',
type: 'ColorPicker', type: 'ColorPicker',
key: 'color', key: 'color'
}], }
]
}, },
{ {
name: '主题色', name: '主题色',
key: 'skinConf.themeConf', key: 'skinConf.themeConf',
formConfigList: [{ formConfigList: [
direction: 'space_between', {
direction: 'space_between', label: '全局应用',
label: '全局应用', type: 'ColorPicker',
type: 'ColorPicker', key: 'color'
key: 'color', }
}], ]
}, },
{ {
key: 'skinConf.contentConf', key: 'skinConf.contentConf',
name: '内容区域', name: '内容区域',
formConfigList: [{ formConfigList: [
direction: 'space_between', {
label: '内容透明度', label: '内容透明度',
type: 'SliderSetter', type: 'SliderSetter',
key: 'opacity', key: 'opacity'
}], }
]
}, },
{ {
name: '品牌logo', name: '品牌logo',
key: 'bottomConf', key: 'bottomConf',
formConfigList: logoConfig formConfigList: logoConfig
} }
] ]

View File

@ -1,71 +1,62 @@
export default [ export default [
{ {
label: '提交按钮文案', title: '提交按钮文案',
type: 'Input', type: 'InputSetter',
key: 'submitTitle', key: 'submitTitle',
placeholder: '提交', placeholder: '提交',
value: '', value: ''
labelStyle: {
fontWeight: 'bold',
},
}, },
{ {
label: '提交确认弹窗', title: '提交确认弹窗',
type: 'Customed', type: 'Customed',
key: 'confirmAgain', key: 'confirmAgain',
labelStyle: {
fontWeight: 'bold',
},
content: [ content: [
{ {
label: '是否配置该项', label: '是否配置该项',
labelStyle: { width: '120px' },
type: 'CustomedSwitch', type: 'CustomedSwitch',
key: 'confirmAgain.is_again', key: 'confirmAgain.is_again',
direction: 'horizon', value: true
value: true,
}, },
{ {
label: '二次确认文案', label: '二次确认文案',
type: 'Input', labelStyle: { width: '120px' },
type: 'InputSetter',
key: 'confirmAgain.again_text', key: 'confirmAgain.again_text',
direction: 'horizon',
placeholder: '确认要提交吗?', placeholder: '确认要提交吗?',
value: '确认要提交吗?', value: '确认要提交吗?'
}, }
], ]
}, },
{ {
label: '提交文案配置', title: '提交文案配置',
type: 'Customed', type: 'Customed',
key: 'msgContent', key: 'msgContent',
labelStyle: {
fontWeight: 'bold',
},
content: [ content: [
{ {
label: '已提交', label: '已提交',
type: 'Input', labelStyle: { width: '120px' },
type: 'InputSetter',
key: 'msgContent.msg_9002', key: 'msgContent.msg_9002',
placeholder: '请勿多次提交!', placeholder: '请勿多次提交!',
value: '请勿多次提交!', value: '请勿多次提交!'
direction: 'horizon',
}, },
{ {
label: '提交结束', label: '提交结束',
type: 'Input', labelStyle: { width: '120px' },
type: 'InputSetter',
key: 'msgContent.msg_9003', key: 'msgContent.msg_9003',
placeholder: '您来晚了,已经满额!', placeholder: '您来晚了,已经满额!',
value: '您来晚了,已经满额!', value: '您来晚了,已经满额!'
direction: 'horizon',
}, },
{ {
label: '其他提交失败', label: '其他提交失败',
type: 'Input', labelStyle: { width: '120px' },
type: 'InputSetter',
key: 'msgContent.msg_9004', key: 'msgContent.msg_9004',
placeholder: '提交失败!', placeholder: '提交失败!',
value: '提交失败!', value: '提交失败!'
direction: 'horizon', }
}, ]
], }
}, ]
];

View File

@ -1,7 +1,6 @@
export default { export default {
'default-1': { 'default-1': {
'skinConf.backgroundConf.color': '#90b4fa', 'skinConf.backgroundConf.color': '#90b4fa',
'skinConf.themeConf.color': '#FAA600', 'skinConf.themeConf.color': '#FAA600'
} }
} }

View File

@ -0,0 +1,16 @@
import { cleanRichText } from '@/common/xss'
import type { DirectiveBinding, Directive, Plugin } from 'vue'
function _plainText(el: HTMLElement, binding: DirectiveBinding) {
const text = cleanRichText(binding.value)
el.innerText = `${text}`
}
const plainText: Directive & Plugin = {
mounted: _plainText,
updated: _plainText,
install: function (app) {
app.directive('plain-text', this)
}
}
export default plainText

View File

@ -0,0 +1,17 @@
import { filterXSS } from '@/common/xss'
import type { Directive, Plugin, DirectiveBinding } from 'vue'
function _safeHtml(el: HTMLElement, binding: DirectiveBinding) {
const res = filterXSS(binding.value)
el.innerHTML = res
}
const safeHtml: Directive & Plugin = {
mounted: _safeHtml,
updated: _safeHtml,
install: function (app) {
app.directive('safe-html', this)
}
}
export default safeHtml

View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/imgs/favicon.ico" />
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -1,36 +1,17 @@
import Vue from 'vue'; import { createApp } from 'vue'
import App from './App.vue'; import store from './store'
import router from './router'; import plainText from './directive/plainText'
import store from './store'; import safeHtml from './directive/safeHtml'
import ElementUI from 'element-ui';
import './styles/element-variables.scss';
import { filterXSS, cleanRichText } from '@/common/xss';
Vue.config.productionTip = false; import App from './App.vue'
Vue.use(ElementUI); import router from './router'
const safeHtml = function (el, binding) { const app = createApp(App)
const res = filterXSS(binding.value);
el.innerHTML = res;
};
const plainText = function (el, binding) { app.use(store)
const text = cleanRichText(binding.value); app.use(router)
el.innerText = text;
};
Vue.directive('safe-html', { app.use(plainText)
inserted: safeHtml, app.use(safeHtml)
componentUpdated: safeHtml,
});
Vue.directive('plain-text', { app.mount('#app')
inserted: plainText,
componentUpdated: plainText,
});
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');

View File

@ -6,7 +6,7 @@
<h2 class="data-list">数据列表</h2> <h2 class="data-list">数据列表</h2>
<div class="menus"> <div class="menus">
<el-switch <el-switch
:value="isShowOriginData" :model-value="isShowOriginData"
active-text="是否展示原数据" active-text="是否展示原数据"
@input="onIsShowOriginChange" @input="onIsShowOriginChange"
> >
@ -15,10 +15,7 @@
</template> </template>
<template v-if="tableData.total"> <template v-if="tableData.total">
<DataTable <DataTable :main-table-loading="mainTableLoading" :table-data="tableData" />
:main-table-loading="mainTableLoading"
:table-data="tableData"
/>
<el-pagination <el-pagination
background background
layout="prev, pager, next" layout="prev, pager, next"
@ -29,110 +26,114 @@
</el-pagination> </el-pagination>
</template> </template>
<div v-else> <div v-else>
<empty :data="noDataConfig" /> <EmptyIndex :data="noDataConfig" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import DataTable from './components/table.vue'; import { ElMessage } from 'element-plus'
import empty from '@/management/components/empty'; import 'element-plus/theme-chalk/src/message.scss'
import leftMenu from '@/management/components/leftMenu.vue';
import { getRecycleList } from '@/management/api/analysis'; import EmptyIndex from '@/management/components/EmptyIndex.vue'
import LeftMenu from '@/management/components/LeftMenu.vue'
import { getRecycleList } from '@/management/api/analysis'
import DataTable from './components/DataTable.vue'
export default { export default {
name: 'analysisPage', name: 'AnalysisPage',
data() { data() {
return { return {
mainTableLoading: false, mainTableLoading: false,
tableData: { tableData: {
total: 0, total: 0,
listHead: [], listHead: [],
listBody: [], listBody: []
}, },
noDataConfig: { noDataConfig: {
title: '暂无数据', title: '暂无数据',
desc: '您的问卷当前还没有数据,快去回收问卷吧!', desc: '您的问卷当前还没有数据,快去回收问卷吧!',
img: '/imgs/icons/analysis-empty.webp', img: '/imgs/icons/analysis-empty.webp'
}, },
currentPage: 1, currentPage: 1,
isShowOriginData: false, isShowOriginData: false,
tmpIsShowOriginData: false, tmpIsShowOriginData: false
}; }
}, },
computed: {}, computed: {},
created() { created() {
this.init(); this.init()
}, },
methods: { methods: {
async init() { async init() {
if (!this.$route.params.id) { if (!this.$route.params.id) {
this.$message.error('没有传入问卷参数~'); ElMessage.error('没有传入问卷参数~')
return; return
} }
this.mainTableLoading = true; this.mainTableLoading = true
try { try {
const res = await getRecycleList({ const res = await getRecycleList({
page: this.currentPage, page: this.currentPage,
surveyId: this.$route.params.id, surveyId: this.$route.params.id,
isDesensitive: !this.tmpIsShowOriginData, // isShowOriginData isDesensitive: !this.tmpIsShowOriginData // isShowOriginData
}); })
if (res.code === 200) { if (res.code === 200) {
const listHead = this.formatHead(res.data.listHead); const listHead = this.formatHead(res.data.listHead)
this.tableData = { ...res.data, listHead }; this.tableData = { ...res.data, listHead }
this.mainTableLoading = false; this.mainTableLoading = false
} }
} catch (error) { } catch (error) {
this.$message.error('查询回收数据失败,请重试'); ElMessage.error('查询回收数据失败,请重试')
} }
}, },
handleCurrentChange(current) { handleCurrentChange(current) {
if (this.mainTableLoading) { if (this.mainTableLoading) {
return; return
} }
this.currentPage = current; this.currentPage = current
this.init(); this.init()
}, },
formatHead(listHead = []) { formatHead(listHead = []) {
const head = []; const head = []
listHead.forEach((headItem) => { listHead.forEach((headItem) => {
head.push({ head.push({
field: headItem.field, field: headItem.field,
title: headItem.title, title: headItem.title
}); })
if (headItem.othersCode?.length) { if (headItem.othersCode?.length) {
headItem.othersCode.forEach((item) => { headItem.othersCode.forEach((item) => {
head.push({ head.push({
field: item.code, field: item.code,
title: `${headItem.title}-${item.option}`, title: `${headItem.title}-${item.option}`
}); })
}); })
} }
}); })
return head; return head
}, },
async onIsShowOriginChange(data) { async onIsShowOriginChange(data) {
if (this.mainTableLoading) { if (this.mainTableLoading) {
return; return
} }
// console.log(data) // console.log(data)
this.tmpIsShowOriginData = data; this.tmpIsShowOriginData = data
await this.init(); await this.init()
this.isShowOriginData = data; this.isShowOriginData = data
}, }
}, },
components: { components: {
DataTable, DataTable,
empty, EmptyIndex,
leftMenu, LeftMenu
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -166,7 +167,7 @@ export default {
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
::v-deep .el-pagination { :deep(.el-pagination) {
margin-top: 20px; margin-top: 20px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -0,0 +1,120 @@
<template>
<div class="data-table-wrapper">
<el-table
ref="multipleTable"
:data="props.tableData.listBody"
style="width: 100%"
header-row-class-name="thead-cell"
class="table-border"
v-loading="props.mainTableLoading"
element-loading-text="数据处理中,请稍等..."
>
<el-table-column
v-for="item in props.tableData.listHead"
:key="item.field"
:prop="item.field"
:label="cleanRichText(item.title)"
minWidth="200"
>
<template #header="scope">
<div class="table-row-cell">
<span
@mouseover="onPopoverRefOver(scope, 'head')"
:ref="(el) => (popoverRefMap[scope.column.id] = el)"
>
{{ scope.column.label.replace(/&nbsp;/g, '') }}
</span>
</div>
</template>
<template #default="scope">
<div>
<span
class="table-row-cell"
@mouseover="onPopoverRefOver(scope, 'content')"
:ref="(el) => (popoverRefMap[scope.$index + scope.column.property] = el)"
>
{{ getContent(scope.row[scope.column.property]) }}
</span>
</div>
</template>
</el-table-column>
</el-table>
<el-popover
ref="popover"
popper-style="text-align: center;"
:virtual-ref="popoverVirtualRef"
placement="top"
trigger="hover"
virtual-triggering
:content="popoverContent"
>
</el-popover>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { cleanRichText } from '@/common/xss'
const props = defineProps({
tableData: {
type: Object
},
mainTableLoading: {
type: Boolean
}
})
const popoverRefMap = ref({})
const popoverVirtualRef = ref()
const popoverContent = ref('')
const getContent = (value) => {
const content = cleanRichText(value)
return content === 0 ? 0 : content || '未知'
}
const setPopoverContent = (content) => {
popoverContent.value = content
}
const onPopoverRefOver = (scope, type) => {
let popoverContent
if (type == 'head') {
popoverVirtualRef.value = popoverRefMap.value[scope.column.id]
popoverContent = scope.column.label.replace(/&nbsp;/g, '')
}
if (type == 'content') {
popoverVirtualRef.value = popoverRefMap.value[scope.$index + scope.column.property]
popoverContent = getContent(scope.row[scope.column.property])
}
setPopoverContent(popoverContent)
}
</script>
<style lang="scss" scoped>
.data-table-wrapper {
position: relative;
width: 100%;
padding-bottom: 20px;
min-height: 620px;
background: #fff;
padding: 10px 20px;
.table-border {
box-sizing: border-box;
text-align: center;
}
:deep(.el-table__header) {
width: 100%;
.thead-cell .el-table__cell {
.cell {
height: 24px;
color: #4a4c5b;
font-size: 14px;
}
}
}
.table-row-cell {
white-space: nowrap; /* 禁止自动换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 显示省略号 */
}
}
</style>

View File

@ -1,108 +0,0 @@
<template>
<div class="data-table-wrapper">
<el-table
ref="multipleTable"
:data="tableData.listBody"
tooltip-effect="dark"
style="width: 100%"
header-row-class-name="thead-cell"
class="table-border"
show-overflow-tooltip
v-loading="mainTableLoading"
element-loading-text="数据处理中,请稍等..."
>
<el-table-column
v-for="item in tableData.listHead"
:key="item.field"
:prop="item.field"
:label="cleanRichText(item.title)"
minWidth="200"
>
<template slot="header" slot-scope="scope">
<div class="table-row-cell">
<span slot="reference" v-popover="scope.column.id">
{{ scope.column.label.replace(/&nbsp;/g, '') }}
</span>
<el-popover
:ref="scope.column.id"
placement="top-start"
width="200"
trigger="hover"
:content="scope.column.label.replace(/&nbsp;/g, '')"
>
</el-popover>
</div>
</template>
<template slot-scope="scope">
<span
slot="reference"
class="table-row-cell"
v-popover="scope.$index + scope.column.property"
>
{{ getContent(scope.row[scope.column.property]) }}
</span>
<el-popover
:ref="scope.$index + scope.column.property"
placement="top-start"
trigger="hover"
width="300"
:content="getContent(scope.row[scope.column.property])"
>
</el-popover>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { cleanRichText } from '@/common/xss';
export default {
name: 'DataTable',
data() {
return {};
},
props: {
mainTableLoading: Boolean,
tableData: Object,
},
methods: {
cleanRichText,
getContent(value) {
const content = cleanRichText(value)
return content === 0 ? 0 : (content || '未知')
}
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.data-table-wrapper {
position: relative;
width: 100%;
padding-bottom: 20px;
min-height: 620px;
background: #fff;
padding: 10px 20px;
.table-border {
box-sizing: border-box;
text-align: center;
}
::v-deep .el-table__header {
width: 100%;
.thead-cell .el-table__cell {
.cell {
height: 24px;
color: #4a4c5b;
font-size: 14px;
}
}
}
.table-row-cell {
white-space: nowrap; /* 禁止自动换行 */
overflow: hidden; /* 超出部分隐藏 */
text-overflow: ellipsis; /* 显示省略号 */
}
}
</style>

View File

@ -1,35 +1,34 @@
<template> <template>
<div class="new"> <div class="new">
<type-list <TypeList :selectType="selectType" @selectTypeChange="onSelectTypeChange" />
:selectType="selectType" <CreateForm :selectType="selectType" />
@selectTypeChange="onSelectTypeChange"
/>
<create-form :selectType="selectType" />
</div> </div>
</template> </template>
<script> <script>
import typeList from './components/typeList'; import TypeList from './components/TypeList.vue'
import createForm from './components/createForm'; import CreateForm from './components/CreateForm.vue'
export default { export default {
name: 'createPage', name: 'CreatePage',
components: { components: {
typeList, TypeList,
createForm, CreateForm
}, },
data() { data() {
return { return {
selectType: 'normal', selectType: 'normal'
}; }
}, },
methods: { methods: {
onSelectTypeChange(selectType) { onSelectTypeChange(selectType) {
this.selectType = selectType; this.selectType = selectType
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.new { .new {
position: relative; position: relative;
min-height: 750px; min-height: 750px;

View File

@ -8,20 +8,18 @@
:model="form" :model="form"
label-width="100px" label-width="100px"
:rules="rules" :rules="rules"
@submit.native.prevent @submit.prevent
> >
<el-form-item prop="title" label="问卷名称"> <el-form-item prop="title" label="问卷名称">
<el-input <el-input
v-model="form.title" v-model="form.title"
:class="form.title ? 'nonempty' : 'empty'" :class="form.title ? 'nonempty' : 'empty'"
size="small"
placeholder="请输入问卷名称" placeholder="请输入问卷名称"
/> />
<p class="form-item-tip">该标题可在打开问卷的浏览器顶部展示</p> <p class="form-item-tip">该标题可在打开问卷的浏览器顶部展示</p>
</el-form-item> </el-form-item>
<el-form-item prop="remark" label="问卷备注"> <el-form-item prop="remark" label="问卷备注">
<el-input <el-input
size="small"
v-model="form.remark" v-model="form.remark"
:class="form.remark ? 'nonempty' : 'empty'" :class="form.remark ? 'nonempty' : 'empty'"
placeholder="请输入备注" placeholder="请输入备注"
@ -29,91 +27,90 @@
<p class="form-item-tip">备注仅自己可见</p> <p class="form-item-tip">备注仅自己可见</p>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button class="create-btn" type="primary" @click="submit" :loading="!canSubmit">
class="create-btn"
type="primary"
size="small"
@click="submit"
:loading="!canSubmit"
>
开始创建 开始创建
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
</template> </template>
<script> <script>
import { SURVEY_TYPE_LIST } from '../types'; import { ElMessage } from 'element-plus'
import { createSurvey } from '@/management/api/survey'; import 'element-plus/theme-chalk/src/message.scss'
import { createSurvey } from '@/management/api/survey'
import { SURVEY_TYPE_LIST } from '../types'
export default { export default {
name: 'CreateForm', name: 'CreateForm',
props: { props: {
selectType: { selectType: {
type: String, type: String,
default: 'normal', default: 'normal'
}, }
}, },
data() { data() {
return { return {
rules: { rules: {
title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }], title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }]
}, },
canSubmit: true, canSubmit: true,
form: { form: {
title: '问卷调研', title: '问卷调研',
remark: '问卷调研', remark: '问卷调研'
}, }
}; }
}, },
computed: { computed: {
SURVEY_TYPE_LIST() { SURVEY_TYPE_LIST() {
return SURVEY_TYPE_LIST; return SURVEY_TYPE_LIST
}, },
title() { title() {
return this.SURVEY_TYPE_LIST.find((item) => item.type === this.selectType) return this.SURVEY_TYPE_LIST.find((item) => item.type === this.selectType)?.title
?.title; }
},
}, },
methods: { methods: {
checkForm(fn) { checkForm(fn) {
this.$refs.ruleForm.validate((valid) => { this.$refs.ruleForm.validate((valid) => {
valid && typeof fn === 'function' && fn(); valid && typeof fn === 'function' && fn()
}); })
}, },
submit() { submit() {
if (!this.canSubmit) { if (!this.canSubmit) {
return; return
} }
this.checkForm(async () => { this.checkForm(async () => {
const { selectType } = this; const { selectType } = this
if (!this.canSubmit) { if (!this.canSubmit) {
return; return
} }
this.canSubmit = false; this.canSubmit = false
const res = await createSurvey({ const res = await createSurvey({
surveyType: selectType, surveyType: selectType,
...this.form, ...this.form
}); })
if (res.code === 200 && res?.data?.id) { if (res.code === 200 && res?.data?.id) {
const id = res.data.id; const id = res.data.id
this.$router.push({ this.$router.push({
name: 'QuestionEditIndex', name: 'QuestionEditIndex',
params: { params: {
id, id
}, }
}); })
} else { } else {
this.$message.error(res.errmsg || '创建失败'); ElMessage.error(res.errmsg || '创建失败')
} }
this.canSubmit = true; this.canSubmit = true
}); })
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.right-side { .right-side {
width: 538px; width: 538px;
margin: auto; margin: auto;
@ -142,7 +139,7 @@ export default {
border: unset; border: unset;
color: white; color: white;
::v-deep span { :deep(span) {
font-size: 14px; font-size: 14px;
} }
} }

View File

@ -8,33 +8,35 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
boxShadow: { boxShadow: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}, },
name: 'NavHeader', name: 'NavHeader',
data() { data() {
return { return {
img: '/imgs/s-logo.webp', img: '/imgs/s-logo.webp'
}; }
}, },
methods: { methods: {
toHomePage() { toHomePage() {
this.$router.replace({ this.$router.replace({
name: 'survey', name: 'survey'
}); })
}, },
onBack() { onBack() {
this.$router.go(-1); this.$router.go(-1)
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.nav-header { .nav-header {
z-index: 99; z-index: 99;
position: relative; position: relative;

View File

@ -23,36 +23,38 @@
</div> </div>
</div> </div>
</template> </template>
<script>
import NavHeader from './navHeader';
import { SURVEY_TYPE_LIST } from '../types'; <script>
import NavHeader from './NavHeader.vue'
import { SURVEY_TYPE_LIST } from '../types'
export default { export default {
name: 'LeftSide', name: 'LeftSide',
components: { components: {
NavHeader, NavHeader
}, },
props: { props: {
selectType: { selectType: {
type: String, type: String,
default: '', default: ''
}, }
}, },
computed: { computed: {
renderData() { renderData() {
return SURVEY_TYPE_LIST; return SURVEY_TYPE_LIST
}, }
}, },
methods: { methods: {
handleSelectType(key, value) { handleSelectType(key, value) {
const { type } = value; const { type } = value
this.$emit('selectTypeChange', type); this.$emit('selectTypeChange', type)
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.left-side { .left-side {
position: relative; position: relative;
height: 100%; height: 100%;

View File

@ -3,7 +3,7 @@ export const SURVEY_TYPE_LIST = [
type: 'normal', type: 'normal',
title: '基础调查', title: '基础调查',
img: '/imgs/create/normal-icon.webp', img: '/imgs/create/normal-icon.webp',
desc: '市场调研 / 用户分析 / 产品测评 / 需求调研', desc: '市场调研 / 用户分析 / 产品测评 / 需求调研'
}, },
// { // {
// type: 'nps', // type: 'nps',
@ -15,12 +15,12 @@ export const SURVEY_TYPE_LIST = [
type: 'vote', type: 'vote',
title: '投票评选', title: '投票评选',
img: '/imgs/create/vote-icon.webp', img: '/imgs/create/vote-icon.webp',
desc: '才艺比赛 / 优秀员工 / 最佳人气 / 投票选举', desc: '才艺比赛 / 优秀员工 / 最佳人气 / 投票选举'
}, },
{ {
type: 'register', type: 'register',
title: '在线报名', title: '在线报名',
img: '/imgs/create/register-icon.webp', img: '/imgs/create/register-icon.webp',
desc: '活动报名 / 会议报名', desc: '活动报名 / 会议报名'
}, }
]; ]

View File

@ -1,29 +1,31 @@
<template> <template>
<div class="main"> <div class="main">
<div class="nav" v-if="$slots.hasOwnProperty('nav')"> <div class="nav" v-if="slots.hasOwnProperty('nav')">
<slot name="nav"></slot> <slot name="nav"></slot>
</div> </div>
<div class="body"> <div class="body">
<slot v-if="$slots.hasOwnProperty('body')" name="body"></slot> <slot v-if="slots.hasOwnProperty('body')" name="body"></slot>
<template v-else> <template v-else>
<div class="left" v-if="$slots.hasOwnProperty('left')"> <div class="left" v-if="slots.hasOwnProperty('left')">
<slot name="left"></slot> <slot name="left"></slot>
</div> </div>
<div class="center" v-if="$slots.hasOwnProperty('center')"> <div class="center" v-if="slots.hasOwnProperty('center')">
<slot name="center"></slot> <slot name="center"></slot>
</div> </div>
<div class="right" v-if="$slots.hasOwnProperty('right')"> <div class="right" v-if="slots.hasOwnProperty('right')">
<slot name="right"></slot> <slot name="right"></slot>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
</template> </template>
<script>
export default { <script setup>
name: 'commonTemplate', import { useSlots } from 'vue'
};
const slots = useSlots()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.main { .main {
width: 100%; width: 100%;

View File

@ -14,6 +14,7 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'LogoPreview', name: 'LogoPreview',
@ -22,24 +23,25 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
isSelected: Boolean, isSelected: Boolean
}, },
data() { data() {
return {}; return {}
}, },
methods: { methods: {
onSelect() { onSelect() {
this.$emit('select'); this.$emit('select')
}, }
}, },
computed: { computed: {
logoImg() { logoImg() {
const { logoImage = {} } = this.logoConf; const { logoImage } = this.logoConf
return logoImage; return logoImage
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" rel="stylesheet/scss" scoped>
.container { .container {
display: flex; display: flex;

View File

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

View File

@ -0,0 +1,101 @@
<template>
<draggable
:list="renderData"
handle=".question-wrapper.isSelected"
filter=".question-wrapper.isSelected .question.isSelected"
:onEnd="checkEnd"
:move="checkMove"
itemKey="field"
>
<template #item="{ element, index }">
<QuestionWrapper
v-bind="$attrs"
:ref="`questionWrapper-${element.field}`"
:moduleConfig="element"
:qIndex="element.qIndex"
:indexNumber="element.indexNumber"
:isSelected="currentEditOne === index"
:isLast="index + 1 === questionDataList.length"
@select="handleSelect"
>
<QuestionContainerB
v-bind="$attrs"
:type="element.type"
:moduleConfig="element"
:indexNumber="element.indexNumber"
:isSelected="currentEditOne === index"
:readonly="true"
@select="handleSelect"
></QuestionContainerB>
</QuestionWrapper>
</template>
</draggable>
</template>
<script>
import { computed, defineComponent, ref, getCurrentInstance } from 'vue'
import QuestionContainerB from '@/materials/questions/QuestionContainerB'
import QuestionWrapper from '@/management/pages/edit/components/QuestionWrapper.vue'
import draggable from 'vuedraggable'
import { filterQuestionPreviewData } from '@/management/utils/index'
export default defineComponent({
components: {
draggable,
QuestionWrapper,
QuestionContainerB
},
props: {
currentEditOne: {
type: [Number, String],
default: null
},
questionDataList: {
type: Array,
default: () => {
return []
}
}
},
setup(props, { emit }) {
const renderData = computed(() => {
return filterQuestionPreviewData(props.questionDataList)
})
const handleSelect = (index) => {
emit('select', index)
}
const handleChangeSeq = (data) => {
emit('changeSeq', data)
}
const isMoving = ref(false)
const checkMove = () => {
isMoving.value = true
}
const checkEnd = ({ oldIndex, newIndex }) => {
emit('changeSeq', {
type: 'move',
index: oldIndex,
range: newIndex - oldIndex
})
}
const instance = getCurrentInstance()
const getQuestionRefByField = (field) => {
return instance?.proxy?.$refs[`questionWrapper-${field}`] || null
}
return {
renderData,
handleSelect,
handleChangeSeq,
checkMove,
checkEnd,
getQuestionRefByField
}
}
})
</script>

View File

@ -1,49 +1,49 @@
<template> <template>
<div class="nav"> <div class="nav">
<div class="left-group"> <div class="left-group">
<back></back> <BackPanel></BackPanel>
<pageTitle :style="{ marginLeft: '30px' }" :title="title"></pageTitle> <TitlePanel :style="{ marginLeft: '30px' }" :title="title"></TitlePanel>
</div> </div>
<div class="center-group"> <div class="center-group">
<pageNav></pageNav> <NavPanel></NavPanel>
</div> </div>
<div class="right-group"> <div class="right-group">
<history></history> <HistoryPanel></HistoryPanel>
<save></save> <SavePanel></SavePanel>
<publish></publish> <PublishPanel></PublishPanel>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import back from '../modules/generalModule/back.vue'; import BackPanel from '../modules/generalModule/BackPanel.vue'
import pageTitle from '../modules/generalModule/pageTitle.vue'; import TitlePanel from '../modules/generalModule/TitlePanel.vue'
import pageNav from '../modules/generalModule/pageNav.vue'; import NavPanel from '../modules/generalModule/NavPanel.vue'
import history from '../modules/contentModule/history.vue'; import HistoryPanel from '../modules/contentModule/HistoryPanel.vue'
import save from '../modules/contentModule/save.vue'; import SavePanel from '../modules/contentModule/SavePanel.vue'
import publish from '../modules/contentModule/publish.vue'; import PublishPanel from '../modules/contentModule/PublishPanel.vue'
import { mapState } from 'vuex'; import { mapState } from 'vuex'
import { get as _get } from 'lodash-es'; import { get as _get } from 'lodash-es'
export default { export default {
name: 'navbar', name: 'ModuleNavbar',
components: { components: {
back, BackPanel,
pageTitle, TitlePanel,
pageNav, NavPanel,
history, HistoryPanel,
save, SavePanel,
publish, PublishPanel
}, },
data() { data() {
return {}; return {}
}, },
computed: { computed: {
...mapState({ ...mapState({
title: (state) => _get(state, 'edit.schema.metaData.title'), title: (state) => _get(state, 'edit.schema.metaData.title')
}), })
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -0,0 +1,182 @@
<template>
<div
:class="itemClass"
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
@click="clickFormItem"
>
<div><slot v-if="moduleConfig.type !== 'section'"></slot></div>
<div :class="[showHover ? 'visibily' : 'hidden', 'hoverItem']">
<div class="item el-icon-rank" @click.stop.prevent="onMove">
<i-ep-rank />
</div>
<div v-if="showUp" class="item" @click.stop.prevent="onMoveUp">
<i-ep-top />
</div>
<div v-if="showDown" class="item" @click.stop.prevent="onMoveDown">
<i-ep-bottom />
</div>
<div v-if="showCopy" class="item" @click.stop.prevent="onCopy">
<i-ep-copyDocument />
</div>
<div class="item" @click.stop.prevent="onDelete">
<i-ep-close />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message-box.scss'
const props = defineProps({
qIndex: {
type: Number,
default: 0
},
indexNumber: {
type: Number,
default: 1
},
isSelected: {
type: Boolean,
default: false
},
isLast: {
type: Boolean,
default: false
},
moduleConfig: {
type: Object,
default: () => {
return {}
}
}
})
const emit = defineEmits(['changeSeq', 'select'])
const isHover = ref(false)
const itemClass = computed(() => {
return {
'question-wrapper': true,
'mouse-hover': isHover.value,
isSelected: props.isSelected,
spliter: props.moduleConfig.showSpliter
}
})
const showHover = computed(() => {
return isHover.value
})
const showUp = computed(() => {
return props.qIndex !== 0
})
const showDown = computed(() => {
return !props.isLast
})
const showCopy = computed(() => {
const field = props.moduleConfig.field
const hiddenCopFields = ['mob', 'mobileHidden', 'userAgreement']
return hiddenCopFields.indexOf(field) <= -1
})
const clickFormItem = () => {
const index = props.qIndex
emit('select', index)
}
const onCopy = () => {
const index = props.qIndex
emit('changeSeq', { type: 'copy', index })
isHover.value = false
return false
}
const onMoveUp = () => {
const index = props.qIndex
emit('changeSeq', { type: 'move', index, range: -1 })
isHover.value = false
}
const onMouseenter = () => {
isHover.value = true
}
const onMouseleave = () => {
isHover.value = false
}
const onMoveDown = () => {
const index = props.qIndex
emit('changeSeq', { type: 'move', index, range: 1 })
isHover.value = false
}
const onDelete = async () => {
try {
await ElMessageBox.confirm('本次操作会影响数据统计查看,是否确认删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const index = props.qIndex
emit('changeSeq', { type: 'delete', index })
isHover.value = false
} catch (error) {
console.log('取消删除')
}
}
const onMove = () => {}
</script>
<style lang="scss" scoped>
.question-wrapper {
position: relative;
padding: 0.36rem 0 0.36rem;
border: 1px solid transparent;
&.spliter {
border-bottom: 0.12rem solid $spliter-color;
}
&.mouse-hover {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.09);
}
&.isSelected {
background-color: #f2f4f7;
box-shadow: 0 0 5px #e3e4e8;
}
.hoverItem {
position: absolute;
top: 0;
margin-top: -5px;
right: -32px;
z-index: 2;
display: flex;
flex-direction: column;
&.hidden {
display: none;
}
.item {
display: flex;
align-items: center;
justify-content: center;
margin-top: 5px;
width: 28px;
height: 28px;
border-radius: 50%;
background: #eceff1;
margin-right: 2px;
cursor: pointer;
color: #506b7b;
font-size: 12px;
text-align: center;
line-height: 28px;
&:hover {
background-color: $primary-color;
color: #fff;
}
}
}
}
</style>

View File

@ -0,0 +1,225 @@
<template>
<el-form class="config-form" @submit.prevent>
<div v-for="(item, index) in formFieldData" :key="`${item.key}${index}`" class="group-wrap">
<div v-if="item.title" class="group-title">
{{ item.title }}
<el-tooltip v-if="item.tip" :content="item.tip" placement="right">
<i-ep-questionFilled class="icon-tip" />
</el-tooltip>
</div>
<template v-if="item.type === 'Customed'">
<FormItem
v-for="(content, contentIndex) in item.content"
:key="`${item.key}${contentIndex}`"
:form-config="content"
>
<Component
:is="content.type"
:form-config="content"
:module-config="moduleConfig"
@form-change="onFormChange($event, content)"
:class="content.contentClass"
/>
</FormItem>
</template>
<FormItem v-else :form-config="item">
<Component
:is="item.type"
:form-config="item"
:module-config="moduleConfig"
@form-change="onFormChange($event, item)"
:class="item.contentClass"
/>
</FormItem>
</div>
</el-form>
</template>
<script>
import { get as _get, pick as _pick, isFunction as _isFunction } from 'lodash-es'
import FormItem from '@/materials/setters/widgets/FormItem.vue'
import setterLoader from '@/materials/setters/setterLoader'
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
//
const formatValue = ({ item, moduleConfig }) => {
if (_isFunction(item.valueAdapter)) {
const value = item.valueAdapter({ moduleConfig })
return value
} else {
const { key, keys } = item
let result = null
if (key) {
result = _get(moduleConfig, key, item.value)
}
if (keys) {
result = _pick(moduleConfig, keys)
}
return result
}
}
export default {
name: 'SettersField',
props: {
formConfigList: Array, // meta.js
moduleConfig: Object
},
data() {
return {
register: {},
formFieldData: [],
init: true
}
},
components: {
FormItem
},
watch: {
formConfigList: {
deep: true,
immediate: true,
async handler(newVal) {
this.init = true
if (!newVal || !newVal.length) {
return
}
//
await this.handleComponentRegister(newVal)
this.init = false
this.formFieldData = this.setValues(this.formConfigList)
}
},
// schema
moduleConfig: {
deep: true,
async handler() {
// value
if (this.init) {
return
}
// TODO: schema
this.formFieldData = this.setValues(this.formConfigList)
}
}
},
methods: {
setValues(configList = []) {
return configList
.filter((item) => {
//
if (item.type === 'Customed') {
item.content = this.setValues(item.content)
return true
}
if (!item.type) {
return false
}
if (item.hidden) {
return false
}
//
if (_isFunction(item.relyFunc)) {
return item.relyFunc(this.moduleConfig)
}
return true
})
.map((item) => {
return {
...item,
value: formatValue({ item, moduleConfig: this.moduleConfig }) //
}
})
},
async handleComponentRegister(formFieldData) {
let innerSetters = []
const setters = formFieldData.map((item) => {
if (item.type === 'Customed') {
innerSetters.push(...(item.content || []).map((content) => content.type))
}
return item.type
})
const settersSet = new Set([...setters, ...innerSetters])
const settersArr = Array.from(settersSet)
const allSetters = settersArr.map((item) => {
return {
type: item,
path: item
}
})
try {
const comps = await setterLoader.loadComponents(allSetters)
for (const comp of comps) {
if (!comp) {
continue
}
const { type, component, err } = comp
if (!err) {
const componentName = component.name
if (!this.$options.components) {
this.$options.components = {}
}
this.$options.components[componentName] = component
this.register[type] = componentName
}
}
} catch (err) {
console.error(err)
}
},
onFormChange(data, formConfig) {
if (_isFunction(formConfig?.setterAdapter)) {
const resultData = formConfig.setterAdapter(data)
if (Array.isArray(resultData)) {
resultData.forEach((item) => {
this.$emit(FORM_CHANGE_EVENT_KEY, item)
})
} else {
this.$emit(FORM_CHANGE_EVENT_KEY, resultData)
}
} else {
this.$emit(FORM_CHANGE_EVENT_KEY, data)
}
}
}
}
</script>
<style lang="scss" scoped>
.config-form {
padding: 15px 0;
.group-wrap {
margin-bottom: 20px;
}
.group-title {
font-size: 14px;
color: #606266;
margin-bottom: 20px;
font-weight: bold;
align-items: center;
display: flex;
.icon-tip {
font-size: 13px;
color: #606266;
}
}
}
</style>

View File

@ -1,38 +1,32 @@
<template> <template>
<div <div class="submit-wrapper" @click="onClick" :class="{ isSelected: isSelected }">
class="submit-wrapper" <el-button class="submit-btn" type="primary">{{ submitConf.submitTitle }}</el-button>
@click="onClick"
:class="{ isSelected: isSelected }"
>
<el-button
class="submit-btn"
type="primary"
>{{ submitConf.submitTitle }}</el-button
>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'Submit', name: 'SubmitButton',
data() { data() {
return {}; return {}
}, },
props: { props: {
submitConf: Object, submitConf: Object,
isSelected: Boolean, isSelected: Boolean,
skinConf: { skinConf: {
type: Object, type: Object,
required: true, required: true
}, }
}, },
methods: { methods: {
onClick() { onClick() {
this.$emit('select'); this.$emit('select')
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.submit-wrapper { .submit-wrapper {
padding: 25px; padding: 25px;
text-align: center; text-align: center;

View File

@ -1,121 +0,0 @@
<script>
import { computed, defineComponent, ref, getCurrentInstance } from 'vue';
import QuestionContainer from '@/materials/questions/widgets/QuestionContainer.jsx';
import questionWrapper from './questionWrapper.vue';
import draggable from 'vuedraggable';
import { filterQuestionPreviewData } from '@/management/utils/index';
export default defineComponent({
name: '',
components: { draggable },
props: {
currentEditOne: {
type: [Number, String],
default: null,
},
questionDataList: {
type: Array,
default: () => {
return [];
},
},
},
watch: {},
setup(props, { emit }) {
const renderData = computed(() => {
return filterQuestionPreviewData(props.questionDataList);
});
const handleSelect = (index) => {
emit('select', index);
};
const handleChangeSeq = (data) => {
emit('changeSeq', data);
};
const isMoving = ref(false);
const checkMove = () => {
isMoving.value = true;
};
const checkEnd = ({ oldIndex, newIndex }) => {
emit('changeSeq', {
type: 'move',
index: oldIndex,
range: newIndex - oldIndex,
});
};
const instance = getCurrentInstance();
const getQuestionRefByField = (field) => {
return instance?.proxy?.$refs[`questionWrapper-${field}`] || null;
};
return {
renderData,
handleSelect,
handleChangeSeq,
checkMove,
checkEnd,
dragOptions: {
animation: 0,
group: 'previewList',
handle: '.el-icon-rank',
scroll: true,
scrollSpeed: 2500,
scrollSensitivity: 150,
forceFallback: true,
},
getQuestionRefByField,
};
},
render(h) {
return (
<draggable
list={this.renderData}
options={this.dragOptions}
onEnd={this.checkEnd}
move={this.checkMove}
>
{this.renderData.map((item, index) => {
return h(
questionWrapper,
{
ref: `questionWrapper-${item.field}`,
key: item.field,
props: {
moduleConfig: item,
qIndex: item.qIndex,
indexNumber: item.indexNumber,
isSelected: this.currentEditOne === index,
isLast: index + 1 === this.questionDataList.length,
},
on: {
...this.$listeners,
select: this.handleSelect,
changeSeq: this.handleChangeSeq,
},
},
[
h(QuestionContainer, {
props: {
type: item.type,
moduleConfig: item,
indexNumber: item.indexNumber,
isSelected: this.currentEditOne === index,
readonly: true,
},
on: {
...this.$listeners,
select: this.handleSelect,
},
}),
]
);
})}
</draggable>
);
},
});
</script>

View File

@ -1,335 +0,0 @@
<script>
import {
defineComponent,
reactive,
toRefs,
computed,
getCurrentInstance,
} from 'vue';
export default defineComponent({
name: 'QuestionWrapper',
props: {
qIndex: {
type: Number,
default: 0,
},
indexNumber: {
type: Number,
default: 1,
},
isSelected: {
type: Boolean,
default: false,
},
isLast: {
type: Boolean,
default: false,
},
moduleConfig: {
type: Object,
default: () => {
return {};
},
},
},
setup(props, { emit }) {
const state = reactive({
isHover: false,
});
const { proxy } = getCurrentInstance();
const itemClass = computed(() => {
return {
'question-wrapper': true,
'mouse-hover': state.isHover,
isSelected: props.isSelected,
spliter: props.moduleConfig.showSpliter,
};
});
const showHover = computed(() => {
return state.isHover;
});
const showUp = computed(() => {
return props.qIndex !== 0;
});
const showDown = computed(() => {
return !props.isLast;
});
const showCopy = computed(() => {
const field = props.moduleConfig.field;
const hiddenCopFields = ['mob', 'mobileHidden', 'userAgreement'];
return hiddenCopFields.indexOf(field) <= -1;
});
const toggleHoverClass = (status) => {
state.isHover = status;
};
const clickFormItem = () => {
const index = props.qIndex;
emit('select', index);
};
const onCopy = () => {
const index = props.qIndex;
// this.changeQuestionSeq({ type: 'copy', index })
emit('changeSeq', { type: 'copy', index });
state.isHover = false;
};
const onMoveUp = () => {
const index = props.qIndex;
// this.changeQuestionSeq({ type: 'move', index, range: -1 })
emit('changeSeq', { type: 'move', index, range: -1 });
state.isHover = false;
};
// const onMoveTop = () => {
// const index = props.qIndex
// // this.changeQuestionSeq({ type: 'move', index, range: -index })
// emit('changeSeq', { type: 'move', index, range: -index })
// state.isHover = false
// }
const onMoveDown = () => {
const index = props.qIndex;
// this.changeQuestionSeq({ type: 'move', index, range: 1 })
emit('changeSeq', { type: 'move', index, range: 1 });
state.isHover = false;
};
const onDelete = async () => {
try {
await proxy.$confirm(
'本次操作会影响数据统计查看,是否确认删除?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
const index = props.qIndex;
// this.changeQuestionSeq({ type: 'move', index, range: 1 })
emit('changeSeq', { type: 'delete', index });
state.isHover = false;
} catch (error) {
console.log('取消删除');
}
};
// const onMoveBottom = () => {
// const index = props.qIndex
// this.changeQuestionSeq({
// type: 'move',
// index,
// range: props.questionDataList.length - index,
// })
// state.isHover = false
// }
return {
...toRefs(state),
itemClass,
showHover,
showUp,
showDown,
showCopy,
onCopy,
onMoveUp,
onMoveDown,
onDelete,
toggleHoverClass,
clickFormItem,
};
},
render() {
const { showHover, itemClass, showUp, showDown, showCopy } = this;
return (
<div
class={itemClass}
onMouseenter={() => {
this.isHover = true;
}}
onMouseleave={() => {
this.isHover = false;
}}
onClick={this.clickFormItem}
>
{this.moduleConfig.type !== 'section' && (
<div>{this.$slots.default}</div>
)}
{
<div class={[showHover ? 'visibily' : 'hidden', 'hoverItem']}>
<div
class="item move el-icon-rank"
vOn:click_stop_prevent={this.onMove}
></div>
{showUp && (
<div
class="item iconfont icon-shangyi"
vOn:click_stop_prevent={this.onMoveUp}
></div>
)}
{showDown && (
<div
class="item iconfont icon-xiayi"
vOn:click_stop_prevent={this.onMoveDown}
></div>
)}
{showCopy && (
<div
class="item copy iconfont icon-fuzhi"
vOn:click_stop_prevent={this.onCopy}
></div>
)}
<div
class="item iconfont icon-shanchu"
vOn:click_stop_prevent={this.onDelete}
></div>
</div>
}
</div>
);
},
});
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.question-wrapper {
position: relative;
padding: 0.36rem 0 0.36rem;
border: 1px solid transparent;
&.spliter {
border-bottom: 0.12rem solid $spliter-color;
}
&:last-child{
border: none;
}
.editor {
display: flex;
font-size: 0.32rem;
margin-bottom: 0.4rem;
padding: 0 0.4rem;
.icon-required {
color: $error-color;
position: absolute;
left: 0.16rem;
font-size: 0.46rem;
}
.index {
flex-shrink: 0;
}
}
.component-wrapper {
padding: 0 0.4rem;
.editor {
padding-left: 0.4rem;
}
}
&.no-padding {
.component-wrapper {
padding: 0;
}
}
&.mouse-hover {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.09);
}
&.isSelected {
background-color: #f2f4f7;
box-shadow: 0 0 5px #e3e4e8;
}
.clear {
clear: both;
}
&.question-type-section {
.module-title {
padding-bottom: 0;
}
}
&.horizon {
display: flex;
.module-title .m-title {
width: 1.2rem;
margin-right: 8px;
text-align: justify;
position: relative;
&::before {
content: ':';
display: block;
position: absolute;
right: -5px;
}
&::after {
content: '';
display: inline-block;
width: 100%;
}
}
.component-wrapper {
flex: 1;
}
}
.hoverItem {
position: absolute;
top: 0;
margin-top: -5px;
right: -32px;
z-index: 2;
display: flex;
flex-direction: column;
&.hidden {
display: none;
}
.item {
margin-top: 5px;
display: inline-block;
width: 28px;
height: 28px;
border-radius: 50%;
background: #eceff1;
margin-right: 2px;
cursor: pointer;
color: #506b7b;
font-size: 12px;
text-align: center;
line-height: 28px;
&:hover {
background-color: $primary-color;
color: #fff;
}
}
.move {
cursor: move;
font-size: 14px;
}
.copy {
font-size: 14px;
}
}
.titleGray {
color: #ddd;
}
.relation-show,
.jumpto-show,
.listenmerge-show {
margin-top: 0.4rem;
font-size: 12px;
color: $placeholder-color;
padding: 0 0.4rem;
}
.relyList {
white-space: pre-wrap;
}
.font-bold {
font-weight: 500;
}
.option-origin-text {
color: #ccc;
margin-left: 17px;
}
.sort-tip {
font-size: 0.26rem;
line-height: 0.26rem;
opacity: 0.5;
margin-top: -0.24rem;
margin-bottom: 0.4rem;
padding-left: 0.4rem;
color: #92949d;
}
}
</style>

View File

@ -1,190 +0,0 @@
<template>
<el-form
class="config-form"
size="small"
:labelPosition="labelPosition"
label-width="110px"
:inline="inline"
@submit.native.prevent
>
<template v-for="(item, index) in formFieldData">
<FormItem :key="item.key + index" class="form-item" :form-config="item">
<template v-if="item.type === 'Customed'">
<SettersField
:key="index"
:form-config-list="item.content"
:module-config="moduleConfig"
@form-change="onFormChange($event, item)"
:inline="true"
labelPosition="left"
:class="item.contentClass"
></SettersField>
</template>
<Component
v-else
:is="item.type"
:module-config="moduleConfig"
:form-config="item"
@form-change="onFormChange($event, item)"
:slot="item.contentPosition || null"
/>
</FormItem>
</template>
</el-form>
</template>
<script>
import {
get as _get,
pick as _pick,
isFunction as _isFunction,
} from 'lodash-es';
import FormItem from '@/materials/setters/widgets/FormItem.vue';
import setterLoader from '@/materials/setters/setterLoader';
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant';
const formatValue = ({ item, moduleConfig }) => {
if (_isFunction(item.valueAdapter)) {
const value = item.valueAdapter({ moduleConfig });
return value;
} else {
const { key, keys } = item;
let result = null;
if (key) {
result = _get(moduleConfig, key, item.value);
}
if (keys) {
result = _pick(moduleConfig, keys);
}
return result;
}
};
export default {
name: 'SettersField',
props: {
formConfigList: Array,
moduleConfig: Object,
inline: {
type: Boolean,
default: false,
},
labelPosition: {
type: String,
default: 'top',
},
},
data() {
return {
registerd: {},
};
},
components: {
FormItem,
},
computed: {
formFieldData() {
return this.formConfigList
.filter((item) => {
if (!item.type) {
return false;
}
if (item.type !== 'Customed' && !this.registerd[item.type]) {
return false;
}
if (item.hidden) {
return false;
}
if (_isFunction(item.relyFunc)) {
return item.relyFunc(this.moduleConfig);
}
return true;
})
.map((item) => {
return {
...item,
value: formatValue({ item, moduleConfig: this.moduleConfig }),
};
});
},
},
watch: {
formConfigList: {
deep: true,
immediate: true,
handler(newVal) {
if (!newVal || !newVal.length) {
return;
}
this.handleComponentRegister(newVal);
},
},
},
methods: {
async handleComponentRegister(formFieldData) {
const setters = formFieldData.map((item) => item.type);
const settersSet = new Set(setters);
const settersArr = Array.from(settersSet);
const allSetters = settersArr.map((item) => {
return {
type: item,
path: item,
};
});
try {
const comps = await setterLoader.loadComponents(allSetters);
for (const comp of comps) {
if (!comp) {
continue;
}
const { type, component, err } = comp;
if (!err) {
const componentName = component.name;
if (!this.$options.components) {
this.$options.components = {};
}
this.$options.components[componentName] = component;
this.$set(this.registerd, type, componentName);
}
}
} catch (err) {
console.error(err);
}
},
onFormChange(data, formConfig) {
if (_isFunction(formConfig?.setterAdapter)) {
const resultData = formConfig.setterAdapter(data);
if (Array.isArray(resultData)) {
resultData.forEach((item) => {
this.$emit(FORM_CHANGE_EVENT_KEY, item);
});
} else {
this.$emit(FORM_CHANGE_EVENT_KEY, resultData);
}
} else {
this.$emit(FORM_CHANGE_EVENT_KEY, data);
}
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.config-form {
padding: 15px 0;
}
.nps-customed-config {
.el-form-item {
margin-right: 0px;
::v-deep .el-form-item__label {
width: 70px !important;
margin-right: 8px;
}
::v-deep .el-input__inner {
width: 234px;
}
}
}
</style>

View File

@ -1,42 +1,52 @@
<template> <template>
<div class="edit-index"> <div class="edit-index">
<leftMenu class="left"></leftMenu> <LeftMenu class="left"></LeftMenu>
<div class="right"> <div class="right">
<commonTemplate style="background-color: #f6f7f9"> <CommonTemplate style="background-color: #f6f7f9">
<navbar class="navbar" slot="nav"></navbar> <template #nav>
<router-view slot="body"></router-view> <Navbar class="navbar"></Navbar>
</commonTemplate> </template>
<template #body>
<router-view></router-view>
</template>
</CommonTemplate>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import commonTemplate from './components/commonTemplate.vue'; import { ElMessage } from 'element-plus'
import navbar from './components/navbar.vue'; import 'element-plus/theme-chalk/src/message.scss'
import leftMenu from '@/management/components/leftMenu.vue';
import LeftMenu from '@/management/components/LeftMenu.vue'
import CommonTemplate from './components/CommonTemplate.vue'
import Navbar from './components/ModuleNavbar.vue'
export default { export default {
name: 'questionEditPage', name: 'questionEditPage',
components: { components: {
commonTemplate, CommonTemplate,
navbar, Navbar,
leftMenu, LeftMenu
}, },
async created() { async created() {
this.$store.commit('edit/setSurveyId', this.$route.params.id); this.$store.commit('edit/setSurveyId', this.$route.params.id)
try { try {
await this.$store.dispatch('edit/init'); await this.$store.dispatch('edit/init')
} catch (error) { } catch (error) {
this.$message.error(error.message); ElMessage.error(error.message)
// //
setTimeout(() => { setTimeout(() => {
this.$router.replace({ this.$router.replace({
name: 'survey', name: 'survey'
}); })
}, 1000); }, 1000)
} }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.edit-index { .edit-index {
height: 100%; height: 100%;
@ -56,6 +66,7 @@ export default {
padding-left: 80px; padding-left: 80px;
overflow: hidden; overflow: hidden;
} }
.navbar { .navbar {
border-bottom: 1px solid #e7e9eb; border-bottom: 1px solid #e7e9eb;
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<el-popover placement="top" trigger="click" @show="onShow"> <el-popover placement="top" trigger="click" @show="onShow" :width="320">
<el-tabs v-model="currentTab" class="custom-tab" v-if="visible"> <el-tabs v-model="currentTab" class="custom-tab" v-if="visible">
<el-tab-pane label="修改历史" name="daily" class="custom-tab-pane"> <el-tab-pane label="修改历史" name="daily" class="custom-tab-pane">
<div class="line" v-for="(his, index) in dailyList" :key="index"> <div class="line" v-for="(his, index) in dailyList" :key="index">
@ -16,48 +16,51 @@
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<div class="btn" slot="reference"> <template #reference>
<i class="iconfont icon-lishi"></i> <div class="btn">
<span class="btn-txt">历史</span> <i class="iconfont icon-lishi"></i>
</div> <span class="btn-txt">历史</span>
</div>
</template>
</el-popover> </el-popover>
</template> </template>
<script>
import { getSurveyHistory } from '@/management/api/survey';
import moment from 'moment';
//
import 'moment/locale/zh-cn';
//
moment.locale('zh-cn');
import { mapState } from 'vuex'; <script>
import { get as _get } from 'lodash-es'; import { getSurveyHistory } from '@/management/api/survey'
import moment from 'moment'
//
import 'moment/locale/zh-cn'
//
moment.locale('zh-cn')
import { mapState } from 'vuex'
import { get as _get } from 'lodash-es'
const getItemData = (item) => ({ const getItemData = (item) => ({
operator: item?.operator?.username || '未知用户', operator: item?.operator?.username || '未知用户',
time: moment(item.createDate).format('YYYY-MM-DD HH:mm:ss'), time: moment(item.createDate).format('YYYY-MM-DD HH:mm:ss')
}); })
export default { export default {
name: 'history', name: 'HistoryPanel',
computed: { computed: {
...mapState({ ...mapState({
surveyId: (state) => _get(state, 'edit.surveyId'), surveyId: (state) => _get(state, 'edit.surveyId')
}), }),
dailyList() { dailyList() {
return this.dailyHis.map(getItemData); return this.dailyHis.map(getItemData)
}, },
publishList() { publishList() {
return this.publishHis.map(getItemData); return this.publishHis.map(getItemData)
}, }
}, },
data() { data() {
return { return {
dailyHis: [], dailyHis: [],
publishHis: [], publishHis: [],
currentTab: 'daily', currentTab: 'daily',
visible: false, visible: false
}; }
}, },
watch: { watch: {
surveyId: { surveyId: {
@ -67,31 +70,32 @@ export default {
const [dailyHis, publishHis] = await Promise.all([ const [dailyHis, publishHis] = await Promise.all([
getSurveyHistory({ getSurveyHistory({
surveyId: this.surveyId, surveyId: this.surveyId,
historyType: 'dailyHis', historyType: 'dailyHis'
}), }),
getSurveyHistory({ getSurveyHistory({
surveyId: this.surveyId, surveyId: this.surveyId,
historyType: 'publishHis', historyType: 'publishHis'
}), })
]); ])
this.dailyHis = dailyHis.data || []; this.dailyHis = dailyHis.data || []
this.publishHis = publishHis.data || []; this.publishHis = publishHis.data || []
} }
}, }
}, }
}, },
methods: { methods: {
onShow() { onShow() {
this.visible = true; this.visible = true
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url('@/management/styles/edit-btn.scss'); @import url('@/management/styles/edit-btn.scss');
.custom-tab { .custom-tab {
width: 300px; width: 300px;
::v-deep .el-tabs__nav { :deep(.el-tabs__nav) {
width: 100%; width: 100%;
.el-tabs__item { .el-tabs__item {

View File

@ -0,0 +1,75 @@
<template>
<el-button type="primary" :loading="isPublishing" class="publish-btn" @click="onPublish">
发布
</el-button>
</template>
<script>
import { mapState } from 'vuex'
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { get as _get } from 'lodash-es'
import { publishSurvey, saveSurvey } from '@/management/api/survey'
import buildData from './buildData'
export default {
name: 'PublishPanel',
data() {
return {
isPublishing: false
}
},
computed: {
...mapState({
surveyId: (state) => _get(state, 'edit.surveyId')
})
},
methods: {
async onPublish() {
const saveData = buildData(this.$store.state.edit.schema)
if (!saveData.surveyId) {
ElMessage.error('未获取到问卷id')
return
}
if (this.isPublishing) {
return
}
try {
this.isPublishing = true
const saveRes = await saveSurvey(saveData)
if (saveRes.code !== 200) {
ElMessage.error(saveRes.errmsg || '问卷保存失败')
return
}
const publishRes = await publishSurvey({ surveyId: this.surveyId })
if (publishRes.code === 200) {
ElMessage.success('发布成功')
this.$store.dispatch('edit/getSchemaFromRemote')
this.$router.push({
name: 'publishResultPage'
})
} else {
ElMessage.error(`发布失败 ${publishRes.errmsg}`)
}
} catch (error) {
ElMessage.error(`发布失败`)
} finally {
this.isPublishing = false
}
}
}
}
</script>
<style lang="scss" scoped>
.publish-btn {
width: 100px;
font-size: 14px;
height: 36px;
line-height: 36px;
padding: 0;
}
</style>

View File

@ -7,115 +7,119 @@
<span class="sv-text"> <span class="sv-text">
{{ saveText }} {{ saveText }}
</span> </span>
<i class="icon el-icon-loading" v-if="autoSaveStatus === 'saving'"></i> <i-ep-loading class="icon" v-if="autoSaveStatus === 'saving'" />
<i <i-ep-check class="icon succeed" v-else-if="autoSaveStatus === 'succeed'" />
class="icon succeed el-icon-check"
v-else-if="autoSaveStatus === 'succeed'"
></i>
</div> </div>
</transition> </transition>
</div> </div>
</template> </template>
<script> <script>
import { saveSurvey } from '@/management/api/survey'; import { mapState } from 'vuex'
import buildData from './buildData'; import { get as _get } from 'lodash-es'
import { mapState } from 'vuex';
import { get as _get } from 'lodash-es'; import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import { saveSurvey } from '@/management/api/survey'
import buildData from './buildData'
export default { export default {
name: 'save', components: {},
name: 'SavePanel',
data() { data() {
return { return {
isSaving: false, isSaving: false,
isShowAutoSave: false, isShowAutoSave: false,
autoSaveStatus: 'succeed', autoSaveStatus: 'succeed'
}; }
}, },
computed: { computed: {
...mapState({ ...mapState({
schemaUpdateTime: (state) => _get(state, 'edit.schemaUpdateTime'), schemaUpdateTime: (state) => _get(state, 'edit.schemaUpdateTime')
}), }),
saveText() { saveText() {
const statusMap = { const statusMap = {
saving: '保存中', saving: '保存中',
succeed: '保存成功', succeed: '保存成功',
failed: '保存失败', failed: '保存失败'
}; }
return statusMap[this.autoSaveStatus]; return statusMap[this.autoSaveStatus]
}, }
}, },
watch: { watch: {
schemaUpdateTime() { schemaUpdateTime() {
this.triggerAutoSave(); this.triggerAutoSave()
}, }
}, },
methods: { methods: {
triggerAutoSave() { triggerAutoSave() {
if (this.autoSaveStatus === 'saving') { if (this.autoSaveStatus === 'saving') {
// //
setTimeout(() => { setTimeout(() => {
this.triggerAutoSave(); this.triggerAutoSave()
}, 1000); }, 1000)
} else { } else {
if (this.timer) { if (this.timer) {
clearTimeout(this.timer); clearTimeout(this.timer)
} }
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
this.autoSaveStatus = 'saving'; this.autoSaveStatus = 'saving'
this.isShowAutoSave = true; this.isShowAutoSave = true
this.$nextTick(() => { this.$nextTick(() => {
this.saveData() this.saveData()
.then((res) => { .then((res) => {
if (res.code === 200) { if (res.code === 200) {
this.autoSaveStatus = 'succeed'; this.autoSaveStatus = 'succeed'
} else { } else {
this.autoSaveStatus = 'failed'; this.autoSaveStatus = 'failed'
} }
setTimeout(() => { setTimeout(() => {
this.isShowAutoSave = false; this.isShowAutoSave = false
this.timer = null; this.timer = null
}, 300); }, 300)
}) })
.catch(() => { .catch(() => {
this.timer = null; this.timer = null
this.autoSaveStatus = 'failed'; this.autoSaveStatus = 'failed'
this.isShowAutoSave = true; this.isShowAutoSave = true
}); })
}); })
}, 2000); }, 2000)
} }
}, },
async saveData() { async saveData() {
const saveData = buildData(this.$store.state.edit.schema); const saveData = buildData(this.$store.state.edit.schema)
if (!saveData.surveyId) { if (!saveData.surveyId) {
this.$message.error('未获取到问卷id'); ElMessage.error('未获取到问卷id')
return null; return null
} }
const res = await saveSurvey(saveData); const res = await saveSurvey(saveData)
return res; return res
}, },
async onSave() { async onSave() {
if (this.isSaving) { if (this.isSaving) {
return; return
} }
this.isShowAutoSave = false; this.isShowAutoSave = false
try { try {
this.isSaving = true; this.isSaving = true
const res = await this.saveData(); const res = await this.saveData()
if (res.code === 200) { if (res.code === 200) {
this.$message.success('保存成功'); ElMessage.success('保存成功')
} else { } else {
this.$message.error(res.errmsg); ElMessage.error(res.errmsg)
} }
} catch (error) { } catch (error) {
this.$message.error('保存问卷失败'); ElMessage.error('保存问卷失败')
} finally { } finally {
this.isSaving = false; this.isSaving = false
} }
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url('@/management/styles/edit-btn.scss'); @import url('@/management/styles/edit-btn.scss');

View File

@ -1,22 +1,22 @@
import { pick as _pick, get as _get } from 'lodash-es'; import { pick as _pick, get as _get } from 'lodash-es'
// 生成需要保存到接口的数据 // 生成需要保存到接口的数据
export default function (schema) { export default function (schema) {
const surveyId = _get(schema, 'metaData._id'); const surveyId = _get(schema, 'metaData._id')
const configData = _pick(schema, [ const configData = _pick(schema, [
'bannerConf', 'bannerConf',
'baseConf', 'baseConf',
'bottomConf', 'bottomConf',
'skinConf', 'skinConf',
'submitConf', 'submitConf',
'questionDataList', 'questionDataList'
]); ])
configData.dataConf = { configData.dataConf = {
dataList: configData.questionDataList, dataList: configData.questionDataList
}; }
delete configData.questionDataList; delete configData.questionDataList
return { return {
surveyId, surveyId,
configData, configData
}; }
} }

View File

@ -1,72 +0,0 @@
<template>
<el-button
type="primary"
:loading="isPublishing"
class="publish-btn"
@click="onPublish"
>
发布
</el-button>
</template>
<script>
import { mapState } from 'vuex';
import { publishSurvey, saveSurvey } from '@/management/api/survey';
import buildData from './buildData';
import { get as _get } from 'lodash-es';
export default {
name: 'publish',
data() {
return {
isPublishing: false,
};
},
computed: {
...mapState({
surveyId: (state) => _get(state, 'edit.surveyId'),
}),
},
methods: {
async onPublish() {
const saveData = buildData(this.$store.state.edit.schema);
if (!saveData.surveyId) {
this.$message.error('未获取到问卷id');
return;
}
if (this.isPublishing) {
return;
}
try {
this.isPublishing = true;
const saveRes = await saveSurvey(saveData);
if (saveRes.code !== 200) {
this.$message.error(saveRes.errmsg || '问卷保存失败');
return;
}
const publishRes = await publishSurvey({ surveyId: this.surveyId });
if (publishRes.code === 200) {
this.$message.success('发布成功');
this.$store.dispatch('edit/getSchemaFromRemote');
this.$router.push({
name: 'publishResultPage',
});
} else {
this.$message.error(`发布失败 ${publishRes.errmsg}`);
}
} catch (error) {
this.$message.error(`发布失败`);
} finally {
this.isPublishing = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.publish-btn {
width: 100px;
font-size: 14px;
height: 36px;
line-height: 36px;
padding: 0;
}
</style>

View File

@ -4,16 +4,18 @@
<span>返回</span> <span>返回</span>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'back', name: 'BackPanel',
methods: { methods: {
onBack() { onBack() {
this.$router.go(-1); window.open('/survey', '_self')
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.back-btn { .back-btn {
height: 100%; height: 100%;

View File

@ -1,31 +1,34 @@
<template> <template>
<div class="content"> <div class="content">
<template v-for="btnItem in btnList"> <template v-for="btnItem in btnList" :key="btnItem.key">
<router-link <router-link
class="navbar-btn"
:key="btnItem.key"
:to="{ name: btnItem.router }" :to="{ name: btnItem.router }"
tag="div"
replace replace
v-slot="{ href, route, navigate, isActive, isExactActive }" v-slot="{ href, navigate, isActive, isExactActive }"
custom custom
> >
<div <div
:class="[ :class="[
(isActive && btnItem.key === 'skinsettings' ) || isExactActive ? 'router-link-exact-active' : '']" 'navbar-btn',
> (isActive && btnItem.key === 'skinsettings') || isExactActive
<i class="iconfont" :class="[btnItem.icon]"></i> ? 'router-link-exact-active'
<a :href="href" @click="navigate"><span>{{ btnItem.text }}</span></a> : ''
<!-- <span>{{ btnItem.text }}</span> --> ]"
</div> >
<i class="iconfont" :class="[btnItem.icon]"></i>
<a :href="href" @click="navigate"
><span>{{ btnItem.text }}</span></a
>
<!-- <span>{{ btnItem.text }}</span> -->
</div>
</router-link> </router-link>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'pageNav', name: 'NavPanel',
props: {}, props: {},
data() { data() {
return { return {
@ -35,27 +38,28 @@ export default {
text: '问卷编辑', text: '问卷编辑',
router: 'QuestionEditIndex', router: 'QuestionEditIndex',
key: 'edit', key: 'edit',
next: true, next: true
}, },
{ {
icon: 'icon-wenjuanshezhi', icon: 'icon-wenjuanshezhi',
text: '问卷设置', text: '问卷设置',
router: 'QuestionEditSetting', router: 'QuestionEditSetting',
key: 'settings', key: 'settings',
next: true, next: true
}, },
{ {
icon: 'icon-yangshishezhi', icon: 'icon-yangshishezhi',
text: '皮肤设置', text: '皮肤设置',
router: 'QuestionSkinSetting', router: 'QuestionSkinSetting',
key: 'skinsettings', key: 'skinsettings',
next: true, next: true
}, }
], ]
}; }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.content { .content {
display: flex; display: flex;
@ -67,7 +71,7 @@ export default {
color: #92949d; color: #92949d;
padding: 0 20px; padding: 0 20px;
cursor: pointer; cursor: pointer;
a{ a {
color: inherit; color: inherit;
} }
&.router-link-exact-active { &.router-link-exact-active {

View File

@ -3,17 +3,19 @@
{{ title }} {{ title }}
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'pageTitle', name: 'TitlePanel',
props: { props: {
title: { title: {
type: String, type: String,
default: '', default: ''
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.title { .title {
overflow: hidden; overflow: hidden;

View File

@ -1,43 +1,44 @@
<template> <template>
<el-tabs type="border-card" v-model="tabSelected" class="tab-box"> <el-tabs type="border-card" v-model="tabSelected" class="tab-box">
<el-tab-pane label="题型选择"> <el-tab-pane label="题型选择">
<type-list /> <TypeList />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="题目大纲"> <el-tab-pane label="题目大纲">
<catalog /> <QuestionCatalog />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</template> </template>
<script> <script>
import typeList from './components/typeList'; import TypeList from './components/TypeList.vue'
import catalog from './components/catalog.vue'; import QuestionCatalog from './components/QuestionCatalog.vue'
export default { export default {
name: 'EditLeftTabPanel', name: 'CatalogPanel',
data() { data() {
return { return {
tabSelected: '0', tabSelected: '0'
}; }
}, },
components: { components: {
typeList, TypeList,
catalog, QuestionCatalog
}, },
methods: {}, methods: {}
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.tab-box { .tab-box {
width: 300px; width: 300px;
height: 100%; height: 100%;
box-shadow: none; box-shadow: none;
border: none; border: none;
overflow-y: auto; overflow-y: auto;
::v-deep .el-tabs__nav { :deep(.el-tabs__nav) {
width: 100%; width: 100%;
} }
::v-deep .el-tabs__item { :deep(.el-tabs__item) {
width: 50%; width: 50%;
text-align: center; text-align: center;
} }

View File

@ -2,13 +2,13 @@
<div class="main-operation" @click="onMainClick" ref="mainOperation"> <div class="main-operation" @click="onMainClick" ref="mainOperation">
<div class="operation-wrapper" ref="operationWrapper"> <div class="operation-wrapper" ref="operationWrapper">
<div class="box content" ref="box"> <div class="box content" ref="box">
<mainTitle <MainTitle
:bannerConf="bannerConf" :bannerConf="bannerConf"
:is-selected="currentEditOne === 'mainTitle'" :is-selected="currentEditOne === 'mainTitle'"
@select="onSelectEditOne('mainTitle')" @select="onSelectEditOne('mainTitle')"
@change="handleChange" @change="handleChange"
/> />
<materialGroup <MaterialGroup
:current-edit-one="parseInt(currentEditOne)" :current-edit-one="parseInt(currentEditOne)"
:questionDataList="questionDataList" :questionDataList="questionDataList"
@select="onSelectEditOne" @select="onSelectEditOne"
@ -16,7 +16,7 @@
@changeSeq="onQuestionOperation" @changeSeq="onQuestionOperation"
ref="materialGroup" ref="materialGroup"
/> />
<submit <SubmitButton
:submit-conf="submitConf" :submit-conf="submitConf"
:skin-conf="skinConf" :skin-conf="skinConf"
:is-selected="currentEditOne === 'submit'" :is-selected="currentEditOne === 'submit'"
@ -28,25 +28,23 @@
</template> </template>
<script> <script>
import materialGroup from '@/management/pages/edit/components/materialGroup.vue'; import MaterialGroup from '@/management/pages/edit/components/MaterialGroup.vue'
import mainTitle from '@/management/pages/edit/components/mainTitle.vue'; import MainTitle from '@/management/pages/edit/components/MainTitle.vue'
import submit from '@/management/pages/edit/components/submit.vue'; import SubmitButton from '@/management/pages/edit/components/SubmitButton.vue'
import logo from '@/management/pages/edit/components/logo.vue'; import { mapState, mapGetters } from 'vuex'
import { mapState, mapGetters } from 'vuex'; import { get as _get } from 'lodash-es'
import { get as _get } from 'lodash-es';
export default { export default {
name: 'mainOperation', name: 'PreviewPanel',
components: { components: {
mainTitle, MainTitle,
submit, SubmitButton,
logo, MaterialGroup
materialGroup,
}, },
data() { data() {
return { return {
isAnimating: false, isAnimating: false
}; }
}, },
computed: { computed: {
...mapState({ ...mapState({
@ -55,99 +53,91 @@ export default {
skinConf: (state) => _get(state, 'edit.schema.skinConf'), skinConf: (state) => _get(state, 'edit.schema.skinConf'),
bottomConf: (state) => _get(state, 'edit.schema.bottomConf'), bottomConf: (state) => _get(state, 'edit.schema.bottomConf'),
questionDataList: (state) => _get(state, 'edit.schema.questionDataList'), questionDataList: (state) => _get(state, 'edit.schema.questionDataList'),
currentEditOne: (state) => _get(state, 'edit.currentEditOne'), currentEditOne: (state) => _get(state, 'edit.currentEditOne')
}), }),
...mapGetters({ ...mapGetters({
currentEditKey: 'edit/currentEditKey', currentEditKey: 'edit/currentEditKey'
}), }),
autoScrollData() { autoScrollData() {
return { return {
currentEditOne: this.currentEditOne, currentEditOne: this.currentEditOne,
len: this.questionDataList.length, len: this.questionDataList.length
}; }
}, }
}, },
watch: { watch: {
skinConf: { skinConf: {
handler (skinConf) { handler(skinConf) {
const { themeConf, backgroundConf, contentConf} = skinConf const { themeConf, backgroundConf, contentConf } = skinConf
const root = document.documentElement; const root = document.documentElement
if(themeConf?.color) { if (themeConf?.color) {
root.style.setProperty('--primary-color', themeConf?.color); // root.style.setProperty('--primary-color', themeConf?.color) //
} }
if(backgroundConf?.color) { if (backgroundConf?.color) {
root.style.setProperty('--primary-background-color', backgroundConf?.color); // root.style.setProperty('--primary-background-color', backgroundConf?.color) //
} }
if(contentConf?.opacity) { if (contentConf?.opacity) {
root.style.setProperty('--opacity', contentConf?.opacity/100); // root.style.setProperty('--opacity', contentConf?.opacity / 100) //
} }
}, },
immediate: true, // immediate: true, //
deep: true deep: true
}, },
autoScrollData(newVal) { autoScrollData(newVal) {
const { currentEditOne } = newVal; const { currentEditOne } = newVal
if (typeof currentEditOne === 'number') { if (typeof currentEditOne === 'number') {
setTimeout(() => { setTimeout(() => {
// if (this.isAnimating) { const field = this.questionDataList?.[currentEditOne]?.field
// return;
// }
const field = this.questionDataList?.[currentEditOne]?.field;
if (field) { if (field) {
const questionComp = const questionModule = this.$refs.materialGroup.getQuestionRefByField(field)
this.$refs.materialGroup.getQuestionRefByField(field); if (questionModule && questionModule.$el) {
if (questionComp && questionComp.$el) { questionModule.$el.scrollIntoView({
questionComp.$el.scrollIntoView({ behavior: 'smooth'
behavior: 'smooth', })
});
// this.isAnimating = true;
// const maxScrollTop = this.$refs.box.clientHeight - this.$refs.operationWrapper.clientHeight
// const targetVal = Math.min(questionComp.$el.offsetTop - this.$refs.operationWrapper.clientHeight / 2, maxScrollTop)
// this.animate(this.$refs.operationWrapper, 'scrollTop', targetVal)
} }
} }
}, 0); }, 0)
} }
}, }
}, },
methods: { methods: {
animate(dom, property, targetValue) { animate(dom, property, targetValue) {
const origin = dom[property]; const origin = dom[property]
const subVal = targetValue - origin; const subVal = targetValue - origin
const flag = subVal < 0 ? -1 : 1; const flag = subVal < 0 ? -1 : 1
const step = flag * 50; const step = flag * 50
const totalCount = Math.floor(subVal / step) + 1; const totalCount = Math.floor(subVal / step) + 1
let runCount = 0; let runCount = 0
const run = () => { const run = () => {
dom[property] += step; dom[property] += step
runCount++; runCount++
if (runCount < totalCount) { if (runCount < totalCount) {
requestAnimationFrame(run); requestAnimationFrame(run)
} else { } else {
this.isAnimating = false; this.isAnimating = false
} }
}; }
requestAnimationFrame(run); requestAnimationFrame(run)
}, },
async onSelectEditOne(currentEditOne) { async onSelectEditOne(currentEditOne) {
this.$store.commit('edit/setCurrentEditOne', currentEditOne); this.$store.commit('edit/setCurrentEditOne', currentEditOne)
}, },
handleChange(data) { handleChange(data) {
if (this.currentEditOne === null) { if (this.currentEditOne === null) {
return; return
} }
const { key, value } = data; const { key, value } = data
const resultKey = `${this.currentEditKey}.${key}`; const resultKey = `${this.currentEditKey}.${key}`
this.$store.dispatch('edit/changeSchema', { key: resultKey, value }); this.$store.dispatch('edit/changeSchema', { key: resultKey, value })
}, },
onMainClick(e) { onMainClick(e) {
if (e.target === this.$refs.mainOperation) { if (e.target === this.$refs.mainOperation) {
this.$store.commit('edit/setCurrentEditOne', null); this.$store.commit('edit/setCurrentEditOne', null)
} }
}, },
onQuestionOperation(data) { onQuestionOperation(data) {
@ -155,21 +145,21 @@ export default {
case 'move': case 'move':
this.$store.dispatch('edit/moveQuestion', { this.$store.dispatch('edit/moveQuestion', {
index: data.index, index: data.index,
range: data.range, range: data.range
}); })
break; break
case 'delete': case 'delete':
this.$store.dispatch('edit/deleteQuestion', { index: data.index }); this.$store.dispatch('edit/deleteQuestion', { index: data.index })
break; break
case 'copy': case 'copy':
this.$store.dispatch('edit/copyQuestion', { index: data.index }); this.$store.dispatch('edit/copyQuestion', { index: data.index })
break; break
default: default:
break; break
} }
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -7,7 +7,7 @@
</div> </div>
<template v-else> <template v-else>
<div class="setter-title">{{ currentEditMeta?.title || '' }}</div> <div class="setter-title">{{ currentEditMeta?.title || '' }}</div>
<setterField <SetterField
class="question-config-form" class="question-config-form"
:form-config-list="formConfigList" :form-config-list="formConfigList"
:module-config="moduleConfig" :module-config="moduleConfig"
@ -18,40 +18,41 @@
</template> </template>
<script> <script>
import setterField from '@/management/pages/edit/components/setterField.vue'; import SetterField from '@/management/pages/edit/components/SetterField.vue'
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex'
export default { export default {
name: 'setterWrapper', name: 'SetterPanel',
data() { data() {
return { return {
tabSelected: '0', tabSelected: '0'
}; }
}, },
computed: { computed: {
currentEditOne() { currentEditOne() {
return this.$store.state?.edit?.currentEditOne; return this.$store.state?.edit?.currentEditOne
}, },
...mapGetters({ ...mapGetters({
formConfigList: 'edit/formConfigList', formConfigList: 'edit/formConfigList',
moduleConfig: 'edit/moduleConfig', moduleConfig: 'edit/moduleConfig',
currentEditKey: 'edit/currentEditKey', currentEditKey: 'edit/currentEditKey',
currentEditMeta: 'edit/currentEditMeta', currentEditMeta: 'edit/currentEditMeta'
}), })
}, },
components: { components: {
setterField, SetterField
}, },
methods: { methods: {
onFormChange(data) { onFormChange(data) {
const { key, value } = data; const { key, value } = data
const resultKey = `${this.currentEditKey}.${key}`; const resultKey = `${this.currentEditKey}.${key}`
this.$store.dispatch('edit/changeSchema', { key: resultKey, value }); this.$store.dispatch('edit/changeSchema', { key: resultKey, value })
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.setter-wrapper { .setter-wrapper {
width: 360px; width: 360px;
height: 100%; height: 100%;
@ -88,6 +89,6 @@ export default {
} }
.question-config-form { .question-config-form {
padding: 30px 20px 50px 20px!important; padding: 30px 20px 50px 20px;
} }
</style> </style>

View File

@ -10,34 +10,35 @@
<script> <script>
export default { export default {
name: 'QuestionCatalogItem', name: 'CatalogItem',
data() { data() {
return {}; return {}
}, },
computed: {}, computed: {},
props: { props: {
title: { title: {
type: String, type: String,
default: '', default: ''
}, },
indexNumber: { indexNumber: {
type: [String, Number], type: [String, Number],
default: '', default: ''
}, },
showIndex: { showIndex: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}, },
components: {}, components: {},
methods: { methods: {
onSelect() { onSelect() {
this.$emit('select'); this.$emit('select')
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.question-catalog-item { .question-catalog-item {
position: relative; position: relative;
border-top: 1px solid #ebebeb; border-top: 1px solid #ebebeb;

View File

@ -1,12 +1,17 @@
<template> <template>
<div class="question-catalog-wrapper"> <div class="question-catalog-wrapper">
<draggable :list="renderData" :options="dragOptions" @end="onDragEnd"> <draggable
<template v-for="(catalogItem, index) in renderData"> :list="renderData"
<catalogItem @end="onDragEnd"
:key="catalogItem.field" itemKey="field"
:title="catalogItem.title" handle=".draggHandle"
:indexNumber="catalogItem.indexNumber" host-class="catalog-item-ghost"
:showIndex="catalogItem.showIndex" >
<template #item="{ element, index }">
<CatalogItem
:title="element.title"
:indexNumber="element.indexNumber"
:showIndex="element.showIndex"
@select="onSelect(index)" @select="onSelect(index)"
/> />
</template> </template>
@ -15,48 +20,43 @@
</template> </template>
<script> <script>
import draggable from 'vuedraggable'; import draggable from 'vuedraggable'
import catalogItem from './catalogItem'; import CatalogItem from './CatalogItem.vue'
import { filterQuestionPreviewData } from '@/management/utils/index'; import { filterQuestionPreviewData } from '@/management/utils/index'
export default { export default {
name: 'QuestionCatalog', name: 'QuestionCatalog',
data() { data() {
return { return {}
dragOptions: {
handle: '.draggHandle',
ghostClass: 'catalog-item-ghost',
dragClass: 'catalog-item-dragging',
},
};
}, },
computed: { computed: {
questionDataList() { questionDataList() {
return this.$store.state.edit.schema.questionDataList; return this.$store.state.edit.schema.questionDataList
}, },
renderData() { renderData() {
return filterQuestionPreviewData(this.questionDataList) || []; return filterQuestionPreviewData(this.questionDataList) || []
}, }
}, },
components: { components: {
draggable, draggable,
catalogItem, CatalogItem
}, },
methods: { methods: {
onDragEnd(data) { onDragEnd(data) {
const { newIndex, oldIndex } = data; const { newIndex, oldIndex } = data
this.$store.dispatch('edit/moveQuestion', { this.$store.dispatch('edit/moveQuestion', {
index: oldIndex, index: oldIndex,
range: newIndex - oldIndex, range: newIndex - oldIndex
}); })
}, },
onSelect(index) { onSelect(index) {
this.$store.commit('edit/setCurrentEditOne', index); this.$store.commit('edit/setCurrentEditOne', index)
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.question-catalog-wrapper { .question-catalog-wrapper {
padding-bottom: 400px; // padding-bottom: 400px; //
.catelog-first-page { .catelog-first-page {

View File

@ -0,0 +1,131 @@
<template>
<el-collapse class="question-type-wrapper" v-model="activeNames">
<el-collapse-item
v-for="(item, index) of questionMenuConfig"
:title="item.title"
:name="index"
:key="index"
>
<div class="questiontype-list">
<el-popover
v-for="(item, index) in item.questionList"
:key="item.type"
placement="right"
trigger="hover"
:popper-class="'qtype-popper-' + (index % 3)"
:popper-style="{ width: '369px' }"
>
<img :src="item.snapshot" width="345px" />
<template #reference>
<div :key="item.type" class="qtopic-item" @click="onQuestionType({ type: item.type })">
<i class="iconfont" :class="['icon-' + item.icon]"></i>
<p class="text">{{ item.title }}</p>
</div>
</template>
</el-popover>
</div>
</el-collapse-item>
</el-collapse>
</template>
<script setup>
import questionLoader from '@/materials/questions/questionLoader'
import questionMenuConfig, { questionTypeList } from '@/management/config/questionMenuConfig'
import { getQuestionByType } from '@/management/utils/index'
import { useStore } from 'vuex'
import { get as _get } from 'lodash-es'
import { computed, ref } from 'vue'
const activeNames = ref([0, 1])
const store = useStore()
const questionDataList = computed(() => _get(store, 'state.edit.schema.questionDataList'))
questionLoader.init({
typeList: questionTypeList.map((item) => item.type)
})
const onQuestionType = ({ type }) => {
const fields = questionDataList.value.map((item) => item.field)
const currentEditOne = _get(store, 'state.edit.currentEditOne')
const index =
typeof currentEditOne === 'number' ? currentEditOne + 1 : questionDataList.value.length
const newQuestion = getQuestionByType(type, fields)
newQuestion.title = newQuestion.title = `标题${index + 1}`
if (type === 'vote') {
newQuestion.innerType = 'radio'
}
store.dispatch('edit/addQuestion', { question: newQuestion, index })
store.commit('edit/setCurrentEditOne', index)
}
</script>
<style lang="scss" scoped>
.question-type-wrapper {
padding: 0 20px;
border: none;
:deep(.el-collapse-item__header) {
font-size: 16px;
font-weight: bold;
color: #4a4c5b;
}
}
.questiontype-list {
// height: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
// padding-bottom: 25px;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
border-top: none;
&::-webkit-scrollbar {
display: none;
}
.qtopic-item {
height: 77px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 12px;
border: 1px solid $disable-color;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: $primary-color-light;
border: 1px solid $primary-color;
}
.text {
font-size: 12px;
}
}
.iconfont::before {
font-size: 21px;
color: $font-color-title;
}
}
</style>
<style lang="scss">
.qtype-popper-0 {
transform: translateX(183px);
}
.qtype-popper-1 {
transform: translateX(106px);
}
.qtype-popper-2 {
transform: translateX(30px);
}
</style>

View File

@ -1,162 +0,0 @@
<template>
<el-collapse class="question-type-wrapper" v-model="activeNames">
<el-collapse-item
v-for="(item, index) of questionMenuConfig"
:title="item.title"
:name="index"
:key="index"
>
<draggable
class="questiontype-list item-wrapper"
:options="{
element: 'li',
sort: false,
group: { name: 'previewList', pull: 'clone', put: false },
}"
>
<el-popover
v-for="(item, index) in item.questionList"
:key="item.type"
placement="right"
trigger="hover"
:popper-class="'qtype-popper-' + (index % 3)"
>
<img :src="item.snapshot" width="345px" />
<div
slot="reference"
:key="item.type"
class="qtopic-item"
@click="onQuestionType({ type: item.type })"
>
<i class="iconfont" :class="['icon-' + item.icon]"></i>
<p class="text">{{ item.title }}</p>
</div>
</el-popover>
</draggable>
</el-collapse-item>
</el-collapse>
</template>
<script>
import questionLoader from '@/materials/questions/questionLoader';
import draggable from 'vuedraggable';
import questionMenuConfig, {
questionTypeList,
} from '@/management/config/questionMenuConfig';
import { getQuestionByType } from '@/management/utils/index';
import { mapState, mapActions } from 'vuex';
import { get as _get } from 'lodash-es';
export default {
name: 'QuestionTypeList',
components: {
draggable,
},
data() {
return {
activeNames: [0, 1],
questionMenuConfig,
};
},
computed: {
...mapState({
questionDataList: (state) => _get(state, 'edit.schema.questionDataList'),
currentEditOne: (state) => _get(state, 'edit.currentEditOne'),
}),
},
async created() {
await questionLoader.init({
typeList: questionTypeList.map((item) => item.type),
});
},
methods: {
...mapActions({
addQuestion: 'edit/addQuestion',
}),
onQuestionType({ type }) {
const questionDataList = this.questionDataList || [];
const fields = questionDataList.map((item) => item.field);
const currentEditOne = this.currentEditOne;
const index =
typeof currentEditOne === 'number'
? currentEditOne + 1
: questionDataList.length;
const newQuestion = getQuestionByType(type, fields);
newQuestion.title = newQuestion.title = `标题${index + 1}`;
if (type === 'vote') {
newQuestion.innerType = 'radio';
}
this.addQuestion({ question: newQuestion, index });
this.$store.commit('edit/setCurrentEditOne', index);
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.question-type-wrapper {
padding: 0 20px;
border: none;
::v-deep .el-collapse-item__header {
font-size: 16px;
font-weight: bold;
color: #4a4c5b;
}
}
.questiontype-list {
// height: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
// padding-bottom: 25px;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
border-top: none;
&::-webkit-scrollbar {
display: none;
}
.qtopic-item {
height: 77px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 12px;
border: 1px solid $disable-color;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: $primary-color-light;
border: 1px solid $primary-color;
}
.text {
font-size: 12px;
}
}
.iconfont::before {
font-size: 21px;
color: $font-color-title;
}
}
</style>
<style lang="scss" rel="stylesheet/scss">
.qtype-popper-0 {
transform: translateX(183px);
}
.qtype-popper-1 {
transform: translateX(106px);
}
.qtype-popper-2 {
transform: translateX(30px);
}
</style>

View File

@ -10,22 +10,19 @@
</div> </div>
<el-form <el-form
class="question-config-form" class="question-config-form"
size="small"
label-position="left" label-position="left"
label-width="200px" label-width="200px"
@submit.native.prevent @submit.prevent
> >
<template v-for="(item, index) in form.formList"> <template v-for="(item, index) in form.formList">
<FormItem <FormItem
v-if="item.type && !item.hidden && Boolean(register[item.type])"
:key="index" :key="index"
v-if="
item.type && !item.hidden && Boolean(registerd[item.type])
"
:form-config="item" :form-config="item"
:style="item.style" :style="item.style"
> >
<Component <Component
v-if="Boolean(registerd[item.type])" v-if="Boolean(register[item.type])"
:is="item.type" :is="item.type"
:module-config="form.dataConfig" :module-config="form.dataConfig"
:form-config="item" :form-config="item"
@ -39,106 +36,95 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import baseConfig from './config/baseConfig'; import baseConfig from './config/baseConfig'
import baseFormConfig from './config/baseFormConfig'; import baseFormConfig from './config/baseFormConfig'
import FormItem from '@/materials/setters/widgets/FormItem.vue'; import FormItem from '@/materials/setters/widgets/FormItem.vue'
import setterLoader from '@/materials/setters/setterLoader'; import setterLoader from '@/materials/setters/setterLoader'
import { import { cloneDeep as _cloneDeep, isArray as _isArray, get as _get } from 'lodash-es'
cloneDeep as _cloneDeep,
isArray as _isArray,
get as _get,
} from 'lodash-es';
export default { export default {
name: 'QuestionConfig', name: 'SettingPanel',
components: { components: {
FormItem, FormItem
}, },
data() { data() {
return { return {
formConfigList: [], formConfigList: [],
registerd: {}, register: {}
}; }
}, },
methods: { methods: {
onFormChange(data) { onFormChange(data) {
this.$store.dispatch('edit/changeSchema', { this.$store.dispatch('edit/changeSchema', {
key: data.key, key: data.key,
value: data.value, value: data.value
}); })
}, }
}, },
computed: { computed: {
allSetters() { allSetters() {
const formList = this.formConfigList.map((item) => item.formList).flat(); const formList = this.formConfigList.map((item) => item.formList).flat()
const typeList = formList.map((item) => ({ const typeList = formList.map((item) => ({
type: item.type, type: item.type,
path: item.path || item.type, path: item.path || item.type
})); }))
return typeList;
return typeList
}, },
renderData() { renderData() {
// todo: 1formConfigvalue2dataConfig // todo: 1formConfigvalue2dataConfig
const formConfigList = _cloneDeep(this.formConfigList); const formConfigList = _cloneDeep(this.formConfigList)
return formConfigList.map((form) => { return formConfigList.map((form) => {
const dataConfig = {}; const dataConfig = {}
for (const formItem of form.formList) { for (const formItem of form.formList) {
const formKey = formItem.key ? formItem.key : formItem.keys; const formKey = formItem.key ? formItem.key : formItem.keys
let formValue; let formValue
if (_isArray(formKey)) { if (_isArray(formKey)) {
formValue = []; formValue = []
for (const key of formKey) { for (const key of formKey) {
const val = _get( const val = _get(this.$store.state.edit.schema, key, formItem.value)
this.$store.state.edit.schema, formValue.push(val)
key, dataConfig[key] = val
formItem.value
);
formValue.push(val);
dataConfig[key] = val;
} }
} else { } else {
formValue = _get( formValue = _get(this.$store.state.edit.schema, formKey, formItem.value)
this.$store.state.edit.schema, dataConfig[formKey] = formValue
formKey,
formItem.value
);
dataConfig[formKey] = formValue;
} }
formItem.value = formValue; formItem.value = formValue
} }
form.dataConfig = dataConfig; form.dataConfig = dataConfig
return form; return form
}); })
}, }
}, },
async created() { async created() {
this.formConfigList = baseConfig.map((item) => { this.formConfigList = baseConfig.map((item) => {
return { return {
...item, ...item,
formList: item.formList formList: item.formList.map((key) => baseFormConfig[key]).filter((config) => !!config)
.map((key) => baseFormConfig[key]) }
.filter((config) => !!config), })
};
});
const comps = await setterLoader.loadComponents(this.allSetters); const comps = await setterLoader.loadComponents(this.allSetters)
for (const comp of comps) { for (const comp of comps) {
if (!comp) { if (!comp) {
continue; continue
} }
const { type, component, err } = comp; const { type, component, err } = comp
if (!err) { if (!err) {
const componentName = component.name; const componentName = component.name
this.$options.components[componentName] = component; this.$options.components[componentName] = component
this.$set(this.registerd, type, componentName); this.register[type] = componentName
} }
} }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.question-config { .question-config {
width: 100%; width: 100%;
min-width: 1080px; min-width: 1080px;
@ -177,7 +163,7 @@ export default {
&:after { &:after {
position: absolute; position: absolute;
left: 0; left: 0;
top: 41px; top: 42px;
width: 100%; width: 100%;
height: 3px; height: 3px;
background-color: $primary-color; background-color: $primary-color;
@ -191,7 +177,7 @@ export default {
padding-top: 15px; padding-top: 15px;
padding-right: 1rem; padding-right: 1rem;
::v-deep .star-form.star-form_horizon .star-form-label { :deep(.star-form.star-form_horizon .star-form-label) {
display: inline-block; display: inline-block;
width: 3.4rem; width: 3.4rem;
text-align: left; text-align: left;

View File

@ -20,13 +20,14 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { get as _get } from 'lodash-es'; import { get as _get } from 'lodash-es'
export default { export default {
name: 'banner', name: 'BannerContent',
data() { data() {
return {}; return {}
}, },
props: { props: {
bannerConf: { bannerConf: {
@ -34,23 +35,24 @@ export default {
default: () => {} default: () => {}
}, },
isSelected: { isSelected: {
type: Boolean, type: Boolean
}, }
}, },
computed: { computed: {
bgImage() { bgImage() {
return _get(this.bannerConf, 'bannerConfig.bgImage', ''); return _get(this.bannerConf, 'bannerConfig.bgImage', '')
}, }
}, },
methods: { methods: {
handleClick() { handleClick() {
this.$emit('select'); this.$emit('select')
}, }
}, },
components: {}, components: {}
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.banner-preview { .banner-preview {
width: 100%; width: 100%;
} }
@ -61,6 +63,7 @@ export default {
.banner { .banner {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: center;
img { img {
width: 100%; width: 100%;
@ -107,7 +110,7 @@ export default {
background-color: #f6f7f9; background-color: #f6f7f9;
box-shadow: 0 0 5px #dedede; box-shadow: 0 0 5px #dedede;
::v-deep .w-e-text-container { :deep(.w-e-text-container) {
background-color: #f6f7f9; background-color: #f6f7f9;
} }
} }

View File

@ -4,25 +4,25 @@
<p class="title-msg" v-safe-html="resultText"></p> <p class="title-msg" v-safe-html="resultText"></p>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'OverTime', name: 'OverTime',
props: { props: {
moduleConfig: { moduleConfig: {
type: Object, type: Object,
required: true, required: true
}, }
}, },
computed: { computed: {
resultText() { resultText() {
return ( return this.moduleConfig?.submitConf?.msgContent?.msg_9001 || '问卷已过期'
this.moduleConfig?.submitConf?.msgContent?.msg_9001 || '问卷已过期' }
); }
}, }
},
};
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.over-time { .over-time {
text-align: center; text-align: center;
margin-bottom: 5.5rem; margin-bottom: 5.5rem;

View File

@ -9,23 +9,25 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'Success', name: 'SuccessContent',
props: { props: {
moduleConfig: { moduleConfig: {
type: Object, type: Object,
required: true, required: true
}, }
}, },
computed: { computed: {
successText() { successText() {
return this.moduleConfig?.submitConf?.msgContent?.msg_200 || ''; return this.moduleConfig?.submitConf?.msgContent?.msg_200 || ''
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
/*成功页面跳转全屏展示浮层*/ /*成功页面跳转全屏展示浮层*/
.suc-page { .suc-page {
padding: 0; padding: 0;
@ -53,7 +55,7 @@ export default {
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
font-size: 0.36rem; font-size: 0.36rem;
color: #666; color: #666;
::v-deep * { :deep(*) {
font-size: 0.36rem; font-size: 0.36rem;
} }
} }

View File

@ -2,11 +2,11 @@ export default [
{ {
title: '时间配置', title: '时间配置',
key: 'timeConfig', key: 'timeConfig',
formList: ['base_effectTime', 'limit_answerTime'], formList: ['base_effectTime', 'limit_answerTime']
}, },
{ {
title: '提交限制', title: '提交限制',
key: 'limitConfig', key: 'limitConfig',
formList: ['limit_tLimit'], formList: ['limit_tLimit']
}, }
]; ]

View File

@ -4,79 +4,22 @@ export default {
keys: ['baseConf.begTime', 'baseConf.endTime'], keys: ['baseConf.begTime', 'baseConf.endTime'],
label: '答题有效期', label: '答题有效期',
type: 'QuestionTime', type: 'QuestionTime',
placeholder: 'yyyy-MM-dd hh:mm:ss', placeholder: 'yyyy-MM-dd hh:mm:ss'
// direction: 'horizon',
}, },
// base_showVote: {
// key: 'baseConf.showVoteProcess',
// label: '投票配置',
// type: 'Select',
// direction: 'horizon',
// tip: '是否实时展示投票进度',
// placement: 'top',
// options: [
// {
// label: '实时展示投票进度',
// value: 'allow',
// },
// {
// label: '提交后才可以查看进度',
// value: 'notallow',
// },
// {
// label: '不展示投票进度',
// value: 'never',
// },
// ],
// },
// base_shortestTime: {
// key: 'baseConf.shortestTime',
// label: '最短答题时长(分钟)',
// type: 'InputNumber',
// direction: 'horizon',
// tip: '问卷仅可在所设置的时间之后才能进行提交0为无限制',
// tipShow: true,
// placement: 'top',
// },
limit_tLimit: { limit_tLimit: {
key: 'baseConf.tLimit', key: 'baseConf.tLimit',
label: '问卷回收总数', label: '问卷回收总数',
type: 'InputNumber', type: 'InputNumber',
// direction: 'horizon',
tip: '0为无限制此功能用于限制该问卷总提交的数据量。当数据量达到限额时该问卷将不能继续提交', tip: '0为无限制此功能用于限制该问卷总提交的数据量。当数据量达到限额时该问卷将不能继续提交',
tipShow: true, tipShow: true,
placement: 'top', placement: 'top',
min: 0, min: 0
}, },
limit_answerTime: { limit_answerTime: {
keys: ['baseConf.answerBegTime', 'baseConf.answerEndTime'], keys: ['baseConf.answerBegTime', 'baseConf.answerEndTime'],
label: '答题时段', label: '答题时段',
tip: '问卷仅在指定时间段内可填写', tip: '问卷仅在指定时间段内可填写',
type: 'QuestionTimeHour', type: 'QuestionTimeHour',
// direction: 'horizon', placement: 'top'
placement: 'top', }
}, }
// skin_skinColor: {
// key: 'skinConf.skinColor',
// label: '页面主题颜色',
// type: 'Select',
// direction: 'horizon',
// options: [
// {
// label: '橘色主题',
// value: '#ff8a01',
// },
// {
// label: '深灰蓝主题',
// value: '#4a4c5b',
// },
// ],
// },
// skin_inputBgColor: {
// key: 'skinConf.inputBgColor',
// label: '输入框底色',
// type: 'ColorInput',
// direction: 'horizon',
// maxlength: 6,
// },
};

View File

@ -7,9 +7,9 @@ export default {
placeholder: '提交成功', placeholder: '提交成功',
value: '提交成功', value: '提交成功',
labelStyle: { labelStyle: {
'font-weight': 'bold', 'font-weight': 'bold'
}, }
}, }
], ],
OverTime: [ OverTime: [
{ {
@ -19,8 +19,8 @@ export default {
placeholder: '问卷已过期', placeholder: '问卷已过期',
value: '问卷已过期', value: '问卷已过期',
labelStyle: { labelStyle: {
'font-weight': 'bold', 'font-weight': 'bold'
}, }
}, }
], ]
}; }

View File

@ -1,4 +1,4 @@
export const EDIT_STATUS_MAP = { export const EDIT_STATUS_MAP = {
SUCCESS: 'Success', SUCCESS: 'Success',
OVERTIME: 'OverTime', OVERTIME: 'OverTime'
}; }

View File

@ -16,40 +16,42 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapMutations } from 'vuex'; import { mapMutations } from 'vuex'
import { EDIT_STATUS_MAP } from '../enum'; import { EDIT_STATUS_MAP } from '../enum'
export default { export default {
name: 'resultConfigList', name: 'CatalogPanel',
data() { data() {
return { return {
statusList: [ statusList: [
{ {
type: EDIT_STATUS_MAP.SUCCESS, type: EDIT_STATUS_MAP.SUCCESS,
title: '提交成功', title: '提交成功',
previewImg: '/imgs/icons/success.webp', previewImg: '/imgs/icons/success.webp'
}, },
{ {
type: EDIT_STATUS_MAP.OVERTIME, type: EDIT_STATUS_MAP.OVERTIME,
title: '问卷过期', title: '问卷过期',
previewImg: '/imgs/icons/overtime.webp', previewImg: '/imgs/icons/overtime.webp'
}, }
], ]
}; }
}, },
computed: {}, computed: {},
methods: { methods: {
...mapMutations({ ...mapMutations({
changeStatusPreview: 'edit/changeStatusPreview', changeStatusPreview: 'edit/changeStatusPreview'
}), }),
filterDisabledStatus(data) { filterDisabledStatus(data) {
this.changeStatusPreview(data); this.changeStatusPreview(data)
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.tab-box { .tab-box {
width: 300px; width: 300px;
height: 100%; height: 100%;

View File

@ -2,46 +2,44 @@
<div class="result-config-preview"> <div class="result-config-preview">
<div class="result-page-wrap"> <div class="result-page-wrap">
<div class="result-page"> <div class="result-page">
<component <component :is="currentEditStatus" :key="currentEditStatus" :module-config="moduleConfig" />
:is="currentEditStatus"
:key="currentEditStatus"
:module-config="moduleConfig"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex'
import success from '../components/success'; import SuccessContent from '../components/SuccessContent.vue'
import overTime from '../components/overTime'; import OverTime from '../components/OverTime.vue'
import { EDIT_STATUS_MAP } from '../enum'; import { EDIT_STATUS_MAP } from '../enum'
import { get as _get } from 'lodash-es'; import { get as _get } from 'lodash-es'
export default { export default {
name: 'ResultConfigPreivew', name: 'PreviewPanel',
props: {}, props: {},
data() { data() {
return {}; return {}
}, },
computed: { computed: {
...mapState({ ...mapState({
currentEditStatus: (state) => state.edit.currentEditStatus, currentEditStatus: (state) => state.edit.currentEditStatus,
submitConf: (state) => _get(state, 'edit.schema.submitConf'), submitConf: (state) => _get(state, 'edit.schema.submitConf')
}), }),
moduleConfig() { moduleConfig() {
return { return {
submitConf: this.submitConf, submitConf: this.submitConf
}; }
}, }
}, },
components: { components: {
[EDIT_STATUS_MAP.SUCCESS]: success, [EDIT_STATUS_MAP.SUCCESS]: SuccessContent,
[EDIT_STATUS_MAP.OVERTIME]: overTime, [EDIT_STATUS_MAP.OVERTIME]: OverTime
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.result-config-preview { .result-config-preview {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -62,8 +60,8 @@ export default {
overflow-y: auto; overflow-y: auto;
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
padding: 0 0.3rem; padding: 0 0.3rem;
.result-page{ .result-page {
background: rgba(255, 255, 255, var(--opacity)); background: rgba(255, 255, 255, var(--opacity));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;

View File

@ -3,21 +3,15 @@
<div class="setter-title"> <div class="setter-title">
{{ currentEditText }} {{ currentEditText }}
</div> </div>
<el-form <el-form class="question-config-form" label-position="top" @submit.prevent>
class="question-config-form" <template v-for="(item, index) in formFieldData" :key="index">
size="small"
label-position="top"
@submit.native.prevent
>
<template v-for="(item, index) in formFieldData">
<FormItem <FormItem
:key="index" v-if="item.type && !item.hidden && Boolean(register[item.type])"
v-if="item.type && !item.hidden && Boolean(registerd[item.type])"
:form-config="item" :form-config="item"
:style="item.style" :style="item.style"
> >
<Component <Component
v-if="Boolean(registerd[item.type])" v-if="Boolean(register[item.type])"
:is="item.type" :is="item.type"
:module-config="moduleConfig" :module-config="moduleConfig"
:form-config="item" :form-config="item"
@ -28,115 +22,117 @@
</el-form> </el-form>
</div> </div>
</template> </template>
<script> <script>
import FormItem from '@/materials/setters/widgets/FormItem.vue'; import FormItem from '@/materials/setters/widgets/FormItem.vue'
import setterLoader from '@/materials/setters/setterLoader'; import setterLoader from '@/materials/setters/setterLoader'
import statusConfig from '../config/statusConfig'; import statusConfig from '../config/statusConfig'
import { mapState } from 'vuex'; import { mapState } from 'vuex'
import { get as _get, pick as _pick } from 'lodash-es'; import { get as _get, pick as _pick } from 'lodash-es'
const textMap = { const textMap = {
Success: '提交成功页面配置', Success: '提交成功页面配置',
OverTime: '问卷过期页面配置', OverTime: '问卷过期页面配置'
}; }
export default { export default {
name: 'StatusEditForm', name: 'SetterPanel',
components: { components: {
FormItem, FormItem
}, },
data() { data() {
return { return {
registerd: {}, register: {}
}; }
}, },
computed: { computed: {
formFieldData() { formFieldData() {
const formList = statusConfig[this.currentEditStatus] || []; const formList = statusConfig[this.currentEditStatus] || []
return formList.map((item) => { return formList.map((item) => {
const value = _get(this.moduleConfig, item.key, item.value); const value = _get(this.moduleConfig, item.key, item.value)
return { return {
...item, ...item,
value, value
}; }
}); })
}, },
currentEditText() { currentEditText() {
return textMap[this.currentEditStatus] || ''; return textMap[this.currentEditStatus] || ''
}, },
...mapState({ ...mapState({
currentEditStatus: (state) => state.edit.currentEditStatus, currentEditStatus: (state) => state.edit.currentEditStatus,
submitConf: (state) => _get(state, 'edit.schema.submitConf'), submitConf: (state) => _get(state, 'edit.schema.submitConf')
}), }),
moduleConfig() { moduleConfig() {
return this.submitConf; return this.submitConf
}, }
}, },
watch: { watch: {
formFieldData: { formFieldData: {
immediate: true, immediate: true,
handler(newVal) { handler(newVal) {
if (Array.isArray(newVal)) { if (Array.isArray(newVal)) {
this.handleComponentRegister(newVal); this.handleComponentRegister(newVal)
} }
}, }
}, }
}, },
methods: { methods: {
async handleComponentRegister(formFieldData) { async handleComponentRegister(formFieldData) {
const setters = formFieldData.map((item) => item.type); const setters = formFieldData.map((item) => item.type)
const settersSet = new Set(setters); const settersSet = new Set(setters)
const settersArr = Array.from(settersSet); const settersArr = Array.from(settersSet)
const allSetters = settersArr.map((item) => { const allSetters = settersArr.map((item) => {
return { return {
type: item, type: item,
path: item, path: item
}; }
}); })
try { try {
const comps = await setterLoader.loadComponents(allSetters); const comps = await setterLoader.loadComponents(allSetters)
for (const comp of comps) { for (const comp of comps) {
if (!comp) { if (!comp) {
continue; continue
} }
const { type, component, err } = comp; const { type, component, err } = comp
if (!err) { if (!err) {
const componentName = component.name; const componentName = component.name
this.$options.components[componentName] = component; this.$options.components[componentName] = component
this.$set(this.registerd, type, componentName); this.register[type] = componentName
} }
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err)
} }
}, },
getValueFromModuleConfig(item) { getValueFromModuleConfig(item) {
const { key, keys } = item; const { key, keys } = item
const moduleConfig = this.moduleConfig; const moduleConfig = this.moduleConfig
let result = item; let result = item
if (key) { if (key) {
result = { result = {
...item, ...item,
value: _get(moduleConfig, key, item.value), value: _get(moduleConfig, key, item.value)
}; }
} }
if (keys) { if (keys) {
result = { result = {
...item, ...item,
value: _pick(moduleConfig, keys), value: _pick(moduleConfig, keys)
}; }
} }
return result; return result
}, },
onFormChange(data) { onFormChange(data) {
const { key, value } = data; const { key, value } = data
const resultKey = `submitConf.${key}`; const resultKey = `submitConf.${key}`
this.$store.dispatch('edit/changeSchema', { key: resultKey, value }); this.$store.dispatch('edit/changeSchema', { key: resultKey, value })
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.question-edit-form { .question-edit-form {
width: 360px; width: 360px;
height: 100%; height: 100%;

View File

@ -2,14 +2,15 @@
<div class="tab-box"> <div class="tab-box">
<div class="title">主题设置</div> <div class="title">主题设置</div>
<div class="content"> <div class="content">
<div class="tag-list"> <div class="tag-list">
<el-tag <el-tag
v-for="item in groupList" v-for="item in groupList"
:class="[groupName === item.value ? 'current' : '', 'tag']" :class="[groupName === item.value ? 'current' : '', 'tag']"
type = 'info' type="info"
:key="item.value" :key="item.value"
@click="() => changeGroup(item.value)"> @click="() => changeGroup(item.value)"
{{item.label}} >
{{ item.label }}
</el-tag> </el-tag>
</div> </div>
<div class="banner-list-wrapper"> <div class="banner-list-wrapper">
@ -18,17 +19,10 @@
v-for="(banner, bannerIndex) in currentBannerList" v-for="(banner, bannerIndex) in currentBannerList"
:key="bannerIndex" :key="bannerIndex"
> >
<img <img class="banner-img" :src="banner.src" loading="lazy" @click="changePreset(banner)" />
class="banner-img"
:src="banner.src"
loading="lazy"
@click="changePreset(banner)"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
@ -36,16 +30,16 @@ import { mapActions } from 'vuex'
import skinPresets from '@/management/config/skinPresets.js' import skinPresets from '@/management/config/skinPresets.js'
export default { export default {
name: 'catalogPanel', name: 'CatalogPanel',
data() { data() {
return { return {
skinPresets: [], skinPresets: [],
groupName: 'temp', groupName: 'temp'
}; }
}, },
computed: { computed: {
bannerList() { bannerList() {
return this.$store?.state?.bannerList || []; return this.$store?.state?.bannerList || []
}, },
groupList() { groupList() {
return Object.keys(this.bannerList).map((key) => { return Object.keys(this.bannerList).map((key) => {
@ -55,20 +49,22 @@ export default {
} }
}) })
}, },
currentBannerList () { currentBannerList() {
const arr = Object.keys(this.bannerList).map((key) => { const arr = Object.keys(this.bannerList)
return this.bannerList[key] .map((key) => {
}).map(data => { return this.bannerList[key]
return data.list.map(item => {
item.group = data.key;
return item;
}) })
}) .map((data) => {
const allbanner = arr.reduce((acc, curr) => { return data.list.map((item) => {
return acc.concat(curr); item.group = data.key
}, []); return item
return allbanner.filter(item => { })
if(this.groupName === "temp") { })
const allbanner = arr.reduce((acc, curr) => {
return acc.concat(curr)
}, [])
return allbanner.filter((item) => {
if (this.groupName === 'temp') {
return true return true
} else { } else {
return item.group === this.groupName return item.group === this.groupName
@ -76,34 +72,31 @@ export default {
}) })
} }
}, },
mounted() { mounted() {},
},
methods: { methods: {
...mapActions({ ...mapActions({
changeThemePreset: 'edit/changeThemePreset', changeThemePreset: 'edit/changeThemePreset'
}), }),
changeGroup(value) { changeGroup(value) {
this.groupName = value this.groupName = value
}, },
changePreset(banner) { changePreset(banner) {
const name = banner.group + '-' + banner.title const name = banner.group + '-' + banner.title
let presets = { let presets = {
'bannerConf.bannerConfig.bgImage': banner.src, 'bannerConf.bannerConfig.bgImage': banner.src,
'skinConf.themeConf.color': '#FAA600', 'skinConf.themeConf.color': '#FAA600',
'skinConf.backgroundConf.color': '#fff', 'skinConf.backgroundConf.color': '#fff'
} }
if(skinPresets[name]){ if (skinPresets[name]) {
presets = Object.assign(presets, skinPresets[name]) presets = Object.assign(presets, skinPresets[name])
} }
this.changeThemePreset(presets) this.changeThemePreset(presets)
} }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" scoped>
.tab-box { .tab-box {
width: 300px; width: 300px;
height: 100%; height: 100%;
@ -120,16 +113,16 @@ export default {
// background: #f9fafc; // background: #f9fafc;
border-bottom: 1px solid #edeffc; border-bottom: 1px solid #edeffc;
} }
.content{ .content {
padding: 12px; padding: 12px;
} }
.tag-list{ .tag-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
.tag{ .tag {
margin: 5px 8px; margin: 5px 8px;
cursor: pointer; cursor: pointer;
&.current{ &.current {
color: $primary-color; color: $primary-color;
background-color: $primary-bg-color; background-color: $primary-bg-color;
} }
@ -153,18 +146,18 @@ export default {
} }
} }
::v-deep .el-collapse-item__header { :deep(.el-collapse-item__header) {
font-size: 16px; font-size: 16px;
color: $font-color-title; color: $font-color-title;
border-bottom: none; border-bottom: none;
} }
::v-deep .el-collapse-item__arrow.is-active { :deep(.el-collapse-item__arrow.is-active) {
right: 0; right: 0;
} }
::v-deep .el-collapse-item__arrow { :deep(.el-collapse-item__arrow) {
right: 0; right: 0;
} }
} }
} }
</style> </style>

View File

@ -3,27 +3,16 @@
<div class="operation-wrapper"> <div class="operation-wrapper">
<div class="box" ref="box"> <div class="box" ref="box">
<div class="mask"></div> <div class="mask"></div>
<banner <BannerContent :bannerConf="bannerConf" />
:bannerConf="bannerConf"
/>
<div class="content"> <div class="content">
<mainTitle <MainTitle :isSelected="false" :bannerConf="bannerConf" />
:isSelected="false" <MaterialGroup :questionDataList="questionDataList" ref="MaterialGroup" />
:bannerConf="bannerConf" <SubmitButton
/>
<materialGroup
:questionDataList="questionDataList"
ref="materialGroup"
/>
<submit
:submit-conf="submitConf" :submit-conf="submitConf"
:skin-conf="skinConf" :skin-conf="skinConf"
:is-selected="currentEditOne === 'submit'" :is-selected="currentEditOne === 'submit'"
/> />
<logo <LogoPreview :logo-conf="bottomConf" :is-selected="currentEditOne === 'logo'" />
:logo-conf="bottomConf"
:is-selected="currentEditOne === 'logo'"
/>
</div> </div>
</div> </div>
</div> </div>
@ -31,27 +20,27 @@
</template> </template>
<script> <script>
import materialGroup from '@/management/pages/edit/components/materialGroup.vue'; import MaterialGroup from '@/management/pages/edit/components/MaterialGroup.vue'
import banner from '../components/banner.vue'; import BannerContent from '../components/BannerContent.vue'
import mainTitle from '@/management/pages/edit/components/mainTitle.vue'; import MainTitle from '@/management/pages/edit/components/MainTitle.vue'
import submit from '@/management/pages/edit/components/submit.vue'; import SubmitButton from '@/management/pages/edit/components/SubmitButton.vue'
import logo from '@/management/pages/edit/components/logo.vue'; import LogoPreview from '@/management/pages/edit/components/LogoPreview.vue'
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex'
import { get as _get } from 'lodash-es'; import { get as _get } from 'lodash-es'
export default { export default {
name: 'previewPanel', name: 'PreviewPanel',
components: { components: {
banner, BannerContent,
mainTitle, MainTitle,
submit, SubmitButton,
logo, LogoPreview,
materialGroup, MaterialGroup
}, },
data() { data() {
return { return {
isAnimating: false, isAnimating: false
}; }
}, },
computed: { computed: {
...mapState({ ...mapState({
@ -60,25 +49,25 @@ export default {
bottomConf: (state) => _get(state, 'edit.schema.bottomConf'), bottomConf: (state) => _get(state, 'edit.schema.bottomConf'),
skinConf: (state) => _get(state, 'edit.schema.skinConf'), skinConf: (state) => _get(state, 'edit.schema.skinConf'),
questionDataList: (state) => _get(state, 'edit.schema.questionDataList'), questionDataList: (state) => _get(state, 'edit.schema.questionDataList'),
currentEditOne: (state) => _get(state, 'edit.currentEditOne'), currentEditOne: (state) => _get(state, 'edit.currentEditOne')
}), }),
...mapGetters({ ...mapGetters({
currentEditKey: 'edit/currentEditKey', currentEditKey: 'edit/currentEditKey'
}), })
}, },
watch: { watch: {
skinConf: { skinConf: {
handler (skinConf) { handler(skinConf) {
const { themeConf, backgroundConf, contentConf} = skinConf const { themeConf, backgroundConf, contentConf } = skinConf
const root = document.documentElement; const root = document.documentElement
if(themeConf?.color) { if (themeConf?.color) {
root.style.setProperty('--primary-color', themeConf?.color); // root.style.setProperty('--primary-color', themeConf?.color) //
} }
if(backgroundConf?.color) { if (backgroundConf?.color) {
root.style.setProperty('--primary-background-color', backgroundConf?.color); // root.style.setProperty('--primary-background-color', backgroundConf?.color) //
} }
if(contentConf?.opacity.toString()) { if (contentConf?.opacity.toString()) {
root.style.setProperty('--opacity', contentConf?.opacity/100); // root.style.setProperty('--opacity', contentConf?.opacity / 100) //
} }
}, },
immediate: true, // immediate: true, //
@ -87,30 +76,30 @@ export default {
}, },
methods: { methods: {
animate(dom, property, targetValue) { animate(dom, property, targetValue) {
const origin = dom[property]; const origin = dom[property]
const subVal = targetValue - origin; const subVal = targetValue - origin
const flag = subVal < 0 ? -1 : 1; const flag = subVal < 0 ? -1 : 1
const step = flag * 50; const step = flag * 50
const totalCount = Math.floor(subVal / step) + 1; const totalCount = Math.floor(subVal / step) + 1
let runCount = 0; let runCount = 0
const run = () => { const run = () => {
dom[property] += step; dom[property] += step
runCount++; runCount++
if (runCount < totalCount) { if (runCount < totalCount) {
requestAnimationFrame(run); requestAnimationFrame(run)
} else { } else {
this.isAnimating = false; this.isAnimating = false
} }
}; }
requestAnimationFrame(run); requestAnimationFrame(run)
}, }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -143,7 +132,7 @@ export default {
scrollbar-width: none; scrollbar-width: none;
width: 90%; width: 90%;
-ms-overflow-style: none; -ms-overflow-style: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
@ -151,7 +140,7 @@ export default {
.box { .box {
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
position: relative; position: relative;
.mask{ .mask {
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;

View File

@ -1,8 +1,6 @@
<template> <template>
<div class="setter-wrapper"> <div class="setter-wrapper">
<div class="setter-title"> <div class="setter-title">样式设置</div>
样式设置
</div>
<div class="setter-content"> <div class="setter-content">
<el-collapse v-model="collapse"> <el-collapse v-model="collapse">
<el-collapse-item <el-collapse-item
@ -11,51 +9,54 @@
:title="collapse.name" :title="collapse.name"
:name="collapse.key" :name="collapse.key"
> >
<setterField <SetterField
:form-config-list="collapse.formConfigList" :form-config-list="collapse.formConfigList"
:module-config="_get(schema, collapse.key, {})" :module-config="_get(schema, collapse.key, {})"
@form-change="(key) => { onFormChange(key, collapse.key) }" @form-change="
/> (key) => {
onFormChange(key, collapse.key)
}
"
/>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</div> </div>
<!-- -->
</div> </div>
</template> </template>
<script> <script>
import skinConfig from '@/management/config/setterConfig/skinConfig'; import skinConfig from '@/management/config/setterConfig/skinConfig'
import setterField from '@/management/pages/edit/components/setterField.vue'; import SetterField from '@/management/pages/edit/components/SetterField.vue'
import { mapState, mapGetters } from 'vuex'; import { mapState } from 'vuex'
import { get as _get } from 'lodash-es' import { get as _get } from 'lodash-es'
export default { export default {
name: 'setterPanel', name: 'SetterPanel',
components: { components: {
setterField, SetterField
}, },
data() { data() {
return { return {
collapse: '', collapse: '',
skinConfig, skinConfig
}; }
}, },
computed: { computed: {
...mapState({ ...mapState({
skinConf: (state) => _get(state, 'edit.schema.skinConf'), skinConf: (state) => _get(state, 'edit.schema.skinConf'),
schema: (state) => _get(state, 'edit.schema'), schema: (state) => _get(state, 'edit.schema')
}), })
}, },
methods: { methods: {
_get, _get,
onFormChange(data,collapse) { onFormChange(data, collapse) {
const { key, value } = data; const { key, value } = data
const currentEditKey = `${collapse}` const currentEditKey = `${collapse}`
const resultKey = `${currentEditKey}.${key}`; const resultKey = `${currentEditKey}.${key}`
this.$store.dispatch('edit/changeSchema', { key: resultKey, value }); this.$store.dispatch('edit/changeSchema', { key: resultKey, value })
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" scoped>
.setter-wrapper { .setter-wrapper {
width: 360px; width: 360px;
height: 100%; height: 100%;
@ -73,26 +74,25 @@ export default {
// background: #f9fafc; // background: #f9fafc;
border-bottom: 1px solid #edeffc; border-bottom: 1px solid #edeffc;
} }
.setter-content{ .setter-content {
padding: 10px 20px; padding: 10px 20px;
.el-collapse { .el-collapse {
border: none; border: none;
::v-deep .el-collapse-item__header{ :deep(.el-collapse-item__header) {
font-size: 14px; font-size: 14px;
color: #606266; color: #606266;
font-weight: bold; font-weight: bold;
border: none; border: none;
} }
::v-deep .el-collapse-item__wrap{ :deep(.el-collapse-item__wrap) {
border: none; border: none;
.el-collapse-item__content{ .el-collapse-item__content {
padding-bottom: 0px!important; padding-bottom: 0px !important;
} }
} }
} }
.config-form{ .config-form {
padding: 0!important; padding: 0 !important;
} }
} }
.no-select-question { .no-select-question {

View File

@ -0,0 +1,36 @@
<template>
<commonTemplate>
<template #left>
<CatalogPanel></CatalogPanel>
</template>
<template #center>
<PreviewPanel></PreviewPanel>
</template>
<template #right>
<SetterPanel></SetterPanel>
</template>
</commonTemplate>
</template>
<script>
import commonTemplate from '../components/CommonTemplate.vue'
import CatalogPanel from '../modules/questionModule/CatalogPanel.vue'
import PreviewPanel from '../modules/questionModule/PreviewPanel.vue'
import SetterPanel from '../modules/questionModule/SetterPanel.vue'
export default {
name: 'EditPage',
components: {
commonTemplate,
CatalogPanel,
PreviewPanel,
SetterPanel
}
}
</script>
<style lang="scss" scoped>
.navbar {
border-bottom: 1px solid #e7e9eb;
}
</style>

View File

@ -1,17 +1,19 @@
<template> <template>
<div class="setting-page"> <div class="setting-page">
<setting></setting> <SettingPanel></SettingPanel>
</div> </div>
</template> </template>
<script> <script>
import setting from '../modules/settingModule/setting.vue'; import SettingPanel from '../modules/settingModule/SettingPanel.vue'
export default { export default {
name: 'questionSettingPage', name: 'SettingPage',
components: { components: {
setting, SettingPanel
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.setting-page { .setting-page {
width: 100%; width: 100%;

View File

@ -1,28 +0,0 @@
<template>
<commonTemplate>
<catalogPanel slot="left"></catalogPanel>
<previewPanel slot="center"></previewPanel>
<setterPanel slot="right"></setterPanel>
</commonTemplate>
</template>
<script>
import commonTemplate from '../components/commonTemplate.vue';
import catalogPanel from '../modules/questionModule/catalogPanel.vue';
import previewPanel from '../modules/questionModule/previewPanel.vue';
import setterPanel from '../modules/questionModule/setterPanel.vue';
export default {
name: 'editIndex',
components: {
commonTemplate,
catalogPanel,
previewPanel,
setterPanel,
},
};
</script>
<style lang="scss" scoped>
.navbar {
border-bottom: 1px solid #e7e9eb;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<commonTemplate>
<template #left>
<CatalogPanel />
</template>
<template #center>
<PreviewPanel />
</template>
<template #right>
<SetterPanel />
</template>
</commonTemplate>
</template>
<script>
import commonTemplate from '../../components/CommonTemplate.vue'
import CatalogPanel from '../../modules/settingModule/skin/CatalogPanel.vue'
import PreviewPanel from '../../modules/settingModule/skin/PreviewPanel.vue'
import SetterPanel from '../../modules/settingModule/skin/SetterPanel.vue'
export default {
name: 'ContentPage',
components: {
commonTemplate,
CatalogPanel,
PreviewPanel,
SetterPanel
},
created() {
this.$store.dispatch('getBannerData')
}
}
</script>
<style lang="scss" scoped>
.navbar {
border-bottom: 1px solid #e7e9eb;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<commonTemplate>
<template #left>
<ResultCatalog />
</template>
<template #center>
<ResultPreview />
</template>
<template #right>
<ResultSetter />
</template>
</commonTemplate>
</template>
<script>
import commonTemplate from '../../components/CommonTemplate.vue'
import ResultCatalog from '../../modules/settingModule/result/CatalogPanel.vue'
import ResultPreview from '../../modules/settingModule/result/PreviewPanel.vue'
import ResultSetter from '../../modules/settingModule/result/SetterPanel.vue'
export default {
name: 'ResultPage',
components: {
commonTemplate,
ResultCatalog,
ResultPreview,
ResultSetter
}
}
</script>

View File

@ -1,32 +0,0 @@
<template>
<commonTemplate>
<catalogPanel slot="left"></catalogPanel>
<previewPanel slot="center"></previewPanel>
<setterPanel slot="right"></setterPanel>
</commonTemplate>
</template>
<script>
import commonTemplate from '../../components/commonTemplate.vue';
import catalogPanel from '../../modules/settingModule/skin/catalogPanel.vue';
import previewPanel from '../../modules/settingModule/skin/previewPanel.vue';
import setterPanel from '../../modules/settingModule/skin/setterPanel.vue';
export default {
name: 'editIndex',
components: {
commonTemplate,
catalogPanel,
previewPanel,
setterPanel,
},
created() {
this.$store.dispatch('getBannerData');
}
};
</script>
<style lang="scss" scoped>
.navbar {
border-bottom: 1px solid #e7e9eb;
}
</style>

View File

@ -1,10 +1,13 @@
<template> <template>
<div class="skin-content"> <div class="skin-content">
<div class="navbar-tab"> <div class="navbar-tab">
<el-radio-group size="mini" style="margin-bottom: 30px;" v-model="activeRouter"> <el-radio-group v-model="activeRouter">
<el-radio-button :label="btnItem.router" :key="btnItem.router" v-for="btnItem in btnList" > <el-radio-button
<span>{{ btnItem.text }}</span> v-for="btnItem in btnList"
</el-radio-button> :key="btnItem.router"
:label="btnItem.text"
:value="btnItem.router"
/>
</el-radio-group> </el-radio-group>
</div> </div>
<router-view></router-view> <router-view></router-view>
@ -22,24 +25,24 @@ export default {
text: '内容页', text: '内容页',
router: 'QuestionSkinSetting', router: 'QuestionSkinSetting',
key: 'skinsettings', key: 'skinsettings',
next: true, next: true
}, },
{ {
text: '结果页', text: '结果页',
router: 'QuestionEditResultConfig', router: 'QuestionEditResultConfig',
key: 'status', key: 'status'
}, }
], ]
}; }
}, },
watch: { watch: {
activeRouter: { activeRouter: {
handler (val) { handler(val) {
this.$router.push({ name: val}) this.$router.push({ name: val })
} }
} }
}, }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.skin-content { .skin-content {
@ -55,14 +58,14 @@ export default {
top: 10px; top: 10px;
cursor: pointer; cursor: pointer;
z-index: 9999; z-index: 9999;
::v-deep .el-radio-button__orig-radio:checked + .el-radio-button__inner{ :deep(.el-radio-button__original-radio + .el-radio-button__inner) {
color: $primary-color; font-size: 12px;
background-color: #fff!important; height: 28px;
// &:active{ }
// color: $primary-color; :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
// } color: $primary-color;
background-color: #fff;
} }
} }
} }
</style> </style>

View File

@ -1,24 +0,0 @@
<template>
<commonTemplate>
<resultCatalog slot="left"></resultCatalog>
<resultPreview slot="center"></resultPreview>
<resultSetter slot="right"></resultSetter>
</commonTemplate>
</template>
<script>
import commonTemplate from '../../components/commonTemplate.vue';
import resultCatalog from '../../modules/settingModule/result/catalogPanel.vue';
import resultPreview from '../../modules/settingModule/result/previewPanel.vue';
import resultSetter from '../../modules/settingModule/result/setterPanel.vue';
export default {
name: 'editIndex',
components: {
commonTemplate,
resultCatalog,
resultPreview,
resultSetter,
},
};
</script>
<style lang="scss" scoped></style>

View File

@ -2,7 +2,7 @@
<div class="tableview-root"> <div class="tableview-root">
<div class="filter-wrap"> <div class="filter-wrap">
<div class="select"> <div class="select">
<text-select <TextSelect
v-for="item in Object.keys(selectOptionsDict)" v-for="item in Object.keys(selectOptionsDict)"
:key="item" :key="item"
:effect-fun="onSelectChange" :effect-fun="onSelectChange"
@ -11,7 +11,7 @@
/> />
</div> </div>
<div class="search"> <div class="search">
<text-button <TextButton
v-for="item in Object.keys(buttonOptionsDict)" v-for="item in Object.keys(buttonOptionsDict)"
:key="item" :key="item"
:effect-fun="onButtonChange" :effect-fun="onButtonChange"
@ -20,16 +20,11 @@
:icon=" :icon="
buttonOptionsDict[item].icons.find( buttonOptionsDict[item].icons.find(
(iconItem) => iconItem.effectValue === buttonValueMap[item] (iconItem) => iconItem.effectValue === buttonValueMap[item]
).name ).icon
" "
size="mini" link
type="text"
></text-button>
<text-search
placeholder="请输入问卷标题"
:value="searchVal"
@search="onSearchText"
/> />
<TextSearch placeholder="请输入问卷标题" :value="searchVal" @search="onSearchText" />
</div> </div>
</div> </div>
<el-table <el-table
@ -56,7 +51,7 @@
:min-width="field.width || field.minWidth" :min-width="field.width || field.minWidth"
class-name="link" class-name="link"
> >
<template slot-scope="scope"> <template #default="scope">
<template v-if="field.comp"> <template v-if="field.comp">
<component :is="field.comp" type="table" :value="scope.row" /> <component :is="field.comp" type="table" :value="scope.row" />
</template> </template>
@ -67,7 +62,7 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" :width="300" class-name="table-options"> <el-table-column label="操作" :width="300" class-name="table-options">
<template slot-scope="scope"> <template #default="scope">
<ToolBar <ToolBar
:data="scope.row" :data="scope.row"
type="list" type="list"
@ -91,10 +86,10 @@
</div> </div>
<div v-else> <div v-else>
<empty :data="!searchVal ? noListDataConfig : noSearchDataConfig" /> <EmptyIndex :data="!searchVal ? noListDataConfig : noSearchDataConfig" />
</div> </div>
<modify-dialog <ModifyDialog
:type="modifyType" :type="modifyType"
:visible="showModify" :visible="showModify"
:question-info="questionInfo" :question-info="questionInfo"
@ -104,44 +99,43 @@
</template> </template>
<script> <script>
import { get, map } from 'lodash-es'; import { get, map } from 'lodash-es'
import moment from 'moment';
import { ElMessage, ElMessageBox } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import 'element-plus/theme-chalk/src/message-box.scss'
import moment from 'moment'
// //
import 'moment/locale/zh-cn'; import 'moment/locale/zh-cn'
// //
moment.locale('zh-cn'); moment.locale('zh-cn')
import empty from '@/management/components/empty';
import ModifyDialog from './modify'; import EmptyIndex from '@/management/components/EmptyIndex.vue'
import Tag from './tag'; import { CODE_MAP } from '@/management/api/base'
import State from './state'; import { QOP_MAP } from '@/management/utils/constant'
import ToolBar from './toolBar'; import { getSurveyList, deleteSurvey } from '@/management/api/survey'
import TextSearch from './textSearch';
import TextSelect from './textSelect'; import ModifyDialog from './ModifyDialog.vue'
import TextButton from './textButton'; import TagModule from './TagModule.vue'
import StateModule from './StateModule.vue'
import ToolBar from './ToolBar.vue'
import TextSearch from './TextSearch.vue'
import TextSelect from './TextSelect.vue'
import TextButton from './TextButton.vue'
import { import {
fieldConfig, fieldConfig,
noListDataConfig, noListDataConfig,
noSearchDataConfig, noSearchDataConfig,
selectOptionsDict, selectOptionsDict,
buttonOptionsDict, buttonOptionsDict
} from '../config'; } from '../config'
import { CODE_MAP } from '@/management/api/base';
import { QOP_MAP } from '@/management/utils/constant';
import { getSurveyList, deleteSurvey } from '@/management/api/survey';
export default { export default {
name: 'BaseList', name: 'BaseList',
data() { data() {
return { return {
fields: [ fields: ['type', 'title', 'remark', 'owner', 'state', 'createDate', 'updateDate'],
'type',
'title',
'remark',
'owner',
'state',
'createDate',
'updateDate',
],
showModify: false, showModify: false,
modifyType: '', modifyType: '',
loading: false, loading: false,
@ -155,29 +149,29 @@ export default {
selectOptionsDict, selectOptionsDict,
selectValueMap: { selectValueMap: {
surveyType: '', surveyType: '',
'curStatus.status': '', 'curStatus.status': ''
}, },
buttonOptionsDict, buttonOptionsDict,
buttonValueMap: { buttonValueMap: {
'curStatus.date': '', 'curStatus.date': '',
createDate: -1, createDate: -1
}, }
}; }
}, },
computed: { computed: {
fieldList() { fieldList() {
const fieldInfo = map(this.fields, (f) => { const fieldInfo = map(this.fields, (f) => {
return get(fieldConfig, f, null); return get(fieldConfig, f, null)
}); })
return fieldInfo; return fieldInfo
}, },
dataList() { dataList() {
return this.data.map((item) => { return this.data.map((item) => {
return { return {
...item, ...item,
'curStatus.date': item.curStatus.date, 'curStatus.date': item.curStatus.date
}; }
}); })
}, },
filter() { filter() {
return [ return [
@ -187,193 +181,188 @@ export default {
{ {
field: 'title', field: 'title',
value: this.searchVal, value: this.searchVal,
comparator: '$regex', comparator: '$regex'
}, }
], ]
}, },
{ {
comparator: '', comparator: '',
condition: [ condition: [
{ {
field: 'curStatus.status', field: 'curStatus.status',
value: this.selectValueMap['curStatus.status'], value: this.selectValueMap['curStatus.status']
}, }
], ]
}, },
{ {
comparator: '', comparator: '',
condition: [ condition: [
{ {
field: 'surveyType', field: 'surveyType',
value: this.selectValueMap.surveyType, value: this.selectValueMap.surveyType
}, }
], ]
}, }
]; ]
}, },
order() { order() {
const formatOrder = Object.entries(this.buttonValueMap) const formatOrder = Object.entries(this.buttonValueMap)
.filter(([, effectValue]) => effectValue) .filter(([, effectValue]) => effectValue)
.reduce((prev, item) => { .reduce((prev, item) => {
const [effectKey, effectValue] = item; const [effectKey, effectValue] = item
prev.push({ field: effectKey, value: effectValue }); prev.push({ field: effectKey, value: effectValue })
return prev; return prev
}, []); }, [])
return JSON.stringify(formatOrder); return JSON.stringify(formatOrder)
}, }
}, },
created() { created() {
this.init(); this.init()
}, },
methods: { methods: {
async init() { async init() {
this.loading = true; this.loading = true
try { try {
const filter = JSON.stringify( const filter = JSON.stringify(
this.filter.filter((item) => { this.filter.filter((item) => {
return item.condition[0].value; return item.condition[0].value
}) })
); )
const res = await getSurveyList({ const res = await getSurveyList({
curPage: this.currentPage, curPage: this.currentPage,
filter, filter,
order: this.order, order: this.order
}); })
this.loading = false; this.loading = false
if (res.code === CODE_MAP.SUCCESS) { if (res.code === CODE_MAP.SUCCESS) {
this.total = res.data.count; this.total = res.data.count
this.data = res.data.data; this.data = res.data.data
} else { } else {
this.$message({ ElMessage.error(res.errmsg)
type: 'error',
message: res.errmsg,
});
} }
} catch (error) { } catch (error) {
this.$message({ ElMessage.error(error)
type: 'error', this.loading = false
message: error,
});
this.loading = false;
} }
}, },
getStatus(data) { getStatus(data) {
return get(data, 'curStatus.status', 'new'); return get(data, 'curStatus.status', 'new')
}, },
getToolConfig() { getToolConfig() {
const funcList = [ const funcList = [
{ {
key: QOP_MAP.EDIT, key: QOP_MAP.EDIT,
label: '修改', label: '修改'
}, },
{ {
key: 'analysis', key: 'analysis',
label: '数据', label: '数据'
}, },
{ {
key: 'release', key: 'release',
label: '投放', label: '投放'
}, },
{ {
key: 'delete', key: 'delete',
label: '删除', label: '删除',
icon: 'icon-shanchu', icon: 'icon-shanchu'
}, },
{ {
key: QOP_MAP.COPY, key: QOP_MAP.COPY,
label: '复制', label: '复制',
icon: 'icon-shanchu', icon: 'icon-shanchu'
}, }
]; ]
return funcList; return funcList
}, },
async onDelete(row) { async onDelete(row) {
try { try {
await this.$confirm('是否确认删除?', '提示', { await ElMessageBox.confirm('是否确认删除?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning'
}); })
} catch (error) { } catch (error) {
console.log('取消删除'); console.log('取消删除')
return; return
} }
const res = await deleteSurvey(row._id); const res = await deleteSurvey(row._id)
if (res.code === CODE_MAP.SUCCESS) { if (res.code === CODE_MAP.SUCCESS) {
this.$message.success('删除成功'); ElMessage.success('删除成功')
this.init(); this.init()
} else { } else {
this.$message.error(res.errmsg || '删除失败'); ElMessage.error(res.errmsg || '删除失败')
} }
}, },
handleCurrentChange(current) { handleCurrentChange(current) {
this.currentPage = current; this.currentPage = current
this.init(); this.init()
}, },
onModify(data, type = QOP_MAP.EDIT) { onModify(data, type = QOP_MAP.EDIT) {
this.showModify = true; this.showModify = true
this.modifyType = type; this.modifyType = type
this.questionInfo = data; this.questionInfo = data
}, },
onCloseModify(type) { onCloseModify(type) {
this.showModify = false; this.showModify = false
this.questionInfo = {}; this.questionInfo = {}
if (type === 'update') { if (type === 'update') {
this.init(); this.init()
} }
}, },
onRowClick(row) { onRowClick(row) {
this.$router.push({ this.$router.push({
name: 'QuestionEditIndex', name: 'QuestionEditIndex',
params: { params: {
id: row._id, id: row._id
}, }
}); })
}, },
onSearchText(e) { onSearchText(e) {
this.searchVal = e; this.searchVal = e
this.currentPage = 1; this.currentPage = 1
this.init(); this.init()
}, },
onSelectChange(selectValue, selectKey) { onSelectChange(selectValue, selectKey) {
this.selectValueMap[selectKey] = selectValue; this.selectValueMap[selectKey] = selectValue
this.currentPage = 1; this.currentPage = 1
this.init(); this.init()
}, },
onButtonChange(effectValue, effectKey) { onButtonChange(effectValue, effectKey) {
this.buttonValueMap = { this.buttonValueMap = {
'curStatus.date': '', 'curStatus.date': '',
createDate: '', createDate: ''
}; }
this.buttonValueMap[effectKey] = effectValue; this.buttonValueMap[effectKey] = effectValue
this.init(); this.init()
}, }
}, },
components: { components: {
empty, EmptyIndex,
ModifyDialog, ModifyDialog,
Tag, TagModule,
ToolBar, ToolBar,
TextSearch, TextSearch,
TextSelect, TextSelect,
TextButton, TextButton,
State, StateModule
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped> <style lang="scss" scoped>
.tableview-root { .tableview-root {
.filter-wrap { .filter-wrap {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.select { .select {
display: flex; display: flex;
} }
.search { .search {
display: flex; display: flex;
padding-bottom: 20px;
} }
} }
@ -383,12 +372,12 @@ export default {
} }
.list-pagination { .list-pagination {
margin-top: 20px; margin-top: 20px;
::v-deep .el-pagination { :deep(.el-pagination) {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
} }
::v-deep .el-table__header { :deep(.el-table__header) {
.tableview-header .el-table__cell { .tableview-header .el-table__cell {
.cell { .cell {
height: 24px; height: 24px;
@ -397,7 +386,7 @@ export default {
} }
} }
} }
::v-deep .tableview-row { :deep(.tableview-row) {
.tableview-cell { .tableview-cell {
padding: 5px 0; padding: 5px 0;
&.link { &.link {

View File

@ -1,7 +1,7 @@
<template> <template>
<el-dialog <el-dialog
class="base-dialog-root" class="base-dialog-root"
:visible="visible" :model-value="visible"
width="40%" width="40%"
title="基础信息" title="基础信息"
@close="onClose" @close="onClose"
@ -10,91 +10,97 @@
class="base-form-root" class="base-form-root"
ref="ruleForm" ref="ruleForm"
:model="current" :model="current"
label-width="80px"
:rules="rules" :rules="rules"
label-position="top" label-position="top"
@submit.native.prevent size="large"
@submit.prevent
> >
<el-form-item label="标题" prop="title"> <el-form-item label="标题" prop="title">
<el-input size="medium" v-model="current.title" /> <el-input v-model="current.title" />
</el-form-item> </el-form-item>
<el-form-item label="备注"> <el-form-item label="备注">
<el-input size="medium" v-model="current.remark" /> <el-input v-model="current.remark" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <template #footer>
<el-button type="primary" class="save-btn" @click="onSave">{{ <div class="dialog-footer">
type === QOP_MAP.EDIT ? '保存' : '确定' <el-button type="primary" class="save-btn" @click="onSave">{{
}}</el-button> type === QOP_MAP.EDIT ? '保存' : '确定'
</div> }}</el-button>
</div>
</template>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import { CODE_MAP } from '@/management/api/base'; import { pick as _pick } from 'lodash-es'
import { updateSurvey, createSurvey } from '@/management/api/survey';
import { pick as _pick } from 'lodash-es'; import { ElMessage } from 'element-plus'
import { QOP_MAP } from '@/management/utils/constant'; import 'element-plus/theme-chalk/src/message.scss'
import { CODE_MAP } from '@/management/api/base'
import { updateSurvey, createSurvey } from '@/management/api/survey'
import { QOP_MAP } from '@/management/utils/constant'
export default { export default {
name: 'modifyDialog', name: 'ModifyDialog',
props: { props: {
type: String, type: String,
questionInfo: Object, questionInfo: Object,
width: String, width: String,
visible: Boolean, visible: Boolean
}, },
data() { data() {
return { return {
QOP_MAP, QOP_MAP,
loadingInstance: null, loadingInstance: null,
rules: { rules: {
title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }], title: [{ required: true, message: '请输入问卷标题', trigger: 'blur' }]
}, },
current: this.getCurrent(this.questionInfo), current: this.getCurrent(this.questionInfo)
}; }
}, },
watch: { watch: {
questionInfo: { questionInfo: {
handler(val) { handler(val) {
this.current = this.getCurrent(val); this.current = this.getCurrent(val)
}, },
deep: true, deep: true
}, }
}, },
methods: { methods: {
getCurrent(val) { getCurrent(val) {
return { return {
..._pick(val, ['title', 'remark']), ..._pick(val, ['title', 'remark'])
}; }
}, },
onClose() { onClose() {
this.$emit('on-close-codify'); this.$emit('on-close-codify')
}, },
async onSave() { async onSave() {
if (this.type === QOP_MAP.COPY) { if (this.type === QOP_MAP.COPY) {
await this.handleCopy(); await this.handleCopy()
} else { } else {
await this.handleUpdate(); await this.handleUpdate()
} }
this.$emit('on-close-codify', 'update'); this.$emit('on-close-codify', 'update')
}, },
async handleUpdate() { async handleUpdate() {
try { try {
const res = await updateSurvey({ const res = await updateSurvey({
surveyId: this.questionInfo._id, surveyId: this.questionInfo._id,
...this.current, ...this.current
}); })
if (res.code === CODE_MAP.SUCCESS) { if (res.code === CODE_MAP.SUCCESS) {
this.$message.success('修改成功'); ElMessage.success('修改成功')
} else { } else {
this.$message.error(res.errmsg); ElMessage.error(res.errmsg)
} }
} catch (err) { } catch (err) {
this.$message.error(err); ElMessage.error(err)
} }
}, },
async handleCopy() { async handleCopy() {
@ -102,26 +108,30 @@ export default {
const res = await createSurvey({ const res = await createSurvey({
createFrom: this.questionInfo._id, createFrom: this.questionInfo._id,
createMethod: QOP_MAP.COPY, createMethod: QOP_MAP.COPY,
...this.current, ...this.current
}); })
if (res.code === CODE_MAP.SUCCESS) { if (res.code === CODE_MAP.SUCCESS) {
const { data } = res; const { data } = res
this.$router.push({ this.$router.push({
name: 'QuestionEditIndex', name: 'QuestionEditIndex',
params: { params: {
id: data.id, id: data.id
}, }
}); })
} else { } else {
this.$message.error(res.errmsg); ElMessage.error(res.errmsg)
} }
} catch (err) { } catch (err) {
this.$message.error(err); ElMessage.error(err)
} }
}, }
}, }
}; }
</script> </script>
<style lang="scss" rel="lang/scss" scoped></style> <style lang="scss" rel="lang/scss" scoped>
.base-form-root {
padding: 20px;
}
</style>

View File

@ -4,21 +4,23 @@
<span>{{ statusMaps[value.curStatus.status] }}</span> <span>{{ statusMaps[value.curStatus.status] }}</span>
</div> </div>
</template> </template>
<script> <script>
import { statusMaps } from '../config'; import { statusMaps } from '../config'
export default { export default {
name: 'State', name: 'StateModule',
props: { props: {
value: Object, value: Object
}, },
data() { data() {
return { return {
statusMaps, statusMaps
}; }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.list-state { .list-state {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -0,0 +1,15 @@
<template>
<i-ep-sort v-if="props.icon === 'sort'" />
<i-ep-sort-up v-if="props.icon === 'sort-up'" />
<i-ep-sort-down v-if="props.icon === 'sort-down'" />
</template>
<script setup>
// unplugin-icons使icon
// see https://github.com/unplugin/unplugin-icons/issues/5
const props = defineProps({
icon: {
type: String,
required: true
}
})
</script>

View File

@ -1,27 +1,27 @@
<template> <template>
<span :class="['list-tag-root', 'list-tag-' + type]"> <span :class="['list-tag-root', 'list-tag-' + type]">
<div class="tag-bg"></div> <div class="tag-bg"></div>
<span>{{ <span>{{ surveyType[value.surveyType] || surveyType[value.questionType] }}</span>
surveyType[value.surveyType] || surveyType[value.questionType]
}}</span>
</span> </span>
</template> </template>
<script> <script>
import { type as surveyType } from '../config'; import { type as surveyType } from '../config'
export default { export default {
name: 'Tag', name: 'TagModule',
props: { props: {
value: Object, value: Object,
type: String, type: String
}, },
data() { data() {
return { return {
surveyType, surveyType
}; }
}, }
}; }
</script> </script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.list-tag-root { .list-tag-root {
display: inline-block; display: inline-block;
position: relative; position: relative;

Some files were not shown because too many files have changed in this diff Show More