Feature/vue3 (#98)
* wip: vue3 * fix: dev时C端路由被B端router污染 * wip: debugger material * wip: render widgets * wip:回滚editOption * c端抽离编辑态组件 * wip * 事件调通 * wip * feat: 可以正常提交 * fix: typo * fix: 答题进度条 * fix: form 与 input 事件冲突 * 优化field管理 * 优化代码 * update * 标题组件 * fix: 组件操作图标样式 --------- Co-authored-by: taoshaung <taoshaung@didiglobal.com>
@ -1,20 +1,15 @@
|
||||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: false,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
root: true,
|
||||
'extends': [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting'
|
||||
],
|
||||
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'],
|
||||
},
|
||||
};
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
}
|
||||
|
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
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
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,53 @@
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.4.0",
|
||||
"@vue/babel-preset-jsx": "^1.4.0",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@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.5.5",
|
||||
"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"
|
||||
"@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-element-plus": "^0.8.0",
|
||||
"vite": "^5.1.4",
|
||||
"vite-plugin-virtual-mpa": "^1.10.1",
|
||||
"vue-tsc": "^1.8.27"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.21.0",
|
||||
|
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 148 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 151 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 137 KiB |
Before Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 153 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 115 KiB |
@ -1,10 +1,10 @@
|
||||
<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: {
|
||||
|
@ -1,118 +1,111 @@
|
||||
<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 '@/management/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;
|
||||
@ -120,7 +113,10 @@ export default {
|
||||
// min-height: 45px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
.static-toolbar {
|
||||
border-bottom: 1px solid #dedede;
|
||||
}
|
||||
.dynamic-toolbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -44px;
|
||||
|
@ -3,11 +3,13 @@
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import url('./styles/icon.scss');
|
||||
@import url('../materials/questions/common/css/icon.scss');
|
||||
|
@ -1,4 +1,4 @@
|
||||
import axios from './base';
|
||||
import axios from './base'
|
||||
|
||||
export const getRecycleList = (data) => {
|
||||
return axios.get('/survey/dataStatistic/dataTable', {
|
||||
@ -6,5 +6,5 @@ export const getRecycleList = (data) => {
|
||||
pageSize: 10,
|
||||
...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,49 @@
|
||||
import axios from 'axios';
|
||||
import store from '@/management/store/index';
|
||||
import router from '@/management/router/index';
|
||||
import { get as _get } from 'lodash-es';
|
||||
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请求出错');
|
||||
throw new Error('http请求出错')
|
||||
}
|
||||
const res = response.data;
|
||||
const res = response.data
|
||||
if (res.code === 403) {
|
||||
router.replace({
|
||||
name: 'login',
|
||||
});
|
||||
return res;
|
||||
})
|
||||
return res
|
||||
} else {
|
||||
return res;
|
||||
return res
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
throw new Error(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');
|
||||
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 = {}
|
||||
}
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config;
|
||||
});
|
||||
return config
|
||||
})
|
||||
|
||||
export default instance;
|
||||
export default instance
|
||||
|
||||
export const CODE_MAP = {
|
||||
SUCCESS: 200,
|
||||
ERROR: 500,
|
||||
NOTAUTH: 403,
|
||||
};
|
||||
}
|
||||
|
@ -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', {
|
||||
@ -8,30 +8,30 @@ export const getSurveyList = ({ curPage, filter, order }) => {
|
||||
filter,
|
||||
order,
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
export const getSurveyById = (id) => {
|
||||
return axios.get('/survey/getSurvey', {
|
||||
params: {
|
||||
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,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
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', {
|
||||
@ -39,15 +39,15 @@ export const getSurveyHistory = ({ surveyId, historyType }) => {
|
||||
surveyId,
|
||||
historyType,
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteSurvey = (surveyId) => {
|
||||
return axios.post('/survey/deleteSurvey', {
|
||||
surveyId,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSurvey = (data) => {
|
||||
return axios.post('/survey/updateMeta', data);
|
||||
};
|
||||
return axios.post('/survey/updateMeta', data)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<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>
|
||||
|
||||
@ -15,7 +15,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
|
@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logo from './logo.vue';
|
||||
import logo from './logo.vue'
|
||||
|
||||
export default {
|
||||
name: 'leftMenu',
|
||||
@ -49,9 +49,9 @@ export default {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -3,6 +3,7 @@
|
||||
<img src="/imgs/s-logo.webp" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'logoIcon',
|
||||
@ -10,11 +11,12 @@ export default {
|
||||
toHomePage() {
|
||||
this.$router.push({
|
||||
name: 'survey',
|
||||
});
|
||||
})
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar-main-logo {
|
||||
width: 80px;
|
||||
|
44
web/src/management/config/defaultSurveyConfig.js
Normal file
@ -0,0 +1,44 @@
|
||||
export default {
|
||||
metaData: null,
|
||||
bannerConf: {
|
||||
titleConfig: {
|
||||
mainTitle: '<h3 style="text-align: center">欢迎填写问卷</h3>',
|
||||
subTitle: `<p>为了给您提供更好的服务,希望您能抽出几分钟时间,将您的感受和建议告诉我们,<span style="color: rgb(204, 0, 0)">期待您的参与!</span></p>`,
|
||||
applyTitle: '',
|
||||
},
|
||||
bannerConfig: {
|
||||
bgImage: '',
|
||||
bgImageAllowJump: false,
|
||||
bgImageJumpLink: '',
|
||||
videoLink: '',
|
||||
postImg: '',
|
||||
},
|
||||
},
|
||||
bottomConf: {
|
||||
logoImage: '',
|
||||
logoImageWidth: '28%',
|
||||
},
|
||||
skinConf: {
|
||||
skinColor: '#4a4c5b',
|
||||
inputBgColor: '#ffffff',
|
||||
},
|
||||
baseConf: {
|
||||
begTime: '',
|
||||
endTime: '',
|
||||
language: 'chinese',
|
||||
showVoteProcess: 'allow',
|
||||
tLimit: 0,
|
||||
answerBegTime: '',
|
||||
answerEndTime: '',
|
||||
answerLimitTime: 0,
|
||||
},
|
||||
submitConf: {
|
||||
submitTitle: '',
|
||||
msgContent: {},
|
||||
confirmAgain: {
|
||||
is_again: true,
|
||||
},
|
||||
link: '',
|
||||
},
|
||||
questionDataList: [],
|
||||
};
|
@ -74,4 +74,4 @@ export const defaultQuestionConfig = {
|
||||
value: 500,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ const menuItems = {
|
||||
icon: 'tixing-toupiao',
|
||||
title: '投票',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const menuGroup = [
|
||||
{
|
||||
@ -73,15 +73,13 @@ const menuGroup = [
|
||||
'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,3 +1,4 @@
|
||||
|
||||
export default [
|
||||
{
|
||||
label: '顶部图片地址',
|
||||
@ -37,5 +38,22 @@ export default [
|
||||
relyFunc: (data) => {
|
||||
return !!data?.bgImageAllowJump;
|
||||
},
|
||||
content: [
|
||||
{
|
||||
label: '图片支持点击',
|
||||
type: 'CustomedSwitch',
|
||||
direction: 'space_between',
|
||||
key: 'bannerConfig.bgImageAllowJump',
|
||||
},
|
||||
{
|
||||
label: '跳转链接',
|
||||
type: 'Input',
|
||||
direction: 'horizon',
|
||||
key: 'bannerConfig.bgImageJumpLink',
|
||||
relyFunc: (data) => {
|
||||
return !!data?.bannerConfig?.bgImageAllowJump
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
]
|
||||
|
@ -15,4 +15,4 @@ export default [
|
||||
direction: 'horizon',
|
||||
labelStyle: { width: '120px' }
|
||||
},
|
||||
];
|
||||
]
|
||||
|
@ -68,4 +68,4 @@ export default [
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
]
|
||||
|
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
@ -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
|
@ -4,11 +4,10 @@
|
||||
<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>
|
||||
<link rel="icon" href="/imgs/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<!-- <script type="module" src="./main.ts"></script> -->
|
||||
</body>
|
||||
</html>
|
@ -1,36 +0,0 @@
|
||||
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';
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
Vue.use(ElementUI);
|
||||
|
||||
const safeHtml = function (el, binding) {
|
||||
const res = filterXSS(binding.value);
|
||||
el.innerHTML = res;
|
||||
};
|
||||
|
||||
const plainText = function (el, binding) {
|
||||
const text = cleanRichText(binding.value);
|
||||
el.innerText = text;
|
||||
};
|
||||
|
||||
Vue.directive('safe-html', {
|
||||
inserted: safeHtml,
|
||||
componentUpdated: safeHtml,
|
||||
});
|
||||
|
||||
Vue.directive('plain-text', {
|
||||
inserted: plainText,
|
||||
componentUpdated: plainText,
|
||||
});
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: (h) => h(App),
|
||||
}).$mount('#app');
|
23
web/src/management/main.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
import store from './store'
|
||||
import './styles/element-variables.scss'
|
||||
import ElementPlus from 'element-plus'
|
||||
// 手动引入,避免ElMessage动态加载导致的el主题覆盖自定义主题
|
||||
import 'element-plus/theme-chalk/el-message.css'
|
||||
import 'element-plus/theme-chalk/el-message-box.css'
|
||||
import plainText from './directive/plainText'
|
||||
import safeHtml from './directive/safeHtml'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(store)
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { size: 'default'})
|
||||
|
||||
app.use(plainText)
|
||||
app.use(safeHtml)
|
||||
|
||||
app.mount('#app')
|
@ -18,7 +18,7 @@
|
||||
:label="cleanRichText(item.title)"
|
||||
minWidth="200"
|
||||
>
|
||||
<template slot="header" slot-scope="scope">
|
||||
<template #header="scope">
|
||||
<div class="table-row-cell">
|
||||
<span slot="reference" v-popover="scope.column.id">
|
||||
{{ scope.column.label.replace(/ /g, '') }}
|
||||
@ -33,7 +33,7 @@
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<template #default="scope">
|
||||
<span
|
||||
slot="reference"
|
||||
class="table-row-cell"
|
||||
@ -54,13 +54,14 @@
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { cleanRichText } from '@/common/xss';
|
||||
import { cleanRichText } from '@/common/xss'
|
||||
|
||||
export default {
|
||||
name: 'DataTable',
|
||||
data() {
|
||||
return {};
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
mainTableLoading: Boolean,
|
||||
@ -75,8 +76,9 @@ export default {
|
||||
return content === 0 ? 0 : (content || '未知')
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.data-table-wrapper {
|
||||
position: relative;
|
||||
@ -89,7 +91,7 @@ export default {
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
::v-deep .el-table__header {
|
||||
:deep(.el-table__header) {
|
||||
width: 100%;
|
||||
.thead-cell .el-table__cell {
|
||||
.cell {
|
||||
|
@ -6,7 +6,7 @@
|
||||
<h2 class="data-list">数据列表</h2>
|
||||
<div class="menus">
|
||||
<el-switch
|
||||
:value="isShowOriginData"
|
||||
:model-value="isShowOriginData"
|
||||
active-text="是否展示原数据"
|
||||
@input="onIsShowOriginChange"
|
||||
>
|
||||
@ -36,10 +36,10 @@
|
||||
</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 DataTable from './components/table.vue'
|
||||
import empty from '@/management/components/empty.vue'
|
||||
import leftMenu from '@/management/components/leftMenu.vue'
|
||||
import { getRecycleList } from '@/management/api/analysis'
|
||||
|
||||
export default {
|
||||
name: 'analysisPage',
|
||||
@ -59,71 +59,71 @@ export default {
|
||||
currentPage: 1,
|
||||
isShowOriginData: false,
|
||||
tmpIsShowOriginData: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
created() {
|
||||
this.init();
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
if (!this.$route.params.id) {
|
||||
this.$message.error('没有传入问卷参数~');
|
||||
return;
|
||||
this.$message.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还没改变,暂存了一个字段
|
||||
});
|
||||
})
|
||||
|
||||
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('查询回收数据失败,请重试');
|
||||
this.$message.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,
|
||||
});
|
||||
})
|
||||
|
||||
if (headItem.othersCode?.length) {
|
||||
headItem.othersCode.forEach((item) => {
|
||||
head.push({
|
||||
field: item.code,
|
||||
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
|
||||
},
|
||||
},
|
||||
|
||||
@ -132,7 +132,7 @@ export default {
|
||||
empty,
|
||||
leftMenu,
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -166,7 +166,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;
|
||||
|
@ -14,14 +14,12 @@
|
||||
<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="请输入备注"
|
||||
@ -32,7 +30,6 @@
|
||||
<el-button
|
||||
class="create-btn"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="submit"
|
||||
:loading="!canSubmit"
|
||||
>
|
||||
@ -42,9 +39,10 @@
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SURVEY_TYPE_LIST } from '../types';
|
||||
import { createSurvey } from '@/management/api/survey';
|
||||
import { SURVEY_TYPE_LIST } from '../types'
|
||||
import { createSurvey } from '@/management/api/survey'
|
||||
|
||||
export default {
|
||||
name: 'CreateForm',
|
||||
@ -64,55 +62,56 @@ export default {
|
||||
title: '问卷调研',
|
||||
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;
|
||||
?.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,
|
||||
});
|
||||
})
|
||||
if (res.code === 200 && res?.data?.id) {
|
||||
const id = res.data.id;
|
||||
const id = res.data.id
|
||||
this.$router.push({
|
||||
name: 'QuestionEditIndex',
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
})
|
||||
} else {
|
||||
this.$message.error(res.errmsg || '创建失败');
|
||||
this.$message.error(res.errmsg || '创建失败')
|
||||
}
|
||||
|
||||
this.canSubmit = true;
|
||||
});
|
||||
this.canSubmit = true
|
||||
})
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.right-side {
|
||||
width: 538px;
|
||||
@ -142,7 +141,7 @@ export default {
|
||||
border: unset;
|
||||
color: white;
|
||||
|
||||
::v-deep span {
|
||||
:deep(span) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
@ -20,20 +21,21 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
img: '/imgs/s-logo.webp',
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toHomePage() {
|
||||
this.$router.replace({
|
||||
name: 'survey',
|
||||
});
|
||||
})
|
||||
},
|
||||
onBack() {
|
||||
this.$router.go(-1);
|
||||
this.$router.go(-1)
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.nav-header {
|
||||
z-index: 99;
|
||||
|
@ -23,10 +23,11 @@
|
||||
</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',
|
||||
@ -41,17 +42,18 @@ export 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>
|
||||
.left-side {
|
||||
position: relative;
|
||||
|
@ -7,9 +7,10 @@
|
||||
<create-form :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',
|
||||
@ -20,15 +21,16 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
selectType: 'normal',
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelectTypeChange(selectType) {
|
||||
this.selectType = selectType;
|
||||
this.selectType = selectType
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.new {
|
||||
position: relative;
|
||||
|
@ -23,4 +23,4 @@ export const SURVEY_TYPE_LIST = [
|
||||
img: '/imgs/create/register-icon.webp',
|
||||
desc: '活动报名 / 会议报名',
|
||||
},
|
||||
];
|
||||
]
|
||||
|
@ -1,29 +1,35 @@
|
||||
<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, useAttrs, getCurrentInstance } from 'vue'
|
||||
|
||||
const slots = useSlots()
|
||||
const attrs = useAttrs()
|
||||
const current = getCurrentInstance()
|
||||
window.vm = current
|
||||
console.log('current',current)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
width: 100%;
|
||||
|
@ -14,6 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LogoPreview',
|
||||
@ -25,21 +26,22 @@ export default {
|
||||
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;
|
||||
|
@ -2,19 +2,20 @@
|
||||
<div class="title-wrapper" @click="handleClick()">
|
||||
<div class="main-title" :class="{ active: isSelected }" >
|
||||
<richEditor
|
||||
:value="bannerConf?.titleConfig?.mainTitle"
|
||||
:modelValue="bannerConf?.titleConfig?.mainTitle"
|
||||
@input="onTitleInput"
|
||||
></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: {
|
||||
@ -31,27 +32,24 @@ export default {
|
||||
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,
|
||||
});
|
||||
})
|
||||
},
|
||||
},
|
||||
components: {
|
||||
richEditor,
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/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;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,50 @@
|
||||
<template>
|
||||
<draggable
|
||||
:list="renderData"
|
||||
:options="dragOptions"
|
||||
: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"
|
||||
@changeSeq="handleChangeSeq"
|
||||
>
|
||||
<QuestionContainer
|
||||
v-bind="$attrs"
|
||||
:type="element.type"
|
||||
:moduleConfig="element"
|
||||
:indexNumber="element.indexNumber"
|
||||
:isSelected="currentEditOne === index"
|
||||
:readonly="true"
|
||||
@select="handleSelect"
|
||||
></QuestionContainer>
|
||||
</questionWrapper>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<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';
|
||||
import { computed, defineComponent, ref, getCurrentInstance, h } 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 },
|
||||
components: {
|
||||
draggable,
|
||||
questionWrapper,
|
||||
QuestionContainer,
|
||||
},
|
||||
props: {
|
||||
currentEditOne: {
|
||||
type: [Number, String],
|
||||
@ -16,41 +53,40 @@ export default defineComponent({
|
||||
questionDataList: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
return []
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
setup(props, { emit }) {
|
||||
const renderData = computed(() => {
|
||||
return filterQuestionPreviewData(props.questionDataList);
|
||||
});
|
||||
return filterQuestionPreviewData(props.questionDataList)
|
||||
})
|
||||
const handleSelect = (index) => {
|
||||
emit('select', index);
|
||||
};
|
||||
emit('select', index)
|
||||
}
|
||||
const handleChangeSeq = (data) => {
|
||||
emit('changeSeq', data);
|
||||
};
|
||||
emit('changeSeq', data)
|
||||
}
|
||||
|
||||
const isMoving = ref(false);
|
||||
const isMoving = ref(false)
|
||||
|
||||
const checkMove = () => {
|
||||
isMoving.value = true;
|
||||
};
|
||||
isMoving.value = true
|
||||
}
|
||||
|
||||
const checkEnd = ({ oldIndex, newIndex }) => {
|
||||
emit('changeSeq', {
|
||||
type: 'move',
|
||||
index: oldIndex,
|
||||
range: newIndex - oldIndex,
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const getQuestionRefByField = (field) => {
|
||||
return instance?.proxy?.$refs[`questionWrapper-${field}`] || null;
|
||||
};
|
||||
return instance?.proxy?.$refs[`questionWrapper-${field}`] || null
|
||||
}
|
||||
|
||||
return {
|
||||
renderData,
|
||||
@ -68,54 +104,7 @@ export default defineComponent({
|
||||
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>
|
||||
|
@ -16,14 +16,14 @@
|
||||
</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 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'
|
||||
|
||||
export default {
|
||||
name: 'navbar',
|
||||
@ -36,14 +36,14 @@ export default {
|
||||
publish,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
title: (state) => _get(state, 'edit.schema.metaData.title'),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="jsx">
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
@ -6,6 +6,7 @@ import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
} from 'vue';
|
||||
import { Rank, Top, Bottom, CopyDocument, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'QuestionWrapper',
|
||||
@ -119,6 +120,7 @@ export default defineComponent({
|
||||
// })
|
||||
// state.isHover = false
|
||||
// }
|
||||
const onMove = () => {}
|
||||
return {
|
||||
...toRefs(state),
|
||||
itemClass,
|
||||
@ -127,6 +129,7 @@ export default defineComponent({
|
||||
showDown,
|
||||
showCopy,
|
||||
onCopy,
|
||||
onMove,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onDelete,
|
||||
@ -148,36 +151,46 @@ export default defineComponent({
|
||||
onClick={this.clickFormItem}
|
||||
>
|
||||
{this.moduleConfig.type !== 'section' && (
|
||||
<div>{this.$slots.default}</div>
|
||||
<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>
|
||||
class="item"
|
||||
onClickPrevent={this.onMove}
|
||||
>
|
||||
<el-icon><Rank /></el-icon>
|
||||
</div>
|
||||
{showUp && (
|
||||
<div
|
||||
class="item iconfont icon-shangyi"
|
||||
vOn:click_stop_prevent={this.onMoveUp}
|
||||
></div>
|
||||
class="item"
|
||||
onClickPrevent={this.onMoveUp}
|
||||
>
|
||||
<el-icon><Top /></el-icon>
|
||||
</div>
|
||||
)}
|
||||
{showDown && (
|
||||
<div
|
||||
class="item iconfont icon-xiayi"
|
||||
vOn:click_stop_prevent={this.onMoveDown}
|
||||
></div>
|
||||
class="item"
|
||||
onClickPrevent={this.onMoveDown}
|
||||
>
|
||||
<el-icon><Bottom /></el-icon>
|
||||
</div>
|
||||
)}
|
||||
{showCopy && (
|
||||
<div
|
||||
class="item copy iconfont icon-fuzhi"
|
||||
vOn:click_stop_prevent={this.onCopy}
|
||||
></div>
|
||||
class="item"
|
||||
onClickPrevent={this.onCopy}
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
class="item iconfont icon-shanchu"
|
||||
vOn:click_stop_prevent={this.onDelete}
|
||||
></div>
|
||||
class="item"
|
||||
onClickPrevent={this.onDelete}
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -193,9 +206,9 @@ export default defineComponent({
|
||||
&.spliter {
|
||||
border-bottom: 0.12rem solid $spliter-color;
|
||||
}
|
||||
&:last-child{
|
||||
border: none;
|
||||
}
|
||||
// &:last-child{
|
||||
// border: none;
|
||||
// }
|
||||
.editor {
|
||||
display: flex;
|
||||
font-size: 0.32rem;
|
||||
@ -276,8 +289,10 @@ export default defineComponent({
|
||||
display: none;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 5px;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
|
@ -1,14 +1,13 @@
|
||||
<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-for="(item, index) in formFieldData" :key="item.key + index">
|
||||
<FormItem class="form-item" :form-config="item">
|
||||
<template v-if="item.type === 'Customed'">
|
||||
<SettersField
|
||||
:key="index"
|
||||
@ -32,35 +31,32 @@
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
get as _get,
|
||||
pick as _pick,
|
||||
isFunction as _isFunction,
|
||||
} from 'lodash-es';
|
||||
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 FormItem from '@/materials/setters/widgets/FormItem.vue'
|
||||
import setterLoader from '@/materials/setters/setterLoader'
|
||||
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant';
|
||||
import { FORM_CHANGE_EVENT_KEY } from '@/materials/setters/constant'
|
||||
|
||||
const formatValue = ({ item, moduleConfig }) => {
|
||||
if (_isFunction(item.valueAdapter)) {
|
||||
const value = item.valueAdapter({ moduleConfig });
|
||||
return value;
|
||||
const value = item.valueAdapter({ moduleConfig })
|
||||
return value
|
||||
} else {
|
||||
const { key, keys } = item;
|
||||
const { key, keys } = item
|
||||
|
||||
let result = null;
|
||||
let result = null
|
||||
if (key) {
|
||||
result = _get(moduleConfig, key, item.value);
|
||||
result = _get(moduleConfig, key, item.value)
|
||||
}
|
||||
if (keys) {
|
||||
result = _pick(moduleConfig, keys);
|
||||
result = _pick(moduleConfig, keys)
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'SettersField',
|
||||
@ -79,7 +75,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
registerd: {},
|
||||
};
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FormItem,
|
||||
@ -89,25 +85,25 @@ export default {
|
||||
return this.formConfigList
|
||||
.filter((item) => {
|
||||
if (!item.type) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (item.type !== 'Customed' && !this.registerd[item.type]) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (item.hidden) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (_isFunction(item.relyFunc)) {
|
||||
return item.relyFunc(this.moduleConfig);
|
||||
return item.relyFunc(this.moduleConfig)
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
})
|
||||
.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
value: formatValue({ item, moduleConfig: this.moduleConfig }),
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@ -116,61 +112,62 @@ export default {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (!newVal || !newVal.length) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
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;
|
||||
const componentName = component.name
|
||||
if (!this.$options.components) {
|
||||
this.$options.components = {};
|
||||
this.$options.components = {}
|
||||
}
|
||||
this.$options.components[componentName] = component;
|
||||
this.$set(this.registerd, type, componentName);
|
||||
this.$options.components[componentName] = component
|
||||
this.registerd[type] = componentName
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(err)
|
||||
}
|
||||
},
|
||||
|
||||
onFormChange(data, formConfig) {
|
||||
if (_isFunction(formConfig?.setterAdapter)) {
|
||||
const resultData = formConfig.setterAdapter(data);
|
||||
const resultData = formConfig.setterAdapter(data)
|
||||
if (Array.isArray(resultData)) {
|
||||
resultData.forEach((item) => {
|
||||
this.$emit(FORM_CHANGE_EVENT_KEY, item);
|
||||
});
|
||||
this.$emit(FORM_CHANGE_EVENT_KEY, item)
|
||||
})
|
||||
} else {
|
||||
this.$emit(FORM_CHANGE_EVENT_KEY, resultData);
|
||||
this.$emit(FORM_CHANGE_EVENT_KEY, resultData)
|
||||
}
|
||||
} else {
|
||||
this.$emit(FORM_CHANGE_EVENT_KEY, data);
|
||||
this.$emit(FORM_CHANGE_EVENT_KEY, data)
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.config-form {
|
||||
padding: 15px 0;
|
||||
@ -178,11 +175,11 @@ export default {
|
||||
.nps-customed-config {
|
||||
.el-form-item {
|
||||
margin-right: 0px;
|
||||
::v-deep .el-form-item__label {
|
||||
:deep(.el-form-item__label) {
|
||||
width: 70px !important;
|
||||
margin-right: 8px;
|
||||
}
|
||||
::v-deep .el-input__inner {
|
||||
:deep(.el-input__inner) {
|
||||
width: 234px;
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,12 @@
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Submit',
|
||||
data() {
|
||||
return {};
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
submitConf: Object,
|
||||
@ -27,11 +28,12 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('select');
|
||||
this.$emit('select')
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.submit-wrapper {
|
||||
padding: 25px;
|
||||
|
@ -3,17 +3,21 @@
|
||||
<leftMenu class="left"></leftMenu>
|
||||
<div class="right">
|
||||
<commonTemplate style="background-color: #f6f7f9">
|
||||
<navbar class="navbar" slot="nav"></navbar>
|
||||
<router-view slot="body"></router-view>
|
||||
<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 commonTemplate from './components/commonTemplate.vue'
|
||||
import navbar from './components/navbar.vue'
|
||||
import leftMenu from '@/management/components/leftMenu.vue'
|
||||
export default {
|
||||
name: 'questionEditPage',
|
||||
components: {
|
||||
@ -22,21 +26,23 @@ export default {
|
||||
leftMenu,
|
||||
},
|
||||
async created() {
|
||||
this.$store.commit('edit/setSurveyId', this.$route.params.id);
|
||||
this.$store.commit('edit/setSurveyId', this.$route.params.id)
|
||||
this.$store.dispatch('getBannerData')
|
||||
try {
|
||||
await this.$store.dispatch('edit/init');
|
||||
await this.$store.dispatch('edit/init')
|
||||
} catch (error) {
|
||||
this.$message.error(error.message);
|
||||
this.$message.error(error.message)
|
||||
// 自动跳转回列表页
|
||||
setTimeout(() => {
|
||||
this.$router.replace({
|
||||
name: 'survey',
|
||||
});
|
||||
}, 1000);
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-index {
|
||||
height: 100%;
|
||||
@ -56,6 +62,7 @@ export default {
|
||||
padding-left: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-bottom: 1px solid #e7e9eb;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
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',
|
||||
@ -10,13 +10,13 @@ export default function (schema) {
|
||||
'skinConf',
|
||||
'submitConf',
|
||||
'questionDataList',
|
||||
]);
|
||||
])
|
||||
configData.dataConf = {
|
||||
dataList: configData.questionDataList,
|
||||
};
|
||||
delete configData.questionDataList;
|
||||
}
|
||||
delete configData.questionDataList
|
||||
return {
|
||||
surveyId,
|
||||
configData,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -22,21 +22,22 @@
|
||||
</div>
|
||||
</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'),
|
||||
});
|
||||
})
|
||||
|
||||
export default {
|
||||
name: 'history',
|
||||
@ -45,10 +46,10 @@ export default {
|
||||
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() {
|
||||
@ -57,7 +58,7 @@ export default {
|
||||
publishHis: [],
|
||||
currentTab: 'daily',
|
||||
visible: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
surveyId: {
|
||||
@ -73,25 +74,26 @@ export default {
|
||||
surveyId: this.surveyId,
|
||||
historyType: 'publishHis',
|
||||
}),
|
||||
]);
|
||||
this.dailyHis = dailyHis.data || [];
|
||||
this.publishHis = publishHis.data || [];
|
||||
])
|
||||
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 {
|
||||
|
@ -8,17 +8,18 @@
|
||||
发布
|
||||
</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';
|
||||
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({
|
||||
@ -27,40 +28,41 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async onPublish() {
|
||||
const saveData = buildData(this.$store.state.edit.schema);
|
||||
const saveData = buildData(this.$store.state.edit.schema)
|
||||
if (!saveData.surveyId) {
|
||||
this.$message.error('未获取到问卷id');
|
||||
return;
|
||||
this.$message.error('未获取到问卷id')
|
||||
return
|
||||
}
|
||||
if (this.isPublishing) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.isPublishing = true;
|
||||
const saveRes = await saveSurvey(saveData);
|
||||
this.isPublishing = true
|
||||
const saveRes = await saveSurvey(saveData)
|
||||
if (saveRes.code !== 200) {
|
||||
this.$message.error(saveRes.errmsg || '问卷保存失败');
|
||||
return;
|
||||
this.$message.error(saveRes.errmsg || '问卷保存失败')
|
||||
return
|
||||
}
|
||||
const publishRes = await publishSurvey({ surveyId: this.surveyId });
|
||||
const publishRes = await publishSurvey({ surveyId: this.surveyId })
|
||||
if (publishRes.code === 200) {
|
||||
this.$message.success('发布成功');
|
||||
this.$store.dispatch('edit/getSchemaFromRemote');
|
||||
this.$message.success('发布成功')
|
||||
this.$store.dispatch('edit/getSchemaFromRemote')
|
||||
this.$router.push({
|
||||
name: 'publishResultPage',
|
||||
});
|
||||
})
|
||||
} else {
|
||||
this.$message.error(`发布失败 ${publishRes.errmsg}`);
|
||||
this.$message.error(`发布失败 ${publishRes.errmsg}`)
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error(`发布失败`);
|
||||
this.$message.error(`发布失败`)
|
||||
} finally {
|
||||
this.isPublishing = false;
|
||||
this.isPublishing = false
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.publish-btn {
|
||||
width: 100px;
|
||||
|
@ -7,29 +7,35 @@
|
||||
<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>
|
||||
<el-icon class="icon" v-if="autoSaveStatus === 'saving'"><el-icon-loading /></el-icon>
|
||||
<el-icon class="icon succeed" v-else-if="autoSaveStatus === 'succeed'"><el-icon-check /></el-icon>
|
||||
</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 {
|
||||
Loading as ElIconLoading,
|
||||
Check as ElIconCheck,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { saveSurvey } from '@/management/api/survey'
|
||||
import buildData from './buildData'
|
||||
import { mapState } from 'vuex'
|
||||
import { get as _get } from 'lodash-es'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ElIconLoading,
|
||||
ElIconCheck,
|
||||
},
|
||||
name: 'save',
|
||||
data() {
|
||||
return {
|
||||
isSaving: false,
|
||||
isShowAutoSave: false,
|
||||
autoSaveStatus: 'succeed',
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
@ -40,13 +46,13 @@ export default {
|
||||
saving: '保存中',
|
||||
succeed: '保存成功',
|
||||
failed: '保存失败',
|
||||
};
|
||||
return statusMap[this.autoSaveStatus];
|
||||
}
|
||||
return statusMap[this.autoSaveStatus]
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
schemaUpdateTime() {
|
||||
this.triggerAutoSave();
|
||||
this.triggerAutoSave()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@ -54,68 +60,69 @@ export default {
|
||||
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;
|
||||
this.$message.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('保存成功');
|
||||
this.$message.success('保存成功')
|
||||
} else {
|
||||
this.$message.error(res.errmsg);
|
||||
this.$message.error(res.errmsg)
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('保存问卷失败');
|
||||
this.$message.error('保存问卷失败')
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
this.isSaving = false
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url('@/management/styles/edit-btn.scss');
|
||||
|
||||
|
@ -4,16 +4,18 @@
|
||||
<span>返回</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'back',
|
||||
methods: {
|
||||
onBack() {
|
||||
this.$router.go(-1);
|
||||
this.$router.go(-1)
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.back-btn {
|
||||
height: 100%;
|
||||
|
@ -1,9 +1,7 @@
|
||||
<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
|
||||
@ -12,6 +10,7 @@
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'navbar-btn',
|
||||
(isActive && btnItem.key === 'skinsettings' ) || isExactActive ? 'router-link-exact-active' : '']"
|
||||
>
|
||||
<i class="iconfont" :class="[btnItem.icon]"></i>
|
||||
@ -23,6 +22,7 @@
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'pageNav',
|
||||
@ -52,10 +52,11 @@ export default {
|
||||
next: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
display: flex;
|
||||
|
@ -3,6 +3,7 @@
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'pageTitle',
|
||||
@ -12,8 +13,9 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
overflow: hidden;
|
||||
|
@ -1,32 +1,33 @@
|
||||
<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 />
|
||||
<Catalog />
|
||||
</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 Catalog from './components/catalog.vue'
|
||||
|
||||
export default {
|
||||
name: 'EditLeftTabPanel',
|
||||
data() {
|
||||
return {
|
||||
tabSelected: '0',
|
||||
};
|
||||
}
|
||||
},
|
||||
components: {
|
||||
typeList,
|
||||
catalog,
|
||||
TypeList,
|
||||
Catalog,
|
||||
},
|
||||
methods: {},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.tab-box {
|
||||
width: 300px;
|
||||
@ -34,10 +35,10 @@ export default {
|
||||
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;
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
<template>
|
||||
<div class="question-catalog-wrapper">
|
||||
<draggable :list="renderData" :options="dragOptions" @end="onDragEnd">
|
||||
<template v-for="(catalogItem, index) in renderData">
|
||||
<draggable
|
||||
:list="renderData"
|
||||
@end="onDragEnd"
|
||||
itemKey="field"
|
||||
handle=".draggHandle"
|
||||
host-class="catalog-item-ghost"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<catalogItem
|
||||
:key="catalogItem.field"
|
||||
:title="catalogItem.title"
|
||||
:indexNumber="catalogItem.indexNumber"
|
||||
:showIndex="catalogItem.showIndex"
|
||||
:title="element.title"
|
||||
:indexNumber="element.indexNumber"
|
||||
:showIndex="element.showIndex"
|
||||
@select="onSelect(index)"
|
||||
/>
|
||||
</template>
|
||||
@ -15,27 +20,22 @@
|
||||
</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',
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
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: {
|
||||
@ -44,18 +44,19 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onDragEnd(data) {
|
||||
const { newIndex, oldIndex } = data;
|
||||
const { newIndex, oldIndex } = data
|
||||
this.$store.dispatch('edit/moveQuestion', {
|
||||
index: 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>
|
||||
.question-catalog-wrapper {
|
||||
padding-bottom: 400px; // 考试题有个上拉框会盖住,改成和题型一致的
|
||||
|
@ -12,7 +12,7 @@
|
||||
export default {
|
||||
name: 'QuestionCatalogItem',
|
||||
data() {
|
||||
return {};
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
props: {
|
||||
@ -32,11 +32,12 @@ export default {
|
||||
components: {},
|
||||
methods: {
|
||||
onSelect() {
|
||||
this.$emit('select');
|
||||
this.$emit('select')
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
.question-catalog-item {
|
||||
position: relative;
|
||||
|