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>
This commit is contained in:
Weiguo Wang 2024-04-15 11:09:26 +08:00 committed by GitHub
parent a23fc28f5f
commit 6cd5fe892d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
207 changed files with 2546 additions and 2167 deletions

View File

@ -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
View File

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

View File

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

8
web/.prettierrc.json Normal file
View File

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

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

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

View File

@ -2,55 +2,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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

@ -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: {

View File

@ -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;

View File

@ -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');

View File

@ -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,
},
});
};
})
}

View File

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

View File

@ -1,49 +1,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,
};
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import axios from './base';
import axios from './base'
export const getSurveyList = ({ curPage, filter, order }) => {
return axios.get('/survey/getList', {
@ -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)
}

View File

@ -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>

View File

@ -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>

View File

@ -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;

View 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: [],
};

View File

@ -74,4 +74,4 @@ export const defaultQuestionConfig = {
value: 500,
},
},
};
}

View File

@ -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

View File

@ -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
},
},
],
},
];
]

View File

@ -15,4 +15,4 @@ export default [
direction: 'horizon',
labelStyle: { width: '120px' }
},
];
]

View File

@ -68,4 +68,4 @@ export default [
},
],
},
];
]

View File

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

View File

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

View File

@ -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>

View File

@ -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');

View 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')

View File

@ -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(/&nbsp;/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 {

View File

@ -6,7 +6,7 @@
<h2 class="data-list">数据列表</h2>
<div class="menus">
<el-switch
:value="isShowOriginData"
:model-value="isShowOriginData"
active-text="是否展示原数据"
@input="onIsShowOriginChange"
>
@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -23,4 +23,4 @@ export const SURVEY_TYPE_LIST = [
img: '/imgs/create/register-icon.webp',
desc: '活动报名 / 会议报名',
},
];
]

View File

@ -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%;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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%;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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,
};
}
}

View File

@ -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 {

View File

@ -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;

View File

@ -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');

View File

@ -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%;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -1,12 +1,17 @@
<template>
<div class="question-catalog-wrapper">
<draggable :list="renderData" :options="dragOptions" @end="onDragEnd">
<template v-for="(catalogItem, index) in renderData">
<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; //

View File

@ -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;

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