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*
pnpm-debug.log*
package-lock.json
pnpm-lock.yaml
# Editor directories and files
.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",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"report": "vue-cli-service build --report",
"lint": "vue-cli-service lint",
"lintfix": "eslint --fix ."
"serve": "vite",
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"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": {
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
"@vue/babel-preset-jsx": "^1.4.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"async-validator": "^4.2.5",
"axios": "^1.4.0",
"clipboard": "^2.0.11",
"core-js": "^3.8.3",
"crypto-js": "^4.2.0",
"element-ui": "^2.15.13",
"element-plus": "^2.7.0",
"lodash-es": "^4.17.21",
"moment": "^2.29.4",
"node-forge": "^1.3.1",
"qrcode": "^1.5.3",
"vue": "^2.7.14",
"vue-router": "^3.5.1",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"vue": "^3.4.15",
"vue-router": "^4.2.5",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.2",
"xss": "^1.0.14"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.7.1",
"less-loader": "^11.1.3",
"postcss-import": "^15.1.0",
"postcss-url": "^10.1.3",
"prettier": "^2.4.1",
"sass": "^1.32.7",
"sass-loader": "^12.0.0",
"speed-measure-webpack-plugin": "^1.5.0",
"style-resources-loader": "^1.5.0",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.7.14"
"@iconify-json/ep": "^1.1.15",
"@rushstack/eslint-patch": "^1.10.2",
"@tsconfig/node20": "^20.1.2",
"@types/node": "^20.11.19",
"@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"npm-run-all2": "^6.1.1",
"prettier": "^3.0.3",
"sass": "^1.72.0",
"typescript": "~5.3.0",
"unplugin-auto-import": "^0.17.5",
"unplugin-icons": "^0.18.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.4",
"vite-plugin-virtual-mpa": "^1.11.0",
"vue-tsc": "^1.8.27"
},
"engines": {
"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>
<div class="editor-v2">
<RichEditor :value="realData" @input="handleChange" @blur="handleBlur" />
<RichEditor :modelValue="realData" @input="handleChange" @blur="handleBlur" />
</div>
</template>
<script>
import RichEditor from './RichEditor';
import RichEditor from './RichEditor.vue'
export default {
components: {
RichEditor,
RichEditor
// ReadOnly,
},
props: {
realData: {
type: String,
default: () => '',
default: () => ''
},
questionDataList: {
type: Array,
default: () => [],
},
default: () => []
}
},
data() {
return {};
return {}
},
computed: {},
watch: {},
destroyed() {
this.$emit('onDestroy');
unmounted() {
this.$emit('onDestroy')
},
methods: {
handleChange(v) {
this.$emit('change', v);
this.$emit('change', v)
},
handleBlur(v) {
this.$emit('blur', v);
},
},
};
this.$emit('blur', v)
}
}
}
</script>
<style lang="scss" scoped>
.editor-v2 {

View File

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

View File

@ -1,126 +1,119 @@
<template>
<div class="editor-wrapper border">
<div class="toolbar" ref="toolbar" v-show="showToolbar"></div>
<div class="editor" ref="editor"></div>
<Toolbar
:class="['toolbar', props.staticToolBar ? 'static-toolbar' : 'dynamic-toolbar']"
ref="toolbar"
v-show="showToolbar"
:editor="editorRef"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
class="editor"
ref="editor"
:modelValue="curValue"
:defaultConfig="editorConfig"
@onCreated="onCreated"
@onChange="onChange"
@onBlur="onBlur"
@onFocus="onFocus"
:mode="mode"
/>
</div>
</template>
<script>
import { createEditor, createToolbar } from '@wangeditor/editor';
<script setup>
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 {
name: 'richEditor',
data() {
return {
curValue: '',
editor: null,
showToolbar: false,
};
},
props: ['value'], // value v-model
mounted() {
this.create();
},
watch: {
value: {
immediate: true,
handler(newVal) {
const isEqual = newVal === this.curValue;
if (isEqual) return; //
const emit = defineEmits(['input', 'onFocus', 'change', 'blur'])
const model = defineModel()
const props = defineProps(['staticToolBar'])
// HTML
this.setHtml(newVal);
},
},
},
methods: {
// HTML
setHtml(newHtml) {
const editor = this.editor;
if (editor === null) return;
editor.setHtml(newHtml);
},
// editor
create() {
if (this.$refs.editor === null) return;
const curValue = ref('')
const editorRef = shallowRef()
const showToolbar = ref(props.staticToolBar || false)
createEditor({
selector: this.$refs.editor,
html: this.defaultHtml || this.value || '',
config: {
onCreated: (editor) => {
this.editor = Object.seal(editor);
const mode = 'simple'
if (this.value) {
this.setHtml(this.value);
}
this.$refs.toolbar &&
createToolbar({
editor,
selector: this.$refs.toolbar,
config: {
const toolbarConfig = {
toolbarKeys: [
'color', //
'bgColor', //
'bold',
// 'insertImage', //
// 'video',
// 'fontSize', //
// 'justify', //
'insertLink', //
// 'clean',
],
'insertLink' //
]
}
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
setHtml(newVal)
},
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>
<style lang="scss" scoped>
@import url('@wangeditor/editor/dist/css/style.css');
@import url('@/management/styles/reset-wangeditor.scss');
.editor-wrapper {
position: relative;
width: 100%;
// min-height: 45px;
}
.toolbar {
.static-toolbar {
border-bottom: 1px solid #dedede;
}
.dynamic-toolbar {
position: absolute;
left: 0;
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({
onIgnoreTagAttr(tag, name, value) {
if (name === 'style' || name === 'class') {
return `${name}="${value}"`;
return `${name}="${value}"`
}
return undefined;
return undefined
},
onIgnoreTag(tag, html) {
// <xxx>过滤为空,否则不过滤为空
var re1 = new RegExp('<.+?>', 'g');
var re1 = new RegExp('<.+?>', 'g')
if (re1.test(html)) {
return '';
return ''
} else {
return html;
return html
}
},
});
}
})
const isImg = (html) => {
html = html + '';
return html.indexOf('<img') > -1;
};
html = html + ''
return html.indexOf('<img') > -1
}
const isVideo = (html) => {
html = html + '';
return html.indexOf('<video') > -1;
};
html = html + ''
return html.indexOf('<video') > -1
}
export const cleanRichText = (text) => {
if (!text) {
return text === 0 ? 0 : '';
return text === 0 ? 0 : ''
}
const html = transformHtmlTag(text);
const content = html.replace(/<[^<>]+>/g, '').replace(/&nbsp;/g, '');
if (content) return content;
const html = transformHtmlTag(text)
const content = html.replace(/<[^<>]+>/g, '').replace(/&nbsp;/g, '')
if (content) return content
if (isImg(html)) return '图片';
if (isVideo(html)) return '视频';
return '文本';
};
if (isImg(html)) return '图片'
if (isVideo(html)) return '视频'
return '文本'
}
export function escapeHtml(html) {
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
return html.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
export const transformHtmlTag = (html) => {
if (!html) return '';
if (typeof html !== 'string') return html + '';
if (!html) return ''
if (typeof html !== 'string') return html + ''
return html
.replace(html ? /&(?!#?\w+;)/g : /&/g, '&amp;')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/\\\n/g, '\\n');
.replace(/\\\n/g, '\\n')
//.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>
</div>
</template>
<script>
export default {
name: 'App',
};
name: 'App'
}
</script>
<style lang="scss">
@import url('./styles/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) => {
return axios.get('/survey/dataStatistic/dataTable', {
params: {
pageSize: 10,
...data,
},
});
};
...data
}
})
}

View File

@ -1,9 +1,9 @@
import axios from './base';
import axios from './base'
export const register = (data) => {
return axios.post('/auth/register', data);
};
return axios.post('/auth/register', 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 store from '@/management/store/index';
import router from '@/management/router/index';
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;
import axios from 'axios'
import store from '@/management/store/index'
import router from '@/management/router/index'
import { get as _get } from 'lodash-es'
export const CODE_MAP = {
SUCCESS: 200,
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 }) => {
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 = () => {
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 }) => {
return axios.get('/survey/getList', {
@ -6,48 +6,48 @@ export const getSurveyList = ({ curPage, filter, order }) => {
pageSize: 10,
curPage,
filter,
order,
},
});
};
order
}
})
}
export const getSurveyById = (id) => {
return axios.get('/survey/getSurvey', {
params: {
surveyId: id,
},
});
};
surveyId: id
}
})
}
export const saveSurvey = ({ surveyId, configData }) => {
return axios.post('/survey/updateConf', { surveyId, configData });
};
return axios.post('/survey/updateConf', { surveyId, configData })
}
export const publishSurvey = ({ surveyId }) => {
return axios.post('/survey/publishSurvey', {
surveyId,
});
};
surveyId
})
}
export const createSurvey = (data) => {
return axios.post('/survey/createSurvey', data);
};
return axios.post('/survey/createSurvey', data)
}
export const getSurveyHistory = ({ surveyId, historyType }) => {
return axios.get('/surveyHisotry/getList', {
params: {
surveyId,
historyType,
},
});
};
historyType
}
})
}
export const deleteSurvey = (surveyId) => {
return axios.post('/survey/deleteSurvey', {
surveyId,
});
};
surveyId
})
}
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">
<img class="img" :src="data.img" />
<div class="title">{{ data.title }}</div>
<div class="desc" v-html="data.desc" />
<div class="desc" v-html="data.desc"></div>
</div>
</template>
<script>
export default {
name: 'Empty',
name: 'EmptyModule',
props: {
data: {
type: Object,
required: true,
},
},
};
required: true
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.default-empty-root {
width: 350px;
margin: 200px auto;

View File

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

View File

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

View File

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

View File

@ -4,84 +4,75 @@ const menuItems = {
snapshot: '/imgs/question-type-snapshot/iL84te6xxU1657702189333.webp',
path: 'InputModule',
icon: 'tixing-danhangshuru',
title: '单行输入框',
title: '单行输入框'
},
textarea: {
type: 'textarea',
snapshot: '/imgs/question-type-snapshot/11iAo3ca0u1657702225416.webp',
path: 'TextareaModule',
icon: 'tixing-duohangshuru',
title: '多行输入框',
title: '多行输入框'
},
radio: {
type: 'radio',
snapshot: '/imgs/question-type-snapshot/TgeRDfURJZ1657702220602.webp',
icon: 'tixing-danxuan',
path: 'RadioModule',
title: '单项选择',
title: '单项选择'
},
checkbox: {
type: 'checkbox',
path: 'CheckboxModule',
snapshot: '/imgs/question-type-snapshot/Md2YmzBBpV1657702223744.webp',
icon: 'tixing-duoxuan',
title: '多项选择',
title: '多项选择'
},
'binary-choice': {
type: 'binary-choice',
snapshot: '/imgs/question-type-snapshot/blW8U1ckzd1657702223023.webp',
path: 'BinaryChoiceModule',
icon: 'tixing-panduanti',
title: '判断题',
title: '判断题'
},
'radio-star': {
type: 'radio-star',
snapshot: '/imgs/question-type-snapshot/7CU6tn4XqT1657702221208.webp',
path: 'StarModule',
icon: 'tixing-pingfen',
title: '评分',
title: '评分'
},
'radio-nps': {
type: 'radio-nps',
path: 'NpsModule',
snapshot: '/imgs/question-type-snapshot/radio-nps.webp',
icon: 'NPSpingfen',
title: 'nps评分',
title: 'nps评分'
},
vote: {
type: 'vote',
path: 'VoteModule',
snapshot: '/imgs/question-type-snapshot/nGTscsZlwn1657702222857.webp',
icon: 'tixing-toupiao',
title: '投票',
},
};
title: '投票'
}
}
const menuGroup = [
{
title: '输入类题型',
questionList: ['text', 'textarea'],
questionList: ['text', 'textarea']
},
{
title: '选择类题型',
questionList: [
'radio',
'checkbox',
'binary-choice',
'radio-star',
'radio-nps',
'vote',
],
},
];
questionList: ['radio', 'checkbox', 'binary-choice', 'radio-star', 'radio-nps', 'vote']
}
]
const menu = menuGroup.map((group) => {
group.questionList = group.questionList.map(
(question) => menuItems[question]
);
return group;
});
group.questionList = group.questionList.map((question) => menuItems[question])
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 [
{
label: '顶部图片地址',
type: 'Input',
type: 'InputSetter',
key: 'bgImage',
inline: true,
direction: 'horizon',
labelStyle: { width: '120px' }
},
{
label: '顶部视频地址',
type: 'Input',
type: 'InputSetter',
key: 'videoLink',
direction: 'horizon',
labelStyle: { width: '120px' }
},
{
label: '视频海报地址',
type: 'Input',
type: 'InputSetter',
key: 'postImg',
direction: 'horizon',
labelStyle: { width: '120px' }
},
{
label: '图片支持点击',
type: 'CustomedSwitch',
direction: 'horizon',
labelStyle: { width: '120px' },
key: 'bgImageAllowJump',
key: 'bgImageAllowJump'
},
{
label: '跳转链接',
type: 'Input',
direction: 'horizon',
type: 'InputSetter',
labelStyle: { width: '120px' },
key: 'bgImageJumpLink',
relyFunc: (data) => {
return !!data?.bgImageAllowJump;
},
},
];
return !!data?.bgImageAllowJump
}
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,58 +1,56 @@
<template>
<div class="title-wrapper" @click="handleClick()">
<div class="main-title" :class="{ active: isSelected }">
<richEditor
:value="bannerConf?.titleConfig?.mainTitle"
<RichEditor
:modelValue="bannerConf?.titleConfig?.mainTitle"
@input="onTitleInput"
></richEditor>
></RichEditor>
</div>
</div>
</template>
<script>
import richEditor from '@/common/Editor/RichEditor';
import RichEditor from '@/common/Editor/RichEditor.vue'
export default {
name: 'mainTitlePreview',
data() {
return {};
return {}
},
props: {
preview: {
type: Boolean,
default: false,
default: false
},
bannerConf: {
type: Object,
type: Object
},
isSelected: {
type: Boolean,
},
type: Boolean
}
},
computed: {},
methods: {
handleClick() {
if(this.preview) {
return false
} else {
this.$emit('select');
}
this.$emit('select')
},
onTitleInput(val) {
if (!this.isSelected) {
return;
return
}
this.$emit('change', {
key: 'titleConfig.mainTitle',
value: val,
});
},
value: val
})
}
},
components: {
richEditor,
},
};
RichEditor
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.title-wrapper {
padding: 15px;
}
@ -65,7 +63,7 @@ export default {
background-color: #f6f7f9;
box-shadow: 0 0 5px #dedede;
::v-deep .w-e-text-container {
:deep(.w-e-text-container) {
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>
<div class="nav">
<div class="left-group">
<back></back>
<pageTitle :style="{ marginLeft: '30px' }" :title="title"></pageTitle>
<BackPanel></BackPanel>
<TitlePanel :style="{ marginLeft: '30px' }" :title="title"></TitlePanel>
</div>
<div class="center-group">
<pageNav></pageNav>
<NavPanel></NavPanel>
</div>
<div class="right-group">
<history></history>
<save></save>
<publish></publish>
<HistoryPanel></HistoryPanel>
<SavePanel></SavePanel>
<PublishPanel></PublishPanel>
</div>
</div>
</template>
<script>
import back from '../modules/generalModule/back.vue';
import pageTitle from '../modules/generalModule/pageTitle.vue';
import pageNav from '../modules/generalModule/pageNav.vue';
import history from '../modules/contentModule/history.vue';
import save from '../modules/contentModule/save.vue';
import publish from '../modules/contentModule/publish.vue';
import { mapState } from 'vuex';
import { get as _get } from 'lodash-es';
import BackPanel from '../modules/generalModule/BackPanel.vue'
import TitlePanel from '../modules/generalModule/TitlePanel.vue'
import NavPanel from '../modules/generalModule/NavPanel.vue'
import HistoryPanel from '../modules/contentModule/HistoryPanel.vue'
import SavePanel from '../modules/contentModule/SavePanel.vue'
import PublishPanel from '../modules/contentModule/PublishPanel.vue'
import { mapState } from 'vuex'
import { get as _get } from 'lodash-es'
export default {
name: 'navbar',
name: 'ModuleNavbar',
components: {
back,
pageTitle,
pageNav,
history,
save,
publish,
BackPanel,
TitlePanel,
NavPanel,
HistoryPanel,
SavePanel,
PublishPanel
},
data() {
return {};
return {}
},
computed: {
...mapState({
title: (state) => _get(state, 'edit.schema.metaData.title'),
}),
},
};
title: (state) => _get(state, 'edit.schema.metaData.title')
})
}
}
</script>
<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>
<div
class="submit-wrapper"
@click="onClick"
:class="{ isSelected: isSelected }"
>
<el-button
class="submit-btn"
type="primary"
>{{ submitConf.submitTitle }}</el-button
>
<div class="submit-wrapper" @click="onClick" :class="{ isSelected: isSelected }">
<el-button class="submit-btn" type="primary">{{ submitConf.submitTitle }}</el-button>
</div>
</template>
<script>
export default {
name: 'Submit',
name: 'SubmitButton',
data() {
return {};
return {}
},
props: {
submitConf: Object,
isSelected: Boolean,
skinConf: {
type: Object,
required: true,
},
required: true
}
},
methods: {
onClick() {
this.$emit('select');
},
},
};
this.$emit('select')
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.submit-wrapper {
padding: 25px;
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>
<div class="edit-index">
<leftMenu class="left"></leftMenu>
<LeftMenu class="left"></LeftMenu>
<div class="right">
<commonTemplate style="background-color: #f6f7f9">
<navbar class="navbar" slot="nav"></navbar>
<router-view slot="body"></router-view>
</commonTemplate>
<CommonTemplate style="background-color: #f6f7f9">
<template #nav>
<Navbar class="navbar"></Navbar>
</template>
<template #body>
<router-view></router-view>
</template>
</CommonTemplate>
</div>
</div>
</template>
<script>
import commonTemplate from './components/commonTemplate.vue';
import navbar from './components/navbar.vue';
import leftMenu from '@/management/components/leftMenu.vue';
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/src/message.scss'
import LeftMenu from '@/management/components/LeftMenu.vue'
import CommonTemplate from './components/CommonTemplate.vue'
import Navbar from './components/ModuleNavbar.vue'
export default {
name: 'questionEditPage',
components: {
commonTemplate,
navbar,
leftMenu,
CommonTemplate,
Navbar,
LeftMenu
},
async created() {
this.$store.commit('edit/setSurveyId', this.$route.params.id);
this.$store.commit('edit/setSurveyId', this.$route.params.id)
try {
await this.$store.dispatch('edit/init');
await this.$store.dispatch('edit/init')
} catch (error) {
this.$message.error(error.message);
ElMessage.error(error.message)
//
setTimeout(() => {
this.$router.replace({
name: 'survey',
});
}, 1000);
name: 'survey'
})
}, 1000)
}
}
}
},
};
</script>
<style lang="scss" scoped>
.edit-index {
height: 100%;
@ -56,6 +66,7 @@ export default {
padding-left: 80px;
overflow: hidden;
}
.navbar {
border-bottom: 1px solid #e7e9eb;
}

View File

@ -1,5 +1,5 @@
<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-tab-pane label="修改历史" name="daily" class="custom-tab-pane">
<div class="line" v-for="(his, index) in dailyList" :key="index">
@ -16,48 +16,51 @@
</div>
</el-tab-pane>
</el-tabs>
<div class="btn" slot="reference">
<template #reference>
<div class="btn">
<i class="iconfont icon-lishi"></i>
<span class="btn-txt">历史</span>
</div>
</template>
</el-popover>
</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';
import { get as _get } from 'lodash-es';
<script>
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) => ({
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 {
name: 'history',
name: 'HistoryPanel',
computed: {
...mapState({
surveyId: (state) => _get(state, 'edit.surveyId'),
surveyId: (state) => _get(state, 'edit.surveyId')
}),
dailyList() {
return this.dailyHis.map(getItemData);
return this.dailyHis.map(getItemData)
},
publishList() {
return this.publishHis.map(getItemData);
},
return this.publishHis.map(getItemData)
}
},
data() {
return {
dailyHis: [],
publishHis: [],
currentTab: 'daily',
visible: false,
};
visible: false
}
},
watch: {
surveyId: {
@ -67,31 +70,32 @@ export default {
const [dailyHis, publishHis] = await Promise.all([
getSurveyHistory({
surveyId: this.surveyId,
historyType: 'dailyHis',
historyType: 'dailyHis'
}),
getSurveyHistory({
surveyId: this.surveyId,
historyType: 'publishHis',
}),
]);
this.dailyHis = dailyHis.data || [];
this.publishHis = publishHis.data || [];
historyType: 'publishHis'
})
])
this.dailyHis = dailyHis.data || []
this.publishHis = publishHis.data || []
}
}
}
},
},
},
methods: {
onShow() {
this.visible = true;
},
},
};
this.visible = true
}
}
}
</script>
<style lang="scss" scoped>
@import url('@/management/styles/edit-btn.scss');
.custom-tab {
width: 300px;
::v-deep .el-tabs__nav {
:deep(.el-tabs__nav) {
width: 100%;
.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">
{{ saveText }}
</span>
<i class="icon el-icon-loading" v-if="autoSaveStatus === 'saving'"></i>
<i
class="icon succeed el-icon-check"
v-else-if="autoSaveStatus === 'succeed'"
></i>
<i-ep-loading class="icon" v-if="autoSaveStatus === 'saving'" />
<i-ep-check class="icon succeed" v-else-if="autoSaveStatus === 'succeed'" />
</div>
</transition>
</div>
</template>
<script>
import { saveSurvey } from '@/management/api/survey';
import buildData from './buildData';
import { mapState } from 'vuex';
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 {
name: 'save',
components: {},
name: 'SavePanel',
data() {
return {
isSaving: false,
isShowAutoSave: false,
autoSaveStatus: 'succeed',
};
autoSaveStatus: 'succeed'
}
},
computed: {
...mapState({
schemaUpdateTime: (state) => _get(state, 'edit.schemaUpdateTime'),
schemaUpdateTime: (state) => _get(state, 'edit.schemaUpdateTime')
}),
saveText() {
const statusMap = {
saving: '保存中',
succeed: '保存成功',
failed: '保存失败',
};
return statusMap[this.autoSaveStatus];
},
failed: '保存失败'
}
return statusMap[this.autoSaveStatus]
}
},
watch: {
schemaUpdateTime() {
this.triggerAutoSave();
},
this.triggerAutoSave()
}
},
methods: {
triggerAutoSave() {
if (this.autoSaveStatus === 'saving') {
//
setTimeout(() => {
this.triggerAutoSave();
}, 1000);
this.triggerAutoSave()
}, 1000)
} else {
if (this.timer) {
clearTimeout(this.timer);
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
this.autoSaveStatus = 'saving';
this.isShowAutoSave = true;
this.autoSaveStatus = 'saving'
this.isShowAutoSave = true
this.$nextTick(() => {
this.saveData()
.then((res) => {
if (res.code === 200) {
this.autoSaveStatus = 'succeed';
this.autoSaveStatus = 'succeed'
} else {
this.autoSaveStatus = 'failed';
this.autoSaveStatus = 'failed'
}
setTimeout(() => {
this.isShowAutoSave = false;
this.timer = null;
}, 300);
this.isShowAutoSave = false
this.timer = null
}, 300)
})
.catch(() => {
this.timer = null;
this.autoSaveStatus = 'failed';
this.isShowAutoSave = true;
});
});
}, 2000);
this.timer = null
this.autoSaveStatus = 'failed'
this.isShowAutoSave = true
})
})
}, 2000)
}
},
async saveData() {
const saveData = buildData(this.$store.state.edit.schema);
const saveData = buildData(this.$store.state.edit.schema)
if (!saveData.surveyId) {
this.$message.error('未获取到问卷id');
return null;
ElMessage.error('未获取到问卷id')
return null
}
const res = await saveSurvey(saveData);
return res;
const res = await saveSurvey(saveData)
return res
},
async onSave() {
if (this.isSaving) {
return;
return
}
this.isShowAutoSave = false;
this.isShowAutoSave = false
try {
this.isSaving = true;
const res = await this.saveData();
this.isSaving = true
const res = await this.saveData()
if (res.code === 200) {
this.$message.success('保存成功');
ElMessage.success('保存成功')
} else {
this.$message.error(res.errmsg);
ElMessage.error(res.errmsg)
}
} catch (error) {
this.$message.error('保存问卷失败');
ElMessage.error('保存问卷失败')
} finally {
this.isSaving = false;
this.isSaving = false
}
}
}
}
},
},
};
</script>
<style lang="scss" scoped>
@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) {
const surveyId = _get(schema, 'metaData._id');
const surveyId = _get(schema, 'metaData._id')
const configData = _pick(schema, [
'bannerConf',
'baseConf',
'bottomConf',
'skinConf',
'submitConf',
'questionDataList',
]);
'questionDataList'
])
configData.dataConf = {
dataList: configData.questionDataList,
};
delete configData.questionDataList;
dataList: configData.questionDataList
}
delete configData.questionDataList
return {
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>
</div>
</template>
<script>
export default {
name: 'back',
name: 'BackPanel',
methods: {
onBack() {
this.$router.go(-1);
},
},
};
window.open('/survey', '_self')
}
}
}
</script>
<style lang="scss" scoped>
.back-btn {
height: 100%;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,17 @@
<template>
<div class="question-catalog-wrapper">
<draggable :list="renderData" :options="dragOptions" @end="onDragEnd">
<template v-for="(catalogItem, index) in renderData">
<catalogItem
:key="catalogItem.field"
:title="catalogItem.title"
:indexNumber="catalogItem.indexNumber"
:showIndex="catalogItem.showIndex"
<draggable
:list="renderData"
@end="onDragEnd"
itemKey="field"
handle=".draggHandle"
host-class="catalog-item-ghost"
>
<template #item="{ element, index }">
<CatalogItem
:title="element.title"
:indexNumber="element.indexNumber"
:showIndex="element.showIndex"
@select="onSelect(index)"
/>
</template>
@ -15,48 +20,43 @@
</template>
<script>
import draggable from 'vuedraggable';
import catalogItem from './catalogItem';
import { filterQuestionPreviewData } from '@/management/utils/index';
import draggable from 'vuedraggable'
import CatalogItem from './CatalogItem.vue'
import { filterQuestionPreviewData } from '@/management/utils/index'
export default {
name: 'QuestionCatalog',
data() {
return {
dragOptions: {
handle: '.draggHandle',
ghostClass: 'catalog-item-ghost',
dragClass: 'catalog-item-dragging',
},
};
return {}
},
computed: {
questionDataList() {
return this.$store.state.edit.schema.questionDataList;
return this.$store.state.edit.schema.questionDataList
},
renderData() {
return filterQuestionPreviewData(this.questionDataList) || [];
},
return filterQuestionPreviewData(this.questionDataList) || []
}
},
components: {
draggable,
catalogItem,
CatalogItem
},
methods: {
onDragEnd(data) {
const { newIndex, oldIndex } = data;
const { newIndex, oldIndex } = data
this.$store.dispatch('edit/moveQuestion', {
index: oldIndex,
range: newIndex - oldIndex,
});
range: newIndex - oldIndex
})
},
onSelect(index) {
this.$store.commit('edit/setCurrentEditOne', index);
},
},
};
this.$store.commit('edit/setCurrentEditOne', index)
}
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.question-catalog-wrapper {
padding-bottom: 400px; //
.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>
<el-form
class="question-config-form"
size="small"
label-position="left"
label-width="200px"
@submit.native.prevent
@submit.prevent
>
<template v-for="(item, index) in form.formList">
<FormItem
v-if="item.type && !item.hidden && Boolean(register[item.type])"
:key="index"
v-if="
item.type && !item.hidden && Boolean(registerd[item.type])
"
:form-config="item"
:style="item.style"
>
<Component
v-if="Boolean(registerd[item.type])"
v-if="Boolean(register[item.type])"
:is="item.type"
:module-config="form.dataConfig"
:form-config="item"
@ -39,106 +36,95 @@
</div>
</div>
</template>
<script>
import baseConfig from './config/baseConfig';
import baseFormConfig from './config/baseFormConfig';
import FormItem from '@/materials/setters/widgets/FormItem.vue';
import setterLoader from '@/materials/setters/setterLoader';
import {
cloneDeep as _cloneDeep,
isArray as _isArray,
get as _get,
} from 'lodash-es';
import baseConfig from './config/baseConfig'
import baseFormConfig from './config/baseFormConfig'
import FormItem from '@/materials/setters/widgets/FormItem.vue'
import setterLoader from '@/materials/setters/setterLoader'
import { cloneDeep as _cloneDeep, isArray as _isArray, get as _get } from 'lodash-es'
export default {
name: 'QuestionConfig',
name: 'SettingPanel',
components: {
FormItem,
FormItem
},
data() {
return {
formConfigList: [],
registerd: {},
};
register: {}
}
},
methods: {
onFormChange(data) {
this.$store.dispatch('edit/changeSchema', {
key: data.key,
value: data.value,
});
},
value: data.value
})
}
},
computed: {
allSetters() {
const formList = this.formConfigList.map((item) => item.formList).flat();
const formList = this.formConfigList.map((item) => item.formList).flat()
const typeList = formList.map((item) => ({
type: item.type,
path: item.path || item.type,
}));
return typeList;
path: item.path || item.type
}))
return typeList
},
renderData() {
// todo: 1formConfigvalue2dataConfig
const formConfigList = _cloneDeep(this.formConfigList);
const formConfigList = _cloneDeep(this.formConfigList)
return formConfigList.map((form) => {
const dataConfig = {};
const dataConfig = {}
for (const formItem of form.formList) {
const formKey = formItem.key ? formItem.key : formItem.keys;
let formValue;
const formKey = formItem.key ? formItem.key : formItem.keys
let formValue
if (_isArray(formKey)) {
formValue = [];
formValue = []
for (const key of formKey) {
const val = _get(
this.$store.state.edit.schema,
key,
formItem.value
);
formValue.push(val);
dataConfig[key] = val;
const val = _get(this.$store.state.edit.schema, key, formItem.value)
formValue.push(val)
dataConfig[key] = val
}
} else {
formValue = _get(
this.$store.state.edit.schema,
formKey,
formItem.value
);
dataConfig[formKey] = formValue;
formValue = _get(this.$store.state.edit.schema, formKey, formItem.value)
dataConfig[formKey] = formValue
}
formItem.value = formValue;
formItem.value = formValue
}
form.dataConfig = dataConfig
return form
})
}
form.dataConfig = dataConfig;
return form;
});
},
},
async created() {
this.formConfigList = baseConfig.map((item) => {
return {
...item,
formList: item.formList
.map((key) => baseFormConfig[key])
.filter((config) => !!config),
};
});
formList: item.formList.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) {
if (!comp) {
continue;
continue
}
const { type, component, err } = comp;
const { type, component, err } = comp
if (!err) {
const componentName = component.name;
this.$options.components[componentName] = component;
this.$set(this.registerd, type, componentName);
const componentName = component.name
this.$options.components[componentName] = component
this.register[type] = componentName
}
}
}
}
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.question-config {
width: 100%;
min-width: 1080px;
@ -177,7 +163,7 @@ export default {
&:after {
position: absolute;
left: 0;
top: 41px;
top: 42px;
width: 100%;
height: 3px;
background-color: $primary-color;
@ -191,7 +177,7 @@ export default {
padding-top: 15px;
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;
width: 3.4rem;
text-align: left;

View File

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

View File

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

View File

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

View File

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

View File

@ -4,79 +4,22 @@ export default {
keys: ['baseConf.begTime', 'baseConf.endTime'],
label: '答题有效期',
type: 'QuestionTime',
placeholder: 'yyyy-MM-dd hh:mm:ss',
// direction: 'horizon',
placeholder: 'yyyy-MM-dd hh:mm:ss'
},
// 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: {
key: 'baseConf.tLimit',
label: '问卷回收总数',
type: 'InputNumber',
// direction: 'horizon',
tip: '0为无限制此功能用于限制该问卷总提交的数据量。当数据量达到限额时该问卷将不能继续提交',
tipShow: true,
placement: 'top',
min: 0,
min: 0
},
limit_answerTime: {
keys: ['baseConf.answerBegTime', 'baseConf.answerEndTime'],
label: '答题时段',
tip: '问卷仅在指定时间段内可填写',
type: 'QuestionTimeHour',
// direction: 'horizon',
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,
// },
};
placement: 'top'
}
}

View File

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

View File

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

View File

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

View File

@ -2,46 +2,44 @@
<div class="result-config-preview">
<div class="result-page-wrap">
<div class="result-page">
<component
:is="currentEditStatus"
:key="currentEditStatus"
:module-config="moduleConfig"
/>
<component :is="currentEditStatus" :key="currentEditStatus" :module-config="moduleConfig" />
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import success from '../components/success';
import overTime from '../components/overTime';
import { EDIT_STATUS_MAP } from '../enum';
import { get as _get } from 'lodash-es';
import { mapState } from 'vuex'
import SuccessContent from '../components/SuccessContent.vue'
import OverTime from '../components/OverTime.vue'
import { EDIT_STATUS_MAP } from '../enum'
import { get as _get } from 'lodash-es'
export default {
name: 'ResultConfigPreivew',
name: 'PreviewPanel',
props: {},
data() {
return {};
return {}
},
computed: {
...mapState({
currentEditStatus: (state) => state.edit.currentEditStatus,
submitConf: (state) => _get(state, 'edit.schema.submitConf'),
submitConf: (state) => _get(state, 'edit.schema.submitConf')
}),
moduleConfig() {
return {
submitConf: this.submitConf,
};
},
submitConf: this.submitConf
}
}
},
components: {
[EDIT_STATUS_MAP.SUCCESS]: success,
[EDIT_STATUS_MAP.OVERTIME]: overTime,
},
};
[EDIT_STATUS_MAP.SUCCESS]: SuccessContent,
[EDIT_STATUS_MAP.OVERTIME]: OverTime
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.result-config-preview {
width: 100%;
height: 100%;

View File

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

View File

@ -6,9 +6,10 @@
<el-tag
v-for="item in groupList"
:class="[groupName === item.value ? 'current' : '', 'tag']"
type = 'info'
type="info"
:key="item.value"
@click="() => changeGroup(item.value)">
@click="() => changeGroup(item.value)"
>
{{ item.label }}
</el-tag>
</div>
@ -18,17 +19,10 @@
v-for="(banner, bannerIndex) in currentBannerList"
:key="bannerIndex"
>
<img
class="banner-img"
:src="banner.src"
loading="lazy"
@click="changePreset(banner)"
/>
<img class="banner-img" :src="banner.src" loading="lazy" @click="changePreset(banner)" />
</div>
</div>
</div>
</div>
</template>
<script>
@ -36,16 +30,16 @@ import { mapActions } from 'vuex'
import skinPresets from '@/management/config/skinPresets.js'
export default {
name: 'catalogPanel',
name: 'CatalogPanel',
data() {
return {
skinPresets: [],
groupName: 'temp',
};
groupName: 'temp'
}
},
computed: {
bannerList() {
return this.$store?.state?.bannerList || [];
return this.$store?.state?.bannerList || []
},
groupList() {
return Object.keys(this.bannerList).map((key) => {
@ -56,19 +50,21 @@ export default {
})
},
currentBannerList() {
const arr = Object.keys(this.bannerList).map((key) => {
const arr = Object.keys(this.bannerList)
.map((key) => {
return this.bannerList[key]
}).map(data => {
return data.list.map(item => {
item.group = data.key;
return item;
})
.map((data) => {
return data.list.map((item) => {
item.group = data.key
return item
})
})
const allbanner = arr.reduce((acc, curr) => {
return acc.concat(curr);
}, []);
return allbanner.filter(item => {
if(this.groupName === "temp") {
return acc.concat(curr)
}, [])
return allbanner.filter((item) => {
if (this.groupName === 'temp') {
return true
} else {
return item.group === this.groupName
@ -76,12 +72,10 @@ export default {
})
}
},
mounted() {
},
mounted() {},
methods: {
...mapActions({
changeThemePreset: 'edit/changeThemePreset',
changeThemePreset: 'edit/changeThemePreset'
}),
changeGroup(value) {
this.groupName = value
@ -91,19 +85,18 @@ export default {
let presets = {
'bannerConf.bannerConfig.bgImage': banner.src,
'skinConf.themeConf.color': '#FAA600',
'skinConf.backgroundConf.color': '#fff',
'skinConf.backgroundConf.color': '#fff'
}
if (skinPresets[name]) {
presets = Object.assign(presets, skinPresets[name])
}
this.changeThemePreset(presets)
}
},
};
}
}
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
<style lang="scss" scoped>
.tab-box {
width: 300px;
height: 100%;
@ -153,16 +146,16 @@ export default {
}
}
::v-deep .el-collapse-item__header {
:deep(.el-collapse-item__header) {
font-size: 16px;
color: $font-color-title;
border-bottom: none;
}
::v-deep .el-collapse-item__arrow.is-active {
:deep(.el-collapse-item__arrow.is-active) {
right: 0;
}
::v-deep .el-collapse-item__arrow {
:deep(.el-collapse-item__arrow) {
right: 0;
}
}

View File

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

View File

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

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

View File

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

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