feat:新增多级联动题型
This commit is contained in:
parent
d4bdedb325
commit
18d147ae8c
@ -34,4 +34,8 @@ export enum QUESTION_TYPE {
|
|||||||
* 投票
|
* 投票
|
||||||
*/
|
*/
|
||||||
VOTE = 'vote',
|
VOTE = 'vote',
|
||||||
|
/**
|
||||||
|
* 多级联动
|
||||||
|
*/
|
||||||
|
MULTILEVEL = 'multilevel',
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,20 @@ export interface NPS {
|
|||||||
rightText: string;
|
rightText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MultilevelItem {
|
||||||
|
hash: string;
|
||||||
|
text: string;
|
||||||
|
children?: MultilevelItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultilevelData {
|
||||||
|
placeholder: Array<{
|
||||||
|
hash: string;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
children: Array<MultilevelItem>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TextRange {
|
export interface TextRange {
|
||||||
min: {
|
min: {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
@ -60,6 +74,7 @@ export interface DataItem {
|
|||||||
rangeConfig?: any;
|
rangeConfig?: any;
|
||||||
starStyle?: string;
|
starStyle?: string;
|
||||||
innerType?: string;
|
innerType?: string;
|
||||||
|
multilevelData: MultilevelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
|
@ -108,6 +108,7 @@ export class DataStatisticController {
|
|||||||
QUESTION_TYPE.RADIO_STAR,
|
QUESTION_TYPE.RADIO_STAR,
|
||||||
QUESTION_TYPE.RADIO_NPS,
|
QUESTION_TYPE.RADIO_NPS,
|
||||||
QUESTION_TYPE.VOTE,
|
QUESTION_TYPE.VOTE,
|
||||||
|
QUESTION_TYPE.MULTILEVEL,
|
||||||
];
|
];
|
||||||
const fieldList = responseSchema.code.dataConf.dataList
|
const fieldList = responseSchema.code.dataConf.dataList
|
||||||
.filter((item) => allowQuestionType.includes(item.type as QUESTION_TYPE))
|
.filter((item) => allowQuestionType.includes(item.type as QUESTION_TYPE))
|
||||||
|
@ -87,6 +87,21 @@ export class DataStatisticService {
|
|||||||
.join(',')
|
.join(',')
|
||||||
: optionTextMap[data[itemKey]]?.text || data[itemKey];
|
: optionTextMap[data[itemKey]]?.text || data[itemKey];
|
||||||
}
|
}
|
||||||
|
// 将多级联动id还原成选项文案
|
||||||
|
if (
|
||||||
|
itemConfig.multilevelData &&
|
||||||
|
itemConfig.type === QUESTION_TYPE.MULTILEVEL
|
||||||
|
) {
|
||||||
|
let optionTextMap = keyBy(itemConfig.multilevelData.children, 'hash');
|
||||||
|
data[itemKey] = data[itemKey]
|
||||||
|
?.split(',')
|
||||||
|
.map((v) => {
|
||||||
|
const text = optionTextMap[v]?.text || v;
|
||||||
|
optionTextMap = keyBy(optionTextMap[v].children, 'hash');
|
||||||
|
return text;
|
||||||
|
})
|
||||||
|
.join('-');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
|
@ -161,6 +161,7 @@ export function handleAggretionData({ dataMap, item }) {
|
|||||||
title: dataMap[item.field].title,
|
title: dataMap[item.field].title,
|
||||||
type: dataMap[item.field].type,
|
type: dataMap[item.field].type,
|
||||||
data: {
|
data: {
|
||||||
|
...item.data,
|
||||||
aggregation: arr.map((item) => {
|
aggregation: arr.map((item) => {
|
||||||
const num = item.toString();
|
const num = item.toString();
|
||||||
return {
|
return {
|
||||||
@ -173,6 +174,25 @@ export function handleAggretionData({ dataMap, item }) {
|
|||||||
summary,
|
summary,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} else if (type == QUESTION_TYPE.MULTILEVEL) {
|
||||||
|
const aggregation = getTextPaths(
|
||||||
|
dataMap[item.field].multilevelData.children,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
title: dataMap[item.field].title,
|
||||||
|
type: dataMap[item.field].type,
|
||||||
|
data: {
|
||||||
|
...item.data,
|
||||||
|
aggregation: aggregation.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
text: item.text,
|
||||||
|
count: aggregationMap[item.id]?.count || 0,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@ -182,6 +202,27 @@ export function handleAggretionData({ dataMap, item }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTextPaths = (arr, textPrefix = '', idPrefix = '') => {
|
||||||
|
let paths = [];
|
||||||
|
|
||||||
|
arr.forEach((item) => {
|
||||||
|
const currentTextPath = textPrefix
|
||||||
|
? `${textPrefix}-${item.text}`
|
||||||
|
: item.text;
|
||||||
|
const currentIdPath = idPrefix ? `${idPrefix},${item.hash}` : item.hash;
|
||||||
|
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
paths = paths.concat(
|
||||||
|
getTextPaths(item.children, currentTextPath, currentIdPath),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
paths.push({ id: currentIdPath, text: currentTextPath });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
function getAverage({ aggregation }) {
|
function getAverage({ aggregation }) {
|
||||||
const { sum, count } = aggregation.reduce(
|
const { sum, count } = aggregation.reduce(
|
||||||
(pre, cur) => {
|
(pre, cur) => {
|
||||||
|
BIN
web/public/imgs/question-type-snapshot/multilevel.webp
Normal file
BIN
web/public/imgs/question-type-snapshot/multilevel.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
@ -7,7 +7,8 @@ export enum QUESTION_TYPE {
|
|||||||
BINARY_CHOICE = 'binary-choice',
|
BINARY_CHOICE = 'binary-choice',
|
||||||
RADIO_STAR = 'radio-star',
|
RADIO_STAR = 'radio-star',
|
||||||
RADIO_NPS = 'radio-nps',
|
RADIO_NPS = 'radio-nps',
|
||||||
VOTE = 'vote'
|
VOTE = 'vote',
|
||||||
|
MULTILEVEL = 'multilevel',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 题目类型标签映射对象
|
// 题目类型标签映射对象
|
||||||
@ -19,7 +20,8 @@ export const typeTagLabels: Record<QUESTION_TYPE, string> = {
|
|||||||
[QUESTION_TYPE.BINARY_CHOICE]: '判断',
|
[QUESTION_TYPE.BINARY_CHOICE]: '判断',
|
||||||
[QUESTION_TYPE.RADIO_STAR]: '评分',
|
[QUESTION_TYPE.RADIO_STAR]: '评分',
|
||||||
[QUESTION_TYPE.RADIO_NPS]: 'NPS评分',
|
[QUESTION_TYPE.RADIO_NPS]: 'NPS评分',
|
||||||
[QUESTION_TYPE.VOTE]: '投票'
|
[QUESTION_TYPE.VOTE]: '投票',
|
||||||
|
[QUESTION_TYPE.MULTILEVEL]: '多级联动'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入类题型
|
// 输入类题型
|
||||||
@ -38,3 +40,6 @@ export const CHOICES = [
|
|||||||
|
|
||||||
// 评分题题型分类
|
// 评分题题型分类
|
||||||
export const RATES = [QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS]
|
export const RATES = [QUESTION_TYPE.RADIO_STAR, QUESTION_TYPE.RADIO_NPS]
|
||||||
|
|
||||||
|
// 高级题型分类
|
||||||
|
export const ADVANCED = [QUESTION_TYPE.MULTILEVEL]
|
@ -54,6 +54,13 @@ export const menuItems = {
|
|||||||
snapshot: '/imgs/question-type-snapshot/nGTscsZlwn1657702222857.webp',
|
snapshot: '/imgs/question-type-snapshot/nGTscsZlwn1657702222857.webp',
|
||||||
icon: 'tixing-toupiao',
|
icon: 'tixing-toupiao',
|
||||||
title: '投票'
|
title: '投票'
|
||||||
|
},
|
||||||
|
multilevel: {
|
||||||
|
type: 'multilevel',
|
||||||
|
path: 'MultilevelModule',
|
||||||
|
snapshot: '/imgs/question-type-snapshot/multilevel.webp',
|
||||||
|
icon: 'multilevel-select',
|
||||||
|
title: '多级联动'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +72,9 @@ const menuGroup = [
|
|||||||
{
|
{
|
||||||
title: '选择类题型',
|
title: '选择类题型',
|
||||||
questionList: ['radio', 'checkbox', 'binary-choice', 'radio-star', 'radio-nps', 'vote']
|
questionList: ['radio', 'checkbox', 'binary-choice', 'radio-star', 'radio-nps', 'vote']
|
||||||
|
}, {
|
||||||
|
title: '高级题型',
|
||||||
|
questionList: ['multilevel']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
138
web/src/management/hooks/useMultilevelPull.ts
Normal file
138
web/src/management/hooks/useMultilevelPull.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { cloneDeep } from 'lodash-es'
|
||||||
|
|
||||||
|
interface NodeItem {
|
||||||
|
hash: string,
|
||||||
|
text: string,
|
||||||
|
children?: NodeItem[],
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultilevelDat = {
|
||||||
|
placeholder: Array<{
|
||||||
|
hash: string,
|
||||||
|
text: string,
|
||||||
|
}>,
|
||||||
|
children: Array<NodeItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMultilevelPull = () => {
|
||||||
|
const maxCount = 3;
|
||||||
|
const optionsCount = 50;
|
||||||
|
const multilevelVal = ref<Array<null | NodeItem>>([]);
|
||||||
|
const multilevelData = ref<MultilevelDat | null>(null)
|
||||||
|
let hashArr: Array<string> = [];
|
||||||
|
|
||||||
|
|
||||||
|
const extractHash = (obj: MultilevelDat): Array<string> => {
|
||||||
|
const hashes: Array<string> = [];
|
||||||
|
|
||||||
|
function recurse(currentObj: any) {
|
||||||
|
if (Array.isArray(currentObj)) {
|
||||||
|
currentObj.forEach(item => recurse(item));
|
||||||
|
} else if (typeof currentObj === 'object' && currentObj !== null) {
|
||||||
|
if (currentObj.hash) {
|
||||||
|
hashes.push(currentObj.hash);
|
||||||
|
}
|
||||||
|
for (const key in currentObj) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (currentObj.hasOwnProperty(key as any)) {
|
||||||
|
recurse(currentObj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recurse(obj);
|
||||||
|
return hashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRandom = () => {
|
||||||
|
return Math.random().toString().slice(-6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNewHash = () => {
|
||||||
|
let random = getRandom()
|
||||||
|
while (random in hashArr) {
|
||||||
|
random = getRandom()
|
||||||
|
}
|
||||||
|
hashArr.push(random)
|
||||||
|
return random
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMultilevelNode = (key: number) => {
|
||||||
|
const nodeItem: NodeItem = (key == 0 ? multilevelData.value : multilevelVal.value[key - 1]) as NodeItem
|
||||||
|
if (nodeItem.children && nodeItem.children.length > optionsCount) {
|
||||||
|
ElMessageBox.alert(`当前最多添加${optionsCount}个选项`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const optionStr = `选项${nodeItem?.children ? nodeItem?.children?.length + 1 : 1}`
|
||||||
|
nodeItem.children?.push({
|
||||||
|
hash: getNewHash(),
|
||||||
|
text: optionStr,
|
||||||
|
children: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetMultilevelVal = (index: number) => {
|
||||||
|
for (let i = multilevelVal.value.length; index < i; i--) {
|
||||||
|
multilevelVal.value[i - 1] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMultilevelNode = (nodeItem: NodeItem, index: number, key: number) => {
|
||||||
|
try {
|
||||||
|
if (key == 0 && multilevelData.value?.children && multilevelData.value?.children?.length<=1) {
|
||||||
|
ElMessageBox.alert('至少保留一个选项', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (nodeItem.children) {
|
||||||
|
nodeItem.children[index].children = [];
|
||||||
|
}
|
||||||
|
nodeItem.children?.splice(index, 1)
|
||||||
|
resetMultilevelVal(key)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editMultilevelNode = (nodeItem: NodeItem, index: number, text: string) => {
|
||||||
|
nodeItem.children && (nodeItem.children[index].text = text)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const setMultilevelVal = (data: NodeItem, index: number) => {
|
||||||
|
if (multilevelVal.value[index]?.hash == data.hash) return
|
||||||
|
resetMultilevelVal(index)
|
||||||
|
multilevelVal.value[index] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const loadInitData = (data: MultilevelDat) => {
|
||||||
|
multilevelData.value = cloneDeep(data);
|
||||||
|
multilevelVal.value = [];
|
||||||
|
for (let index = 0; index < maxCount; index++) {
|
||||||
|
multilevelVal.value.push(null)
|
||||||
|
}
|
||||||
|
hashArr = extractHash(multilevelData.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
addMultilevelNode,
|
||||||
|
removeMultilevelNode,
|
||||||
|
editMultilevelNode,
|
||||||
|
loadInitData,
|
||||||
|
setMultilevelVal,
|
||||||
|
|
||||||
|
multilevelVal,
|
||||||
|
multilevelData
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,194 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span class="primary-color" @click="openMultiLevelConfig"> 选项编辑 > </span>
|
||||||
|
<el-dialog title="多级联动编辑" class="multiLevel-config-wrapper" v-model="configVisible" :append-to-body="true"
|
||||||
|
width="706px">
|
||||||
|
<div class="placeholder-wrapper">
|
||||||
|
<div class="placeholder-wrapper-item" v-for="(item, i) in multilevelData.placeholder" :key="item.hash">
|
||||||
|
<div class="placeholder-wrapper-list">
|
||||||
|
<div class="placeholder-disable-edit multiLevel-input" @click="showPlaceholderEdit(item.hash)"
|
||||||
|
v-if="editMap[item.hash]">{{ item.text }}</div>
|
||||||
|
<el-input placeholder="请输入内容" :id="`input-${item.hash}`" @blur="editMap[item.hash] = true"
|
||||||
|
v-model="item.text" v-else class="multiLevel-input" />
|
||||||
|
</div>
|
||||||
|
<i-ep-ArrowRight v-if="multilevelData.placeholder.length - 1 > i" style="font-size: 16px;margin:0px 4px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="options-wrapper">
|
||||||
|
<div class="options-wrapper-panel" v-for="(item, key) in multilevelVal" :key="key">
|
||||||
|
<template v-if="key == 0">
|
||||||
|
<draggable :list="multilevelData?.children" itemKey="hash">
|
||||||
|
<template #item="{ element, index }">
|
||||||
|
<div :class="`option-wrapper-item ${element.hash == multilevelVal[key]?.hash ? 'input-active' : ''}`"
|
||||||
|
:key="element.hash" @click="setMultilevelVal(element, key)">
|
||||||
|
<el-input v-model="element.text" class="multiLevel-input">
|
||||||
|
<template #suffix>
|
||||||
|
<i-ep-RemoveFilled v-if="element.hash == multilevelVal[key]?.hash" class="remove-icon"
|
||||||
|
@click.stop="removeMultilevelNode(multilevelData, index,key)" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="multilevelVal[key - 1]">
|
||||||
|
<draggable :list="multilevelVal[key - 1].children" itemKey="hash">
|
||||||
|
<template #item="{ element, index }">
|
||||||
|
<div :class="`option-wrapper-item ${element.hash == multilevelVal[key]?.hash ? 'input-active' : ''}`"
|
||||||
|
:key="element.hash" @click="setMultilevelVal(element, key)">
|
||||||
|
<el-input v-model="element.text" class="multiLevel-input">
|
||||||
|
<template #suffix>
|
||||||
|
<i-ep-RemoveFilled v-if="element.hash == multilevelVal[key]?.hash" class="remove-icon"
|
||||||
|
@click.stop="removeMultilevelNode(multilevelVal[key - 1], index,key)" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="add-node-wrapper">
|
||||||
|
<template v-for="(item, key) in multilevelVal" :key="key">
|
||||||
|
<div v-if="key == 0 || (multilevelVal[key - 1])" @click="addMultilevelNode(key)" class="add-node-item">
|
||||||
|
<i-ep-Plus />
|
||||||
|
添加选项
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="configVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="multilevelConfigChange">确认</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick } from 'vue'
|
||||||
|
import { useEditStore } from '@/management/stores/edit'
|
||||||
|
import { useMultilevelPull } from '@/management/hooks/useMultilevelPull'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
const emit = defineEmits(['handleChange'])
|
||||||
|
|
||||||
|
const editStore = useEditStore()
|
||||||
|
const { loadInitData, multilevelVal, multilevelData, addMultilevelNode, setMultilevelVal, removeMultilevelNode } = useMultilevelPull()
|
||||||
|
const configVisible = ref(false)
|
||||||
|
const editMap = ref({})
|
||||||
|
const openMultiLevelConfig = () => {
|
||||||
|
init();
|
||||||
|
configVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPlaceholderEdit = (hash) => {
|
||||||
|
editMap.value[hash] = false;
|
||||||
|
nextTick(() => {
|
||||||
|
document.getElementById(`input-${hash}`)?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
loadInitData(editStore.moduleConfig.multilevelData)
|
||||||
|
editMap.value = [];
|
||||||
|
multilevelData.value.placeholder.map(v => {
|
||||||
|
editMap.value[v.hash] = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const multilevelConfigChange = () => {
|
||||||
|
emit('handleChange', { key: 'multilevelData', value: multilevelData.value })
|
||||||
|
configVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.multiLevel-config-wrapper {
|
||||||
|
.placeholder-wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiLevel-input {
|
||||||
|
width: 200px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-wrapper-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-disable-edit {
|
||||||
|
display: flex;
|
||||||
|
padding: 1px 11px;
|
||||||
|
align-items: center;
|
||||||
|
background: #F6F7F9;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-wrapper {
|
||||||
|
display: flex;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.options-wrapper-panel {
|
||||||
|
margin-top: 24px;
|
||||||
|
width: 200px;
|
||||||
|
margin-left: 28px;
|
||||||
|
height: 252px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-node-wrapper {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.add-node-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 200px;
|
||||||
|
margin-left: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: $primary-color;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-wrapper-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-active {
|
||||||
|
:deep(.el-input__wrapper) {
|
||||||
|
box-shadow: 0 0 0 1px var(--el-input-focus-border-color) inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: red
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-node-wrapper {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -40,6 +40,9 @@ switch (props.moduleConfig.type) {
|
|||||||
case QUESTION_TYPE.RADIO_NPS:
|
case QUESTION_TYPE.RADIO_NPS:
|
||||||
advancedComponent.value = defineAsyncComponent(() => import('./RateConfig.vue'))
|
advancedComponent.value = defineAsyncComponent(() => import('./RateConfig.vue'))
|
||||||
break
|
break
|
||||||
|
case QUESTION_TYPE.MULTILEVEL:
|
||||||
|
advancedComponent.value = defineAsyncComponent(() => import('./MultiLevelConfig.vue'))
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ const editStore = useEditStore()
|
|||||||
const { newQuestionIndex } = storeToRefs(editStore)
|
const { newQuestionIndex } = storeToRefs(editStore)
|
||||||
const { addQuestion, setCurrentEditOne, createNewQuestion } = editStore
|
const { addQuestion, setCurrentEditOne, createNewQuestion } = editStore
|
||||||
|
|
||||||
const activeNames = ref([0, 1])
|
const activeNames = ref([0, 1, 2])
|
||||||
const previewImg = ref('')
|
const previewImg = ref('')
|
||||||
const isShowPreviewImage = ref(false)
|
const isShowPreviewImage = ref(false)
|
||||||
const previewTop = ref(0)
|
const previewTop = ref(0)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'iconfont'; /* Project id 4263849 */
|
font-family: 'iconfont'; /* Project id 4263849 */
|
||||||
src:
|
src:
|
||||||
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.woff2?t=1723600417360') format('woff2'),
|
url('//at.alicdn.com/t/c/font_4263849_pddd8961y4d.woff2?t=1728727761985') format('woff2'),
|
||||||
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.woff?t=1723600417360') format('woff'),
|
url('//at.alicdn.com/t/c/font_4263849_pddd8961y4d.woff?t=1728727761985') format('woff'),
|
||||||
url('//at.alicdn.com/t/c/font_4263849_2re4gm4ryc3.ttf?t=1723600417360') format('truetype');
|
url('//at.alicdn.com/t/c/font_4263849_pddd8961y4d.ttf?t=1728727761985') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@ -177,3 +177,6 @@
|
|||||||
.icon-shiying:before {
|
.icon-shiying:before {
|
||||||
content: '\e6f6';
|
content: '\e6f6';
|
||||||
}
|
}
|
||||||
|
.icon-multilevel-select:before {
|
||||||
|
content: '\e6f9';
|
||||||
|
}
|
@ -10,5 +10,6 @@ export default {
|
|||||||
city: 'CityModule',
|
city: 'CityModule',
|
||||||
vote: 'VoteModule',
|
vote: 'VoteModule',
|
||||||
'matrix-checkbox': 'GroupModule',
|
'matrix-checkbox': 'GroupModule',
|
||||||
selectMoreModule: 'SelectMoreModule'
|
selectMoreModule: 'SelectMoreModule',
|
||||||
|
multilevel:'MultilevelModule',
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,180 @@
|
|||||||
|
<template>
|
||||||
|
<div class="multilevel-wrapper">
|
||||||
|
<div class="multilevel-wrapper-mobile" v-if="isMobile">
|
||||||
|
<div v-for="(v, i) in valList" :key="v+i" class="multilevel-select-item">
|
||||||
|
<template v-if="i == 0">
|
||||||
|
<div :class="`select-input ${i==pickIndex ? 'border-active' : ''}`" @click="showPickPop(props.multilevelData.children,i)">
|
||||||
|
<div class="select-input-left">
|
||||||
|
<div v-if="valList[i]" class="select-input-active">{{ valList[i].text }}</div>
|
||||||
|
<div v-else class="select-input-placeholder">{{ placeholderList[i].text }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="select-input-right"><i-ep-ArrowDown class="arrow-down-icon"/></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="valList[i - 1] && valList[i - 1]?.children?.length > 0">
|
||||||
|
<div :class="`select-input ${i==pickIndex ? 'border-active' : ''}`" @click="showPickPop(valList[i - 1].children,i)">
|
||||||
|
<div class="select-input-left">
|
||||||
|
<div v-if="valList[i]" class="select-input-active">{{ valList[i].text }}</div>
|
||||||
|
<div v-else class="select-input-placeholder">{{ placeholderList[i].text }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="select-input-right"><i-ep-ArrowDown class="arrow-down-icon"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<Picker v-model="pickPop" :list="listPop" @confirm="onConfirm" @cancel="onCancel" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="multilevel-wrapper-pc">
|
||||||
|
<div v-for="(v, i) in valList" :key="v+i" class="multilevel-select-item">
|
||||||
|
<template v-if="i == 0">
|
||||||
|
<div v-if="props.readonly" class="multilevel-mask"></div>
|
||||||
|
<el-select v-model="valList[i]" @change="(data)=>handleChange(data,i)" :placeholder="placeholderList[i].text" size="large" value-key="hash"
|
||||||
|
style="width: 194px">
|
||||||
|
<el-option v-for="item in props.multilevelData.children" :key="item.hash" :label="item.text"
|
||||||
|
:value="item" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div v-if="valList[i - 1] && valList[i - 1]?.children?.length > 0">
|
||||||
|
<el-select v-model="valList[i]" @change="(data)=>handleChange(data,i)" :placeholder="placeholderList[i].text" size="large" value-key="hash"
|
||||||
|
style="width: 194px">
|
||||||
|
<el-option v-for="item in valList[i - 1].children" :key="item.hash" :label="item.text" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick, onMounted, computed } from 'vue'
|
||||||
|
import Picker from '../Picker/index.vue'
|
||||||
|
import { isMobile as isInMobile } from '@/render/utils/index'
|
||||||
|
const props = defineProps({
|
||||||
|
multilevelData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
|
const valList = ref([]);
|
||||||
|
const pickPop = ref(false)
|
||||||
|
const listPop = ref([])
|
||||||
|
const pickIndex = ref(-1)
|
||||||
|
const isMobile = isInMobile()
|
||||||
|
|
||||||
|
const placeholderList = computed(() => {
|
||||||
|
return props.multilevelData.placeholder
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetValList = (index) => {
|
||||||
|
for (let i = valList.value.length-1; index < i; i--) {
|
||||||
|
valList.value[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = async(val,i) => {
|
||||||
|
await nextTick()
|
||||||
|
resetValList(i)
|
||||||
|
if (val?.children?.length == 0) {
|
||||||
|
const hashList = [];
|
||||||
|
valList.value.map(v => {
|
||||||
|
if (v) {
|
||||||
|
hashList.push(v.hash)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
emit('change',hashList.join(','))
|
||||||
|
} else {
|
||||||
|
if (props.value) {
|
||||||
|
emit('change','')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onConfirm = (val) => {
|
||||||
|
valList.value[pickIndex.value] = val;
|
||||||
|
handleChange(val, pickIndex.value);
|
||||||
|
pickIndex.value=-1
|
||||||
|
}
|
||||||
|
const onCancel = () => {
|
||||||
|
pickIndex.value=-1
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPickPop = (list,index) => {
|
||||||
|
pickPop.value = true;
|
||||||
|
listPop.value = list
|
||||||
|
pickIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
placeholderList.value.map(() => {
|
||||||
|
valList.value.push(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.multilevel-wrapper {
|
||||||
|
.multilevel-mask{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
&-pc {
|
||||||
|
display: flex;
|
||||||
|
.multilevel-select-item{
|
||||||
|
margin-right: 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-mobile {
|
||||||
|
.multilevel-select-item{
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.select-input{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width:100%;
|
||||||
|
height: 40px;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid rgba(227,228,232,1);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
&-placeholder{
|
||||||
|
font-size: 14px;
|
||||||
|
color: #C8C9CD;
|
||||||
|
}
|
||||||
|
&-active{
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4A4C5B;
|
||||||
|
}
|
||||||
|
&:active{
|
||||||
|
// background: #F6F7F9;
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
&.border-active{
|
||||||
|
border-color: $primary-color;
|
||||||
|
}
|
||||||
|
.arrow-down-icon{
|
||||||
|
color:#C8C9CD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
interface IEvent {
|
||||||
|
handleConfirm: () => void,
|
||||||
|
handleCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useEvent = ({ emit, ctx }: any): IEvent => {
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('confirm', ctx.list[ctx.index])
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
emit('cancel', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleConfirm,
|
||||||
|
handleCancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useEvent
|
@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div class="picker-wrapper" @click.stop>
|
||||||
|
<div class="picker-mask" v-show="modelValue" @click.stop="handleCancel"></div>
|
||||||
|
<transition name="slide-picker">
|
||||||
|
<div class="x-picker" v-show="modelValue">
|
||||||
|
<div class="x-picker__header">
|
||||||
|
<p class="x-picker__header-left" @click.stop="handleCancel">取消</p>
|
||||||
|
<p class="x-picker__header-right" @click.stop="handleConfirm">确定</p>
|
||||||
|
</div>
|
||||||
|
<div class="x-picker__content">
|
||||||
|
<div class="x-picker__content-wrapper"></div>
|
||||||
|
<ul class="x-picker__content-box" ref="box" :style="getOffsetY" @touchstart="handleStart"
|
||||||
|
@touchend="handleEnd" @touchmove="handleMove">
|
||||||
|
<li class="x-picker__content-item" v-for="(item, idx) in list" @click="goItem(idx)" :key="idx" :style="getStyle(Number(idx))">{{
|
||||||
|
item?.text }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent, getCurrentInstance,watch } from 'vue'
|
||||||
|
import useList from './list'
|
||||||
|
import useEvent from './event'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
list: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const { ctx } = getCurrentInstance()
|
||||||
|
const $useList = useList(props)
|
||||||
|
const $useEvent = useEvent({ emit, ctx })
|
||||||
|
|
||||||
|
|
||||||
|
watch(()=>props.list,()=>{
|
||||||
|
$useList.resetData(0)
|
||||||
|
$useList.list.value = props.list
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...$useList,
|
||||||
|
...$useEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.x-picker {
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
.picker-mask {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: rgba(0,0,0,0.50);
|
||||||
|
z-index: 98;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__header>p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__header-left {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6E707C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__header-center {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__header-right {
|
||||||
|
font-size: 14px;
|
||||||
|
color: $primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__content {
|
||||||
|
height: 230px;
|
||||||
|
padding: 15px 0;
|
||||||
|
margin: 0 24px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__content-box {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transition: all .2s;
|
||||||
|
font-size: 20px;
|
||||||
|
transition-timing-function: cubic-bezier(0.23, 1, 0.68, 1);
|
||||||
|
transform: translate(-50%, -20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__content-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-18px);
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border-top: 1px solid #E3E4E8;
|
||||||
|
border-bottom: 1px solid #E3E4E8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-picker__content-item {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
transition: color .5s;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-picker-enter-active,
|
||||||
|
.slide-picker-leave-active {
|
||||||
|
transition: all .5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-picker-enter-from,
|
||||||
|
.slide-picker-leave-to {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,128 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
interface IList {
|
||||||
|
box: Ref,
|
||||||
|
list: Ref<Array<any>>,
|
||||||
|
getOffsetY: any,
|
||||||
|
getStyle: any,
|
||||||
|
handleMove: (e: TouchEvent) => void,
|
||||||
|
handleStart: (e: TouchEvent) => void,
|
||||||
|
handleEnd: (e: TouchEvent) => void,
|
||||||
|
goItem: (idx: number) => void,
|
||||||
|
resetData: () => void,
|
||||||
|
index: Ref<number>
|
||||||
|
}
|
||||||
|
|
||||||
|
const useList = (props: any): IList => {
|
||||||
|
const colors = ['gray', '#ccc', '#ddd', '#eee']
|
||||||
|
const scales = [.96, .9, .88, .84]
|
||||||
|
let startY: number, activeIndex = 0
|
||||||
|
const box = ref()
|
||||||
|
const offY = ref()
|
||||||
|
const index = ref(0)
|
||||||
|
const list = ref(props.list)
|
||||||
|
|
||||||
|
const getStyle = (idx: number) => {
|
||||||
|
let color = '#000', scale = 1
|
||||||
|
const len = colors.length - 1
|
||||||
|
if (idx > activeIndex) {
|
||||||
|
const _idx = idx - activeIndex > len ? len : idx - activeIndex - 1
|
||||||
|
color = colors[_idx]
|
||||||
|
scale = scales[_idx]
|
||||||
|
} else if (idx < activeIndex) {
|
||||||
|
const _idx = activeIndex - idx > len ? len : activeIndex - idx - 1
|
||||||
|
color = colors[_idx]
|
||||||
|
scale = scales[_idx]
|
||||||
|
}
|
||||||
|
return { color, transform: `scale(${scale})` }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节流
|
||||||
|
const throttle = function (callback: any, delay = 20) {
|
||||||
|
let timer: number | null = null
|
||||||
|
return function (args: any) {
|
||||||
|
if (timer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
callback(args)
|
||||||
|
timer = null
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动的实现
|
||||||
|
const move = throttle((e: any) => {
|
||||||
|
offY.value = e.touches[0].clientY - startY
|
||||||
|
if (offY.value > 40) {
|
||||||
|
offY.value = 40
|
||||||
|
} else if (offY.value < -box.value.offsetHeight - 40) {
|
||||||
|
offY.value = -box.value.offsetHeight - 40
|
||||||
|
}
|
||||||
|
// 计算当前位置的就近下标
|
||||||
|
index.value = Math.abs(Math.ceil(offY.value / 40))
|
||||||
|
// 判断顶部和底部的一个界限,然后做一个位置的重置
|
||||||
|
if (index.value <= 0 || offY.value > 0) {
|
||||||
|
index.value = 0
|
||||||
|
} else if (index.value > list.value.length - 1 || offY.value < -box.value.offsetHeight - 18) {
|
||||||
|
index.value = list.value.length - 1
|
||||||
|
}
|
||||||
|
activeIndex = index.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const goItem = (idx: number) => {
|
||||||
|
index.value = idx;
|
||||||
|
activeIndex = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetData = () => {
|
||||||
|
startY = 0;
|
||||||
|
activeIndex = 0
|
||||||
|
index.value = 0
|
||||||
|
box.value = null;
|
||||||
|
offY.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStart = (e: TouchEvent) => {
|
||||||
|
const transform = box.value.style.transform
|
||||||
|
transform.match(/,(.*)px/)
|
||||||
|
startY = e.touches[0].clientY - Number(RegExp.$1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMove = (e: TouchEvent) => move(e)
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
// 重置当前位置,加setTimeout避免出现Bug
|
||||||
|
setTimeout(() => {
|
||||||
|
offY.value = -index.value * 40 - 18
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOffsetY = computed(() => {
|
||||||
|
if (typeof offY.value === 'number') {
|
||||||
|
return {
|
||||||
|
transform: `translate(-50%, ${offY.value}px)`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
transform: 'translate(-50%, -18px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
box,
|
||||||
|
list,
|
||||||
|
getOffsetY,
|
||||||
|
getStyle,
|
||||||
|
handleMove,
|
||||||
|
handleStart,
|
||||||
|
handleEnd,
|
||||||
|
goItem,
|
||||||
|
resetData,
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useList
|
@ -0,0 +1,45 @@
|
|||||||
|
import { defineComponent} from 'vue'
|
||||||
|
import BaseMultilevel from './BaseMultilevel/index.vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'MultilevelModule',
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
multilevelData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['change'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const onChange = (value) => {
|
||||||
|
const key = props.field
|
||||||
|
emit('change', {
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props,
|
||||||
|
onChange
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
const { props } = this
|
||||||
|
return (
|
||||||
|
<BaseMultilevel multilevelData={props.multilevelData} readonly={props.readonly} value={props.value} onChange={this.onChange} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
94
web/src/materials/questions/widgets/MultilevelModule/meta.js
Normal file
94
web/src/materials/questions/widgets/MultilevelModule/meta.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import basicConfig from '@materials/questions/common/config/basicConfig'
|
||||||
|
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: '多级联动',
|
||||||
|
type: 'multilevel',
|
||||||
|
componentName: 'MultilevelModule',
|
||||||
|
attrs: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
propType: 'String',
|
||||||
|
description: '这是用于描述题目类型',
|
||||||
|
defaultValue: 'multilevel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isRequired',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '是否必填',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showIndex',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示序号',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showType',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示类型',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showSpliter',
|
||||||
|
propType: Boolean,
|
||||||
|
description: '显示分割线',
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'multilevelData',
|
||||||
|
propType: Array,
|
||||||
|
description: '这是用于描述选项',
|
||||||
|
defaultValue:
|
||||||
|
{
|
||||||
|
placeholder: [{
|
||||||
|
text: '请选择',
|
||||||
|
hash: '115016'
|
||||||
|
}, {
|
||||||
|
text: '请选择',
|
||||||
|
hash: '115017'
|
||||||
|
}, {
|
||||||
|
text: '请选择',
|
||||||
|
hash: '115018'
|
||||||
|
}],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text: '选项1',
|
||||||
|
children: [],
|
||||||
|
hash: '115019'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '选项2',
|
||||||
|
children: [],
|
||||||
|
hash: '115020'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '选项3',
|
||||||
|
children: [],
|
||||||
|
hash: '115011'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
],
|
||||||
|
formConfig: [
|
||||||
|
basicConfig
|
||||||
|
],
|
||||||
|
editConfigure: {
|
||||||
|
optionEdit: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
optionEditBar: {
|
||||||
|
show: true,
|
||||||
|
configure: {
|
||||||
|
showOthers: false,
|
||||||
|
showAdvancedConfig: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
@ -3,6 +3,7 @@ import App from './App.vue'
|
|||||||
import EventBus from './utils/eventbus'
|
import EventBus from './utils/eventbus'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import 'default-passive-events'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
Loading…
Reference in New Issue
Block a user