feat: vue3 (#103)
This commit is contained in:
parent
2bca93bf12
commit
6771b831e5
15
web/.eslintrc.cjs
Normal file
15
web/.eslintrc.cjs
Normal 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'
|
||||
}
|
||||
}
|
@ -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
1
web/.gitignore
vendored
@ -14,6 +14,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
singleQuote: true, // 使用单引号
|
||||
semi: true, // 不使用分号
|
||||
// trailingComma: 'all', // 在对象和数组末尾加上逗号
|
||||
};
|
8
web/.prettierrc.json
Normal file
8
web/.prettierrc.json
Normal 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
10
web/auto-imports.d.ts
vendored
Normal 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']
|
||||
}
|
@ -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
63
web/components.d.ts
vendored
Normal 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
8
web/env.d.ts
vendored
Normal 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";
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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>
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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'])
|
||||
|
||||
const curValue = ref('')
|
||||
const editorRef = shallowRef()
|
||||
const showToolbar = ref(props.staticToolBar || false)
|
||||
|
||||
const mode = 'simple'
|
||||
|
||||
const toolbarConfig = {
|
||||
toolbarKeys: [
|
||||
'color', // 字体色
|
||||
'bgColor', // 背景色
|
||||
'bold',
|
||||
'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
|
||||
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;
|
||||
|
||||
createEditor({
|
||||
selector: this.$refs.editor,
|
||||
html: this.defaultHtml || this.value || '',
|
||||
config: {
|
||||
onCreated: (editor) => {
|
||||
this.editor = Object.seal(editor);
|
||||
|
||||
if (this.value) {
|
||||
this.setHtml(this.value);
|
||||
}
|
||||
|
||||
this.$refs.toolbar &&
|
||||
createToolbar({
|
||||
editor,
|
||||
selector: this.$refs.toolbar,
|
||||
config: {
|
||||
toolbarKeys: [
|
||||
'color', // 字体色
|
||||
'bgColor', // 背景色
|
||||
'bold',
|
||||
// 'insertImage', // 插入图片
|
||||
// 'video',
|
||||
// 'fontSize', // 字号
|
||||
// 'justify', // 对齐方式
|
||||
'insertLink', // 链接
|
||||
// 'clean',
|
||||
],
|
||||
},
|
||||
mode: 'simple',
|
||||
});
|
||||
},
|
||||
onChange: (editor) => {
|
||||
const editorHtml = editor.getHtml();
|
||||
this.curValue = editorHtml; // 记录当前 html 内容
|
||||
this.$emit('input', editorHtml); // 用于自定义 v-model
|
||||
},
|
||||
onDestroyed: (editor) => {
|
||||
this.$emit('onDestroyed', editor);
|
||||
editor.destroy();
|
||||
},
|
||||
onFocus: (editor) => {
|
||||
this.$emit('onFocus', editor);
|
||||
this.showToolbar = true;
|
||||
},
|
||||
onBlur: (editor) => {
|
||||
const editorHtml = editor.getHtml();
|
||||
this.curValue = editorHtml; // 记录当前 html 内容
|
||||
this.$emit('change', editorHtml);
|
||||
this.$emit('blur', editor);
|
||||
this.showToolbar = false;
|
||||
},
|
||||
// customPaste: (editor, event) => {
|
||||
// let res;
|
||||
// this.$emit('customPaste', editor, event, (val) => {
|
||||
// res = val;
|
||||
// });
|
||||
// return res;
|
||||
// },
|
||||
},
|
||||
content: [],
|
||||
mode: 'simple',
|
||||
});
|
||||
},
|
||||
setHtml(newVal)
|
||||
},
|
||||
};
|
||||
{
|
||||
// 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;
|
||||
|
19
web/src/common/Editor/styles/reset-wangeditor.scss
Normal file
19
web/src/common/Editor/styles/reset-wangeditor.scss
Normal 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;
|
||||
}
|
@ -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(/ /g, '');
|
||||
if (content) return content;
|
||||
const html = transformHtmlTag(text)
|
||||
const content = html.replace(/<[^<>]+>/g, '').replace(/ /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, '<').replace(/>/g, '>');
|
||||
return html.replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\\\n/g, '\\n');
|
||||
.replace(/\\\n/g, '\\n')
|
||||
//.replace(/ /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))
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
@ -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>
|
@ -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;
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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' }
|
||||
},
|
||||
];
|
||||
}
|
||||
]
|
||||
|
@ -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',
|
||||
label: '背景颜色',
|
||||
type: 'ColorPicker',
|
||||
key: 'color',
|
||||
}],
|
||||
formConfigList: [
|
||||
{
|
||||
label: '背景颜色',
|
||||
type: 'ColorPicker',
|
||||
key: 'color'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '主题色',
|
||||
key: 'skinConf.themeConf',
|
||||
formConfigList: [{
|
||||
direction: 'space_between',
|
||||
direction: 'space_between',
|
||||
label: '全局应用',
|
||||
type: 'ColorPicker',
|
||||
key: 'color',
|
||||
}],
|
||||
formConfigList: [
|
||||
{
|
||||
label: '全局应用',
|
||||
type: 'ColorPicker',
|
||||
key: 'color'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'skinConf.contentConf',
|
||||
name: '内容区域',
|
||||
formConfigList: [{
|
||||
direction: 'space_between',
|
||||
label: '内容透明度',
|
||||
type: 'SliderSetter',
|
||||
key: 'opacity',
|
||||
}],
|
||||
formConfigList: [
|
||||
{
|
||||
label: '内容透明度',
|
||||
type: 'SliderSetter',
|
||||
key: 'opacity'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '品牌logo',
|
||||
|
@ -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: '提交失败!'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -1,7 +1,6 @@
|
||||
|
||||
export default {
|
||||
'default-1': {
|
||||
'skinConf.backgroundConf.color': '#90b4fa',
|
||||
'skinConf.themeConf.color': '#FAA600',
|
||||
'skinConf.themeConf.color': '#FAA600'
|
||||
}
|
||||
}
|
16
web/src/management/directive/plainText.ts
Normal file
16
web/src/management/directive/plainText.ts
Normal 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
|
17
web/src/management/directive/safeHtml.ts
Normal file
17
web/src/management/directive/safeHtml.ts
Normal 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
|
12
web/src/management/index.html
Normal file
12
web/src/management/index.html
Normal 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>
|
@ -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')
|
||||
|
@ -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;
|
120
web/src/management/pages/analysis/components/DataTable.vue
Normal file
120
web/src/management/pages/analysis/components/DataTable.vue
Normal 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(/ /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(/ /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>
|
@ -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(/ /g, '') }}
|
||||
</span>
|
||||
<el-popover
|
||||
:ref="scope.column.id"
|
||||
placement="top-start"
|
||||
width="200"
|
||||
trigger="hover"
|
||||
:content="scope.column.label.replace(/ /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>
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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%;
|
@ -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: '活动报名 / 会议报名'
|
||||
}
|
||||
]
|
||||
|
@ -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%;
|
@ -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;
|
@ -1,58 +1,56 @@
|
||||
<template>
|
||||
<div class="title-wrapper" @click="handleClick()">
|
||||
<div class="main-title" :class="{ active: isSelected }" >
|
||||
<richEditor
|
||||
:value="bannerConf?.titleConfig?.mainTitle"
|
||||
<div class="main-title" :class="{ active: isSelected }">
|
||||
<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;
|
||||
}
|
||||
}
|
101
web/src/management/pages/edit/components/MaterialGroup.vue
Normal file
101
web/src/management/pages/edit/components/MaterialGroup.vue
Normal 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>
|
@ -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>
|
182
web/src/management/pages/edit/components/QuestionWrapper.vue
Normal file
182
web/src/management/pages/edit/components/QuestionWrapper.vue
Normal 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>
|
225
web/src/management/pages/edit/components/SetterField.vue
Normal file
225
web/src/management/pages/edit/components/SetterField.vue
Normal 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>
|
@ -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;
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
@ -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">
|
||||
<i class="iconfont icon-lishi"></i>
|
||||
<span class="btn-txt">历史</span>
|
||||
</div>
|
||||
<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 {
|
@ -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>
|
@ -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');
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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%;
|
@ -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' : '']"
|
||||
>
|
||||
<i class="iconfont" :class="[btnItem.icon]"></i>
|
||||
<a :href="href" @click="navigate"><span>{{ btnItem.text }}</span></a>
|
||||
<!-- <span>{{ btnItem.text }}</span> -->
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'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
|
||||
>
|
||||
<!-- <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;
|
||||
@ -67,7 +71,7 @@ export default {
|
||||
color: #92949d;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
a{
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
&.router-link-exact-active {
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
||||
if(themeConf?.color) {
|
||||
root.style.setProperty('--primary-color', themeConf?.color); // 设置主题颜色
|
||||
handler(skinConf) {
|
||||
const { themeConf, backgroundConf, contentConf } = skinConf
|
||||
const root = document.documentElement
|
||||
if (themeConf?.color) {
|
||||
root.style.setProperty('--primary-color', themeConf?.color) // 设置主题颜色
|
||||
}
|
||||
if(backgroundConf?.color) {
|
||||
root.style.setProperty('--primary-background-color', backgroundConf?.color); // 设置背景颜色
|
||||
if (backgroundConf?.color) {
|
||||
root.style.setProperty('--primary-background-color', backgroundConf?.color) // 设置背景颜色
|
||||
}
|
||||
if(contentConf?.opacity) {
|
||||
root.style.setProperty('--opacity', contentConf?.opacity/100); // 设置全局透明度
|
||||
if (contentConf?.opacity) {
|
||||
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>
|
@ -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>
|
@ -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;
|
@ -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 {
|
@ -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>
|
@ -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>
|
@ -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: 1、给formConfig组装value;2、新增dataConfig字段
|
||||
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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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']
|
||||
}
|
||||
]
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const EDIT_STATUS_MAP = {
|
||||
SUCCESS: 'Success',
|
||||
OVERTIME: 'OverTime',
|
||||
};
|
||||
OVERTIME: 'OverTime'
|
||||
}
|
||||
|
@ -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%;
|
@ -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%;
|
||||
@ -62,7 +60,7 @@ export default {
|
||||
overflow-y: auto;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 0.3rem;
|
||||
.result-page{
|
||||
.result-page {
|
||||
background: rgba(255, 255, 255, var(--opacity));
|
||||
display: flex;
|
||||
flex-direction: column;
|
@ -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%;
|
@ -2,14 +2,15 @@
|
||||
<div class="tab-box">
|
||||
<div class="title">主题设置</div>
|
||||
<div class="content">
|
||||
<div class="tag-list">
|
||||
<div class="tag-list">
|
||||
<el-tag
|
||||
v-for="item in groupList"
|
||||
:class="[groupName === item.value ? 'current' : '', 'tag']"
|
||||
type = 'info'
|
||||
type="info"
|
||||
:key="item.value"
|
||||
@click="() => changeGroup(item.value)">
|
||||
{{item.label}}
|
||||
@click="() => changeGroup(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="banner-list-wrapper">
|
||||
@ -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) => {
|
||||
@ -55,20 +49,22 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
currentBannerList () {
|
||||
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;
|
||||
currentBannerList() {
|
||||
const arr = Object.keys(this.bannerList)
|
||||
.map((key) => {
|
||||
return this.bannerList[key]
|
||||
})
|
||||
})
|
||||
const allbanner = arr.reduce((acc, curr) => {
|
||||
return acc.concat(curr);
|
||||
}, []);
|
||||
return allbanner.filter(item => {
|
||||
if(this.groupName === "temp") {
|
||||
.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 true
|
||||
} else {
|
||||
return item.group === this.groupName
|
||||
@ -76,34 +72,31 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
...mapActions({
|
||||
changeThemePreset: 'edit/changeThemePreset',
|
||||
changeThemePreset: 'edit/changeThemePreset'
|
||||
}),
|
||||
changeGroup(value) {
|
||||
this.groupName = value
|
||||
},
|
||||
changePreset(banner) {
|
||||
changePreset(banner) {
|
||||
const name = banner.group + '-' + banner.title
|
||||
let presets = {
|
||||
'bannerConf.bannerConfig.bgImage': banner.src,
|
||||
'skinConf.themeConf.color': '#FAA600',
|
||||
'skinConf.backgroundConf.color': '#fff',
|
||||
'skinConf.backgroundConf.color': '#fff'
|
||||
}
|
||||
if(skinPresets[name]){
|
||||
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%;
|
||||
@ -120,16 +113,16 @@ export default {
|
||||
// background: #f9fafc;
|
||||
border-bottom: 1px solid #edeffc;
|
||||
}
|
||||
.content{
|
||||
.content {
|
||||
padding: 12px;
|
||||
}
|
||||
.tag-list{
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.tag{
|
||||
.tag {
|
||||
margin: 5px 8px;
|
||||
cursor: pointer;
|
||||
&.current{
|
||||
&.current {
|
||||
color: $primary-color;
|
||||
background-color: $primary-bg-color;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
if(themeConf?.color) {
|
||||
root.style.setProperty('--primary-color', themeConf?.color); // 设置主题颜色
|
||||
handler(skinConf) {
|
||||
const { themeConf, backgroundConf, contentConf } = skinConf
|
||||
const root = document.documentElement
|
||||
if (themeConf?.color) {
|
||||
root.style.setProperty('--primary-color', themeConf?.color) // 设置主题颜色
|
||||
}
|
||||
if(backgroundConf?.color) {
|
||||
root.style.setProperty('--primary-background-color', backgroundConf?.color); // 设置背景颜色
|
||||
if (backgroundConf?.color) {
|
||||
root.style.setProperty('--primary-background-color', backgroundConf?.color) // 设置背景颜色
|
||||
}
|
||||
if(contentConf?.opacity.toString()) {
|
||||
root.style.setProperty('--opacity', contentConf?.opacity/100); // 设置全局透明度
|
||||
if (contentConf?.opacity.toString()) {
|
||||
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>
|
||||
@ -151,7 +140,7 @@ export default {
|
||||
.box {
|
||||
background-color: var(--primary-background-color);
|
||||
position: relative;
|
||||
.mask{
|
||||
.mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
@ -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;
|
||||
onFormChange(data, collapse) {
|
||||
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%;
|
||||
@ -73,26 +74,25 @@ export default {
|
||||
// background: #f9fafc;
|
||||
border-bottom: 1px solid #edeffc;
|
||||
}
|
||||
.setter-content{
|
||||
.setter-content {
|
||||
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;
|
||||
.el-collapse-item__content {
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.config-form{
|
||||
padding: 0!important;
|
||||
.config-form {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
.no-select-question {
|
36
web/src/management/pages/edit/pages/EditPage.vue
Normal file
36
web/src/management/pages/edit/pages/EditPage.vue
Normal 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>
|
@ -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%;
|
@ -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>
|
37
web/src/management/pages/edit/pages/skin/ContentPage.vue
Normal file
37
web/src/management/pages/edit/pages/skin/ContentPage.vue
Normal 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>
|
30
web/src/management/pages/edit/pages/skin/ResultPage.vue
Normal file
30
web/src/management/pages/edit/pages/skin/ResultPage.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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,24 +25,24 @@ export default {
|
||||
text: '内容页',
|
||||
router: 'QuestionSkinSetting',
|
||||
key: 'skinsettings',
|
||||
next: true,
|
||||
next: true
|
||||
},
|
||||
{
|
||||
text: '结果页',
|
||||
router: 'QuestionEditResultConfig',
|
||||
key: 'status',
|
||||
},
|
||||
],
|
||||
};
|
||||
key: 'status'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeRouter: {
|
||||
handler (val) {
|
||||
this.$router.push({ name: val})
|
||||
handler(val) {
|
||||
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>
|
||||
|
@ -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>
|
@ -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 {
|
@ -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">
|
||||
<el-button type="primary" class="save-btn" @click="onSave">{{
|
||||
type === QOP_MAP.EDIT ? '保存' : '确定'
|
||||
}}</el-button>
|
||||
</div>
|
||||
<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);
|
||||
this.current = this.getCurrent(val)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
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>
|
@ -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;
|
15
web/src/management/pages/list/components/StaticIcon.vue
Normal file
15
web/src/management/pages/list/components/StaticIcon.vue
Normal 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>
|
@ -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
Loading…
Reference in New Issue
Block a user