feat: 新增口味测试

This commit is contained in:
钱冠学
2024-10-09 16:31:25 +08:00
parent f0abcd22f4
commit d7be573bf9
18 changed files with 5444 additions and 15 deletions

View File

@@ -0,0 +1,124 @@
<script setup>
import { defineEmits, defineProps, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
const emits = defineEmits(['update:visible', 'change'])
const props = defineProps({
visible: { type: Boolean, default: false },
textArr: { type: Array, default: () => [] },
title: { type: String, default: '' },
lineMinLength: { type: Number, default: 0 }, // 每行最少几个字
lineMaxLength: { type: Number, default: 0 },
minLine: { type: Number, default: 0 }, // 最少几行
maxLine: { type: Number, default: 0 },
trim: { type: Boolean, default: true }
})
const loading = ref(false)
const shown = ref(false)
const text = ref('')
let retreatText = ''
watch(
() => props.visible,
() => {
shown.value = !!props.visible
},
{ immediate: true }
)
watch(
() => props.textArr,
() => {
text.value = [...(props.textArr || [])].join('\n')
retreatText = text.value
},
{ immediate: true, deep: true }
)
function onClose() {
shown.value = false
emits('update:visible', false)
}
function onSave() {
let lines = text.value.split('\n')
if (props.trim) {
lines = lines.map((line) => line.trim())
}
const resultLines = Array.from(new Set(lines))
const repeatCount = lines.length - resultLines.length
let msg = `添加成功,共成功生成${resultLines.length}`
if (repeatCount) {
msg += `,其中自动去除重复项${repeatCount}`
}
message.success(msg)
onClose()
emits('change', resultLines)
}
function check() {
if (props.maxLine && text.value.split('\n').length > props.maxLine) {
return false
}
if (
props.minLine &&
retreatText.split('\n').length >= props.minLine &&
text.value.split('\n').length < props.minLine
) {
return false
}
return true
}
function onChange() {
if (!check()) {
retreat()
}
retreatText = text.value
}
function retreat() {
text.value = retreatText
}
</script>
<template>
<div>
<a-modal
v-model:visible="shown"
width="800"
wrapClassName="custom-modal"
:destroyOnClose="false"
:centered="true"
:maskClosable="false"
:title="title"
:confirmLoading="loading"
@ok="onSave"
@cancel="onClose"
>
<slot name="messageTop"></slot>
<a-textarea
v-model:value="text"
:auto-size="{ minRows: 8, maxRows: 10 }"
@change="onChange"
/>
<slot name="messageBottom"></slot>
<template #footer>
<div class="flex flex-end">
<a-button type="outline" class="custom-button" @click="onClose"> </a-button>
<a-button type="primary" class="custom-button" @click="onSave"> </a-button>
</div>
</template>
</a-modal>
</div>
</template>
<style scoped lang="scss"></style>

37
src/style/box.scss Normal file
View File

@@ -0,0 +1,37 @@
.flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-start, .flex.flex-start {
display: flex;
justify-content: flex-start;
align-items: center;
}
.flex-end, .flex.flex-end {
display: flex;
justify-content: flex-end;
align-items: center;
}
.flex-auto {
flex: auto;
}
.flex-none {
flex: none;
}
.full-width {
width: 100%;
}
.full-height{
height: 100%;
}
.full-size {
width: 100%;
height: 100%;
}

View File

@@ -3,6 +3,7 @@
覆盖全局样式
*/
@import "./box.scss";
.ant-select-item-option-selected{
font-weight: normal!important;
@@ -16,4 +17,4 @@
.ant-table-column-sorter {
position: relative;
top: -2px;
}
}

View File

@@ -154,9 +154,8 @@
/>
<!-- 概念测试 -->
<ConceptTesting
ref="conceptTestingRef"
/>
<ConceptTesting ref="conceptTestingRef" />
<TasteTest ref="tasteTestRef"/>
</div>
</template>
@@ -172,7 +171,7 @@ import {
} from "vue";
import {message, Modal} from "ant-design-vue";
import { getSceneList } from "@/views/ProjectManage/api";
import SectionTitle from "@/components/layout/Title/SectionTitle.vue";
import SectionTitle from "@/components/layout/title/SectionTitle.vue";
import blankIcon from "./img/blank.png";
import importIcon from "./img/import.png";
import extendIcon from "./img/extend.png";
@@ -191,6 +190,7 @@ import {currentMode} from "@/config";
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons-vue';
import ConceptTesting from "@/views/Concept/ConceptTesting.vue"
import TasteTest from './presets/taste/TasteTest.vue'
const loading = ref(false);
@@ -217,6 +217,8 @@ const cardCount = ref(0);
const normalCurrentPage = ref(0);
const professionalCurrentPage = ref(0);
const tasteTestRef = ref(null)
const props = defineProps({
groupId: {type: Number, value: -1 },
// groupList: {
@@ -407,11 +409,12 @@ function createProfessionalSurvey(record) {
groupInfo.value.scene_code_info = `${record.code}`;
groupInfo.value.project_name = '';
// 产品测试(口味100和包装101)
if (record.code === 23) {
curTemp.value.type = 100;
return createSurveyProductRef.value.openModal();
}
else if (record.code === 24) {
// if (record.code === 23) {
// curTemp.value.type = 100;
// return createSurveyProductRef.value.openModal();
// }
// else
if (record.code === 24) {
curTemp.value.type = 101;
return createSurveyProductRef.value.openModal();
}
@@ -421,6 +424,12 @@ function createProfessionalSurvey(record) {
}
else if ([36, 37, 38].includes(record.code)) {
return conceptTestingRef.value.openModal(0, record.sn, record.other?.split(',') || []);
} else if ([39, 40, 41].includes(record.code)) {
return tasteTestRef.value.openModal({
sn: record.sn,
snList: record.other?.split(',') || [],
sceneCode: '23'
})
}
else{
Modal.confirm({

View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
/* 新增方案配置 */
export function postTemplates(sn, data) {
return request({
method: 'POST',
url: `/console/templates/${sn}`,
data
})
}
/* 获取品类品牌 */
export function getSurveyBrands() {
return request({
method: 'GET',
url: `/console/survey_brands`
})
}

View File

@@ -0,0 +1,437 @@
<script setup>
import { computed, defineEmits, defineExpose, reactive, ref, defineProps, watch } from 'vue'
import SectionTitle from '@/components/layout/title/SectionTitle.vue'
import SurveyConfig from './SurveyConfig.vue'
import { getSurveyBrands } from '../api'
import { testTypeEnum, testTypeList } from '../consts'
import conceptJson from '../json/concept'
import tasteJson from '../json/taste'
import packageJson from '../json/package'
const emits = defineEmits(['autoname', 'testtypechange'])
const props = defineProps({
schemeType: { type: [Number, String] }, // 1概念 2口味 3包装
sceneCode: { type: [Number, String], default: '' },
suffixTitle: { type: String, default: '' }
})
const projectFormRef = ref(null)
const projectFormModel = reactive({
testType: testTypeEnum.standard,
surveyCategoryStr: undefined,
surveyBrandId: undefined,
surveyBrandStr: undefined
})
const projectFormRules = {
surveyCategoryStr: [{ required: true, message: '请选择品类', trigger: 'blur' }],
surveyBrandId: [{ required: true, message: '请选择品牌', trigger: 'blur' }]
}
const categoryList = ref([]) // 品类
const brandList = ref([]) // 品牌
const surveyConfigRef = ref(null)
async function getOptions() {
const res = await getSurveyBrands()
if (res.code === 0) {
categoryList.value = transformToTree(res.data)
}
}
getOptions()
// 筛选全部树形数据
function transformToTree(data) {
const result = []
const map = {}
data.forEach((item) => {
// 初始化映射关系并添加 children 数组
map[item.id] = { ...item, children: map[item.id]?.children || [] }
if (item.parent_id === 0) {
// 如果 parent_id 为 0直接放入 result 作为一级项
result.push(map[item.id])
} else {
// 如果有 parent_id则将其放入对应父项的 children 中
if (!map[item.parent_id]) {
// 父项还没初始化时,先初始化
map[item.parent_id] = { children: [] }
}
map[item.parent_id].children.push(map[item.id])
}
})
return result
}
// 根据id找到子级数据
function findChildrenById(treeData, targetId) {
const stack = [...treeData] // 使用栈避免递归
while (stack.length) {
const node = stack.pop()
if (node.id === targetId) {
return node.children
}
if (node.children.length > 0) {
stack.push(...node.children) // 将子节点加入栈
}
}
return [] // 如果没有找到,返回 []
}
// 选择品类
function onSelectCategory(item) {
projectFormModel.surveyBrandId = undefined
brandList.value = findChildrenById(categoryList.value, item.id)
}
// 选择品牌
function onSelectBrand(item) {
projectFormModel.surveyBrandStr = item.title
surveyConfigRef.value.onAutoCreate()
}
// 修改测试版本
function onChangeTestType(item) {
emits('testtypechange', item)
}
function getPopupContainer(el) {
return el.parentNode.parentNode || document.body
}
const standardChecked = ref(
[{}, conceptJson, tasteJson, packageJson][props.schemeType][`check_list_standard`]
.filter((i) => i.disabled)
.map((i) => i.value)
)
const quickTestChecked = ref(
[{}, conceptJson, tasteJson, packageJson][props.schemeType][`check_list_quick_test`]
.filter((i) => i.disabled)
.map((i) => i.value)
)
const pairChecked = ref(
[{}, conceptJson, tasteJson, packageJson][props.schemeType][`check_list_pair`]
.filter((i) => i.disabled)
.map((i) => i.value)
)
const standardIndicatorList = computed(() =>
props.schemeType
? [{}, conceptJson, tasteJson, packageJson][props.schemeType][`check_list_standard`]
: []
)
const quickTestIndicatorList = computed(() =>
props.schemeType
? [{}, conceptJson, tasteJson, packageJson][props.schemeType][`check_list_quick_test`]
: []
)
const pairIndicatorList = computed(() =>
props.schemeType
? [{}, conceptJson, tasteJson, packageJson][props.schemeType][`check_list_pair`]
: []
)
const isCheckedAll = computed(
() => standardChecked.value.length === standardIndicatorList.value.length
)
const isIndeterminate = computed(
() => standardChecked.value.length !== standardIndicatorList.value.length
)
function toggleCheckAll() {
if (isCheckedAll.value) {
standardChecked.value = standardIndicatorList.value
.filter((item) => item.disabled)
.map((item) => item.value)
} else {
standardChecked.value = standardIndicatorList.value.map((item) => item.value)
}
}
function validateForm() {
return new Promise(async (resolve) => {
const result = await Promise.allSettled([
projectFormRef.value.validate(),
surveyConfigRef.value.formRef.validate()
])
const rejectReason = result
.filter((i) => i.status === 'rejected')
.map((i) => i.reason.errorFields)
.flatMap((i) => i)
const resolveData = result.filter((i) => i.status === 'fulfilled').map((i) => i.value)
if (rejectReason.length) {
return resolve({ status: 'rejected', reason: rejectReason })
}
const concept_indexes = {
[testTypeEnum.standard]: standardChecked.value,
[testTypeEnum.quickTest]: quickTestChecked.value,
[testTypeEnum.pair]: pairChecked.value
}[projectFormModel.testType]
resolve({
status: 'fulfilled',
data: Object.assign({}, resolveData[0], resolveData[1], { concept_indexes })
})
})
}
defineExpose({
validateForm
})
</script>
<template>
<div>
<SectionTitle class="mb-18">项目配置</SectionTitle>
<a-form ref="projectFormRef" :model="projectFormModel" :rules="projectFormRules">
<a-row>
<a-col :span="14">
<a-form-item label="品类品牌" name="surveyCategoryStr" required>
<a-select
v-model:value="projectFormModel.surveyCategoryStr"
placeholder="请选择测试品类"
class="custom-select"
style="margin-right: 6px"
:getPopupContainer="getPopupContainer"
>
<a-select-option
v-for="(item, index) in categoryList"
:key="index"
:value="item.title"
@click="onSelectCategory(item)"
>
{{ item.title }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item name="surveyBrandId">
<a-select
v-model:value="projectFormModel.surveyBrandId"
class="custom-select"
placeholder="请选择测试品牌"
style="margin-left: 6px"
:getPopupContainer="getPopupContainer"
>
<a-select-option
v-for="(item, index) in brandList"
:key="index"
:value="String(item.id)"
@click="onSelectBrand(item)"
>
{{ item.title }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="测试版本" name="testType">
<a-radio-group v-model:value="projectFormModel.testType">
<a-radio
:value="item.id"
v-for="item in testTypeList"
:key="item.id"
@click="onChangeTestType(item)"
>
{{ item.name }}
</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
<a-divider style="margin: 18px 0; border-top: 1px solid #e8e8e8" />
<SectionTitle class="mb-18">问卷配置</SectionTitle>
<SurveyConfig
ref="surveyConfigRef"
:projectConfig="projectFormModel"
:scene-code="props.sceneCode"
:suffix-title="props.suffixTitle"
/>
<a-divider style="margin: 18px 0; border-top: 1px solid #e8e8e8" />
<SectionTitle class="mb-18">
<div class="section-title-inner">
<span>指标配置</span>
<a-tooltip
placement="top"
title="您在此处勾选拟调研的指标,每个指标对应一道问题,问卷将针对每个概念分别询问被访者相关问题。特别的,若勾选[属性具体评价]则会针对每个口味的属性询问被访者的评价"
:overlayStyle="{ width: '250px', 'font-weight': 400 }"
:getPopupContainer="getPopupContainer"
>
<i class="iconfont icon-xingzhuangjiehe2"></i>
</a-tooltip>
<div class="flex-auto text-right">
<a-checkbox
v-if="projectFormModel.testType === testTypeEnum.standard"
class="custom-checkbox"
:indeterminate="isIndeterminate"
:checked="isCheckedAll"
@change="toggleCheckAll"
>
全选
</a-checkbox>
</div>
</div>
</SectionTitle>
<div class="target-box-content">
<div v-if="projectFormModel.testType === testTypeEnum.standard">
<a-checkbox-group v-model:value="standardChecked">
<div class="check-box">
<div v-for="item in standardIndicatorList" :key="item.value" class="check-item">
<a-checkbox
class="custom-checkbox my-checkbox"
:value="item.value"
:disabled="item.disabled"
>
<span :class="{ 'check-label': item.disabled }">
{{ item.label }}
</span>
</a-checkbox>
</div>
<div v-for="item in 10" :key="item" class="check-item"></div>
</div>
</a-checkbox-group>
</div>
<div v-if="projectFormModel.testType === testTypeEnum.quickTest">
<a-checkbox-group v-model:value="quickTestChecked">
<div class="check-box">
<div v-for="item in quickTestIndicatorList" :key="item.value" class="check-item">
<a-checkbox
class="custom-checkbox my-checkbox"
:value="item.value"
:disabled="item.disabled"
>
<span :class="{ 'check-label': item.disabled }">
{{ item.label }}
</span>
</a-checkbox>
</div>
<div v-for="item in 10" :key="item" class="check-item"></div>
</div>
</a-checkbox-group>
</div>
<div v-if="projectFormModel.testType === testTypeEnum.pair">
<a-checkbox-group v-model:value="pairChecked">
<div class="check-box">
<div
v-for="item in pairIndicatorList"
:key="item.value"
class="check-item check-item-three"
>
<a-checkbox
class="custom-checkbox my-checkbox"
:value="item.value"
:disabled="item.disabled"
>
<span :class="{ 'check-label': item.disabled }">
{{ item.label }}
</span>
</a-checkbox>
</div>
<div v-for="item in 10" :key="item" class="check-item check-item-three"></div>
</div>
</a-checkbox-group>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.mb-18 {
margin-bottom: 18px;
}
.flex-auto {
flex: auto;
}
.text-right {
text-align: right;
}
.iconfont {
height: 16px;
line-height: 16px;
margin-left: 9px;
color: #70b936;
}
.section-title-inner {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.div-flex {
display: flex;
justify-content: space-between;
}
.check-box {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.check-item {
width: 150px;
font-size: 14px;
display: flex;
align-content: center;
:deep(.ant-checkbox-wrapper) {
span {
font-size: 14px;
}
}
}
.check-item-three {
width: 200px;
}
.concept-box {
margin-top: 18px;
}
.check-label {
&::after {
display: inline-block;
margin-left: 4px;
color: #ff4d4f;
font-size: 12px;
content: '*';
}
}
.my-checkbox {
:deep(.ant-checkbox-disabled .ant-checkbox-inner) {
background-color: #d5ebc3 !important;
border-color: #d5ebc3 !important;
}
}
</style>

View File

@@ -0,0 +1,468 @@
<template>
<a-form ref="formRef" :model="ruleForm" :rules="rules" :label-col="{ span: 4 }">
<a-form-item label="问卷名称" name="project_name">
<a-input
class="project-input"
v-model:value="ruleForm.project_name"
placeholder="请输入问卷名称"
:maxlength="30"
showCount
>
<template #suffix>
<span class="suffix">
{{ `${ruleForm.project_name.length} / 30` }}
</span>
</template>
</a-input>
<a-button class="auto-button" @click="onAutoCreate">自动生成</a-button>
</a-form-item>
<a-form-item label="问卷场景" name="scene_code_info" v-if="isShow">
<a-select
disabled
v-model:value="ruleForm.scene_code_info"
style="width: 100%; border-radius: 4px"
placeholder="请选择场景"
@change="handleSceneChange"
class="custom-select show-select"
:dropdownStyle="{ zIndex: 10000 }"
>
<a-select-option
:value="`${item.code}`"
:label="item.title"
v-for="item in scenesList"
:key="`${item.code}`"
>
{{ item.parentTitle }}-{{ item.title }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="问卷标签" name="tags" v-if="isShow">
<a-select
v-model:value="ruleForm.tags"
style="width: 100%; border-radius: 4px"
mode="multiple"
placeholder="搜索或新建标签"
@change="handleChange"
:filterOption="filterOption"
class="custom-select show-select"
:dropdownStyle="{ zIndex: 10000 }"
>
<a-select-option
:value="item.id"
:label="item.title"
v-for="item in tagsList"
:key="item.id"
>
<div style="display: flex; justify-content: space-between">
<span :style="countColor(item.color)" :title="item.title">{{ item.title }}</span>
<span class="icon" v-show="isAdmin">
<EditOutlined class="edit" @click.stop="edit(item)" />
<DeleteOutlined class="del" @click.stop="del(item.id)" />
</span>
</div>
</a-select-option>
<template #dropdownRender="{ menuNode: menu }" v-if="isAdmin">
<v-nodes :vnodes="menu" />
<a-divider style="margin: 4px 0" />
<div
style="padding: 2px 8px; cursor: pointer; color: #70b936"
@mousedown="(e) => e.preventDefault()"
@click="onAddTag"
>
<plus-outlined />
新建标签
</div>
</template>
</a-select>
</a-form-item>
<a-form-item label="问卷简介" name="remarks" v-if="isShow">
<a-input
style="border-radius: 4px"
autoSize
:maxlength="150"
placeholder="请输入"
allowClear
v-model:value="ruleForm.remarks"
>
<template #suffix>
<span class="suffix">
{{ `${ruleForm.remarks.length} / 150` }}
</span>
</template>
</a-input>
</a-form-item>
<!-- <a-form-item class="button" style="text-align: right">
<a-button style="margin-right: 12px;border-radius: 4px;" type="default" @click="$emit('cancel')">取消</a-button>
<a-button type="primary" style="border-radius: 4px;" @click="onSubmit">确定</a-button>
</a-form-item> -->
</a-form>
<a-modal v-model:visible="visibleTags" title="新建标签" :destroyOnClose="true" :footer="null">
<addTag @cancel="visibleTags = false" @update="addTagUpdata"></addTag>
</a-modal>
</template>
<script>
import { defineComponent, reactive, ref, watch, onBeforeMount, createVNode } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { message, Modal } from 'ant-design-vue'
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue'
import { getTagsList, deleteTags, getSceneListForSelect } from '@/views/ProjectManage/api'
import addTag from '@/views/ProjectManage/components/addTag.vue'
import useEmitter from '@/composables/useEmitter'
export default defineComponent({
name: 'StandardConceptConfig',
components: {
addTag,
PlusOutlined,
DeleteOutlined,
EditOutlined,
VNodes: (_, { attrs }) => {
return attrs.vnodes
}
},
props: {
projectConfig: {
type: Object,
default: () => {}
},
sceneCode: { type: [Number, String] },
suffixTitle: { type: String, default: '' }
},
setup(props, context) {
const store = useStore()
const emitter = useEmitter()
const items = ref([])
const router = useRouter()
const formRef = ref()
const tagsList = ref([])
const loading = ref(false)
const scenesList = ref([])
const isAdmin = ref(false)
const isShow = ref(true)
const ruleForm = reactive({
project_name: '',
tags: [],
scene_code_info: props.sceneCode,
remarks: ''
})
const rules = {
project_name: [
{ required: true, message: '请输入问卷名称', trigger: 'blur' },
{ min: 1, max: 30, message: '字数超过限制', trigger: 'blur' }
],
scene_code_info: [{ required: true, message: '请选择场景', trigger: 'blur' }]
}
const visibleTags = ref(false)
// 标签颜色
const countColor = (value) => {
let style = {}
switch (value) {
case 1:
style = {
color: '#4DB8FA',
border: '1px solid #4DB8FA',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
break
case 2:
style = {
color: '#0CC126',
// 'border-color' : '#0CC126',
border: '1px solid #0CC126',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
break
case 3:
style = {
color: '#FF8800',
// 'border-color' : '#FF8800',
border: '1px solid #FF8800',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
break
case 4:
style = {
color: '#FF374F',
// 'border-color' : '#FF374F',
border: '1px solid #FF374F',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
break
case 5:
style = {
color: '#1C6FFF',
// 'border-color' : '#1C6FFF',
border: '1px solid #1C6FFF',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
break
case 6:
style = {
color: '#11AEA7',
// 'border-color' : '#11AEA7',
border: '1px solid #11AEA7',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
break
case 7:
style = {
color: '#25D8C8',
// 'border-color' : '#25D8C8',
border: '1px solid #25D8C8',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
break
case 8:
style = {
color: '#FECB0D',
// 'border-color' : '#FECB0D',
border: '1px solid #FECB0D',
padding: '0 5px',
'border-radius': '4px',
'line-height': '20px'
}
// break;
// case 9:
// style = {
// 'color' : '4DB8FA',
// 'border-color' : '4DB8FA',
// }
}
return style
}
/** 获取项目标签列表 */
const getTagsListRequest = async (val) => {
try {
const { data } = await getTagsList()
tagsList.value = data
} catch (error) {
message.error(error.data?.message || error.message || '服务器错误')
}
}
// 删除
const delRequest = async (id) => {
try {
const { data } = await deleteTags(id)
getTagsListRequest()
} catch (error) {
console.log(error)
}
}
const del = (id) => {
Modal.confirm({
content: '删除后,此标签会从当前团队的所有问卷中移除,且无法恢复!',
icon: () => createVNode(ExclamationCircleOutlined),
cancelText: '取消',
okText: '确认',
zIndex: 100001,
onOk: () => {
delRequest(id)
}
})
}
const edit = (item) => {
context.emit('labelEdit', item)
}
const addItem = () => {
emitter.emit('addGroup')
}
const onAddTag = () => {
visibleTags.value = true
}
const addTagUpdata = () => {
visibleTags.value = false
getTagsListRequest()
}
// const onSubmitStatus = () => {
// formRef.value
// .validate()
// .then(() => {
// })
// .catch((error) => {
// console.log("error", error);
// });
// };
const handleChange = (value) => {}
const handleSceneChange = (value) => {
const item = scenesList.value.find((item) => item.code === +value)
ruleForm.scene_code = item.parentCode
}
const getScenes = async () => {
loading.value = true
const data = await getSceneListForSelect()
if (data?.code) {
message.error(data?.message || '获取场景失败,请刷新!')
return
}
loading.value = false
scenesList.value = normalizeScenes(data?.data || [])
}
function normalizeScenes(list) {
const result = []
const parent = []
let index = 0
list.forEach((item) => {
if (item.parentCode > 0) {
// if(!item.sn){
result.push(item)
// }
} else {
parent.push(item)
}
})
result.forEach((item) => {
item.parentTitle = parent.find((pItem) => pItem.code === item.parentCode).title.slice(0, 2)
})
result.sort((a, b) => a.sort - b.sort)
return result
}
function filterOption(inputValue, option) {
const reg = new RegExp(inputValue)
const result = reg.test(option.label)
return result
}
// 自动生成
const onAutoCreate = () => {
if (!props?.projectConfig?.surveyBrandId || !props?.projectConfig?.surveyCategoryStr) {
return message.error('请先填写完全项目信息')
}
// const testTypeStr = props?.projectConfig?.testType == 1 ? '标准版' : props?.projectConfig?.testType == 2 ? '快测版' : props?.projectConfig?.testType == 3 ? '配对版' : ''
const newTitle = `${getNowTime()}${props?.projectConfig?.surveyBrandStr}${
props?.projectConfig?.surveyCategoryStr
}${props.suffixTitle}`
ruleForm.project_name = newTitle
}
// 获取当前时间 年月
const getNowTime = () => {
const yy = new Date().getFullYear()
const MM = new Date().getMonth() + 1
return yy + '年' + MM + '月'
}
onBeforeMount(() => {
getTagsListRequest()
getScenes()
var user = localStorage.getItem('plantUserInfo')
user = JSON.parse(user)
isAdmin.value = user?.super_admin_flag
})
return {
formRef,
rules,
ruleForm,
// onSubmitStatus,
loading,
value: ref([]),
handleChange,
handleSceneChange,
options: [],
del,
edit,
items,
addItem,
onAddTag,
tagsList,
filterOption,
getTagsListRequest,
visibleTags,
addTagUpdata,
countColor,
isShow,
scenesList,
getScenes,
isAdmin,
onAutoCreate
}
}
})
</script>
<style lang="scss" scoped>
.icon {
font-size: 16px;
.del {
margin: 0 10px;
}
}
.tags {
margin: 0 5px 5px;
min-width: 75px;
width: 48px;
height: 19px;
border-radius: 4px;
// position: relative;
box-sizing: border-box;
.title {
// position: absolute;
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
line-height: 17px;
display: flex;
justify-content: center;
}
.tagStyle {
border-radius: 4px;
max-width: 120px;
padding: 0 5px;
margin: 0 5px;
display: inline-block;
border-width: 1px;
border-style: solid;
color: #4db8fa;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.show-select {
.icon {
display: none !important;
}
}
::v-deep .ant-input-textarea-clear-icon {
margin: 4px 3px 0 0;
}
.show-select:deep(.ant-select-selector) {
border-radius: 4px;
}
.project-input {
width: 80%;
border-radius: 4px 0 0 4px;
// border-right: 0px;
}
.auto-button {
width: 20%;
border-radius: 0 4px 4px 0;
}
</style>

View File

@@ -0,0 +1,185 @@
<script setup>
import { computed, defineProps } from 'vue'
import conceptJson from '../json/concept'
import tasteJson from '../json/taste'
import packageJson from '../json/package'
import { schemeLabelEnum, testTypeLabelEnum } from '../consts'
const props = defineProps({
testType: { type: [Number, String], default: 0 },
schemeType: { type: [Number, String], default: 0 }
})
const schemeTypeStr = computed(() => {
return schemeLabelEnum[props.schemeType] || ''
})
const testTypeTitle = computed(() => {
return testTypeLabelEnum[props.testType] || ''
})
const questionList = computed(() => {
if (!props.testType || !props.schemeType) {
return []
}
const questionIndex = `questions_${['', 'standard', 'quick_test', 'pair'][props.testType]}`
return [{}, conceptJson, tasteJson, packageJson][props.schemeType][questionIndex]
})
</script>
<template>
<div class="template-content-box">
<div class="head-title">
<div class="head-title-one">
液态奶产品研究标准化问卷-{{ schemeTypeStr }}
<span class="bold">内测{{ testTypeTitle }}</span>
</div>
<div class="head-title-two">样本量要求60</div>
</div>
<div
class="question-content"
v-for="(item, index) in questionList"
:key="index"
:class="{ nb: item.noBottom }"
>
<div class="question-title" v-if="item?.seq || item?.title">
<span v-if="item?.seq" class="question-seq">{{ item.seq }}</span>
<span v-if="item?.title" v-html="item.title"></span>
</div>
<div v-if="item.subTitle" class="question-sub-title mb-8">
<span>{{ item.subTitle }}</span>
</div>
<div class="mb-8" v-if="item?.topic">{{ item.topic }}</div>
<div class="question-tip mb-8" v-if="item?.tip">{{ item.tip }}</div>
<div
class="question-ans mb-8"
v-for="it in item.ans"
:key="it"
:class="{ 'group-title': it.isGroupTitle }"
>
<span v-if="it.isGroupTitle">{{ it.title }}</span>
<span v-else>{{ it }}</span>
</div>
<div class="div-center mb-8" v-if="item?.center">{{ item.center }}</div>
<div class="div-table" v-if="item?.table">
<table>
<thead>
<tr>
<th v-for="(header, index) in item.table.headers" :key="index">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in item.table.rows" :key="rowIndex">
<td v-for="(col, colIndex) in row" :key="colIndex">
{{ col }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.template-content-box {
.head-title {
text-align: center;
.head-title-one {
font-size: 16px;
margin-top: 24px;
.bold {
font-size: 16px;
font-weight: bold;
}
}
.head-title-two {
font-size: 16px;
margin-top: 8px;
margin-bottom: 22px;
}
}
.question-content {
&:not(.nb) {
margin-bottom: 24px;
}
.question-seq {
margin-right: 5px;
font-family: 'PingFang SC', sans-serif;
font-weight: bold;
}
.question-title {
font-size: 14px;
margin-bottom: 6px;
}
.question-tip {
color: #70b936;
}
.question-ans {
font-size: 12px;
}
.group-title {
padding: 12px 0 4px;
font-size: 14px;
font-family: 'PingFang SC', sans-serif;
font-weight: bold;
}
}
}
.mb-8 {
margin-bottom: 8px;
}
.div-center {
width: 100%;
text-align: center;
}
.div-table {
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border: 1px solid #cccccc;
padding: 10px;
text-align: center;
font-size: 12px;
}
th {
background-color: #f8f8f8;
font-weight: bold;
// color: red;
}
td:first-child {
text-align: left;
}
td {
// width: 33%;
}
}
</style>

View File

@@ -0,0 +1,174 @@
<script setup>
import { defineEmits, ref, defineProps, watch } from 'vue'
import { Modal } from 'ant-design-vue'
import SectionTitle from '@/components/layout/title/SectionTitle.vue'
import SurveySchemeTemplate from './SurveySchemeTemplate.vue'
const emits = defineEmits(['update:visible', 'save'])
const props = defineProps({
visible: { type: Boolean, default: false },
schemeType: { type: [Number, String] }, // 概念,口味,包装
testType: { type: Number } // 1:'标准版' 2:'快测版' 3:'配对版'
})
const shown = ref(false)
watch(
() => props.visible,
() => {
shown.value = !!props.visible
},
{ immediate: true }
)
const templateScrollerRef = ref(null)
watch(
() => props.testType,
() => {
templateScrollerRef.value?.scrollTo?.(0, 0)
}
)
function onClose() {
Modal.confirm({
title: '确定退出?',
content: '退出后当前方案配置无法恢复!',
cancelText: '取 消',
okText: '确 定',
closable: true,
class: 'custom-modal custom-modal-title-confirm-notice',
onOk: closeModal,
onCancel: () => {}
})
}
function closeModal() {
shown.value = false
emits('update:visible', false)
}
function onSave() {
emits('save')
}
</script>
<template>
<a-modal
:visible="shown"
:maskClosable="false"
:destroyOnClose="true"
:closable="false"
:footer="null"
width="100%"
wrapClassName="scheme-config-fullscreen-modal"
>
<div class="layout">
<!-- 头部导航栏 -->
<div class="header">
<div class="left" @click="onClose">
<i class="iconfont" style="font-size: 20px">&#xe6c0;</i>
<span>退出方案配置</span>
</div>
<div class="action-container">
<a-button type="primary" class="custom-button" @click="onSave">保存方案</a-button>
</div>
</div>
<div class="content-box">
<!-- 配置 -->
<div class="content scrollbar">
<slot></slot>
</div>
<!-- 模版详情 -->
<div ref="templateScrollerRef" class="content scrollbar">
<div class="template-box">
<SectionTitle>模板详情</SectionTitle>
<SurveySchemeTemplate :schemeType="props.schemeType" :test-type="props.testType" />
</div>
</div>
</div>
</div>
</a-modal>
</template>
<style scoped lang="scss">
.layout {
min-width: 1200px;
background-color: #f5f5f5;
.header {
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px 0 24px;
background: #ffffff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
.left {
cursor: pointer;
display: flex;
align-items: center;
.iconfont {
font-size: 20px;
color: #434343;
}
span {
font-size: 16px;
color: #434343;
line-height: 24px;
margin-left: 8px;
}
}
.action-container {
display: flex;
align-items: center;
}
}
.content-box {
padding: 24px;
display: flex;
justify-content: space-between;
.content {
width: calc(50% - 12px);
padding: 22px;
height: calc(100vh - 150px);
overflow: auto;
overflow: overlay;
background-color: #ffffff;
border-radius: 6px;
position: relative;
transition: all 0.2s;
max-width: 1280px;
}
}
}
</style>
<style lang="scss">
.scheme-config-fullscreen-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
}
.ant-modal-body {
flex: 1;
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,29 @@
export const schemeEnum = {
concept: 1,
taste: 2,
package: 3
}
export const schemeLabelEnum = {
1: '概念测试',
2: '口味测试',
3: '包装测试'
}
export const testTypeEnum = {
standard: 1,
quickTest: 2,
pair: 3
}
export const testTypeLabelEnum = {
1: '标准版',
2: '快测版',
3: '配对版'
}
export const testTypeList = [
{ id: testTypeEnum.standard, name: testTypeLabelEnum[testTypeEnum.standard] },
{ id: testTypeEnum.quickTest, name: testTypeLabelEnum[testTypeEnum.quickTest] },
{ id: testTypeEnum.pair, name: testTypeLabelEnum[testTypeEnum.pair] }
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,403 @@
<script setup>
import { defineExpose, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { message, Modal } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { v4 as uuid } from 'uuid'
import draggable from 'vuedraggable'
import SectionTitle from '@/components/layout/title/SectionTitle.vue'
import TemplateModal from '../components/TemplateModal.vue'
import BasicConfig from '../components/BasicConfig.vue'
import TasteTestItem from './TasteTestItem.vue'
import { schemeEnum, testTypeEnum } from '../consts'
import { tasteTypeEnum } from './consts'
import { postTemplates } from '../api'
const store = useStore()
const router = useRouter()
const shown = ref(false)
const testType = ref(testTypeEnum.standard)
const list1 = ref([getNewItem(tasteTypeEnum.standard), getNewItem(tasteTypeEnum.newest)])
const list2 = ref([getNewItem(tasteTypeEnum.standard), getNewItem(tasteTypeEnum.newest)])
const list3 = ref([getNewItem(tasteTypeEnum.standard), getNewItem(tasteTypeEnum.newest)])
const sceneCode = ref('')
const templateSn = ref('')
const templateSnList = ref([])
function openModal({ type, sn, snList, sceneCode: scene_code }) {
sceneCode.value = scene_code
templateSn.value = sn
templateSnList.value = snList || []
if (type) {
testType.value = type
}
templateSn.value = getSnByTestType(testType.value)
shown.value = true
}
defineExpose({
openModal
})
function getPopupContainer(el) {
return el.parentNode.parentNode || document.body
}
function getSnByTestType(type) {
return {
[testTypeEnum.standard]: templateSnList.value[0],
[testTypeEnum.quickTest]: templateSnList.value[1],
[testTypeEnum.pair]: templateSnList.value[2]
}[type]
}
function onTestTypeChange(evt) {
testType.value = evt.id
templateSn.value = getSnByTestType(evt.id)
}
function getNewItem(type) {
type = type ?? tasteTypeEnum.newest
return {
key: uuid(),
data: {
taste_type: type, // 口味类型 0标杆口味 1新品口味
taste_encode: '', // 口味编号
taste_name: '', // 口味名称
taste_attr_indicator: ['', ''] // 关键属性指标(字符串列表)
}
}
}
function onAddItem() {
const item = getNewItem(tasteTypeEnum.newest)
switch (testType.value) {
case testTypeEnum.standard:
list1.value.push(item)
break
case testTypeEnum.quickTest:
list2.value.push(item)
break
case testTypeEnum.pair:
break
}
}
function onDeleteItem(key) {
switch (testType.value) {
case testTypeEnum.standard:
const result1 = list1.value.filter((i) => i.key !== key)
if (checkTasteType(result1)) {
list1.value = result1
}
break
case testTypeEnum.quickTest:
const result2 = list2.value.filter((i) => i.key !== key)
if (checkTasteType(result2)) {
list1.value = result2
}
break
case testTypeEnum.pair:
break
}
}
// 判断 taste_type 是否至少有一个 0 和一个 1
function checkTasteType(list) {
const standardList = []
const newestList = []
const newList = JSON.parse(JSON.stringify(list))
newList.forEach((i) => {
switch (i.data.taste_type) {
case tasteTypeEnum.standard:
standardList.push(i)
break
case tasteTypeEnum.newest:
newestList.push(i)
break
}
})
if (!standardList.length) {
message.error('至少配置一个标杆口味')
return false
}
if (!newestList.length) {
message.error('至少配置一个新品口味')
return false
}
return true
}
function checkTaste() {
const list = {
[testTypeEnum.standard]: list1.value,
[testTypeEnum.quickTest]: list2.value,
[testTypeEnum.pair]: list3.value
}[testType.value]
// 检查是否有空名称
const names = list.map((item) => item.data.taste_name)
const emptyNameExists = names.some((name) => !name.trim())
if (emptyNameExists) {
message.error('口味名称不可为空,请检查后重试!')
return false
}
// 检查是否有重复名称
const nameCounts = list
.map((item) => item.data.taste_name)
.reduce((acc, name) => {
acc[name] = (acc[name] || 0) + 1
return acc
}, {})
const duplicateNames = Object.keys(nameCounts).filter((name) => nameCounts[name] > 1)
if (duplicateNames.length > 0) {
message.error('口味名称不可重复,请检查后重试!')
return false
}
// 检查是否有空关键属性指标
const indicators = list.flatMap((item) => item.data.taste_attr_indicator)
const emptyIndicatorsExists = indicators.some((name) => !name.trim())
if (emptyIndicatorsExists) {
message.error('关键属性指标不可为空,请检查后重试!')
return false
}
// 检查同一口味下是否有重复的关键属性指标
if (
list.some((item) => {
item.data.taste_attr_indicator
const idt = item.data.taste_attr_indicator
return Array.from(new Set(idt)).length !== idt.length
})
) {
message.error('同一口味下的关键属性指标不可重复,请检查后重试!')
return false
}
return [testTypeEnum.standard, testTypeEnum.quickTest].includes(testType.value)
? checkTasteType(list)
: true
}
const basicConfigRef = ref(null)
async function onSave() {
const result = await basicConfigRef.value.validateForm()
if (result.status === 'rejected') {
message.error(result.reason.flatMap((i) => i.errors).join('、'))
return
}
if (!checkTaste()) {
return
}
const list = {
[testTypeEnum.standard]: list1.value,
[testTypeEnum.quickTest]: list2.value,
[testTypeEnum.pair]: list3.value
}[testType.value].map((i) => Object.assign({}, i.data, { taste_encode: '' }))
const params = {
brand_id: result.data.brand_id,
scene_code_info: result.data.scene_code_info,
sn: templateSn.value,
project_name: result.data.project_name || '',
tags: result.data.tags || [],
remarks: result.data.remarks || '',
concept_indexes: (result.data.concept_indexes || []).map((i) =>
Object.assign({
type: i,
is_select: 1
})
),
study_tastes: list
}
const confirmModal = Modal.confirm({
title: '确定保存?',
content: '保存方案后无法编辑,请您再次确认已完成最终配置。',
cancelText: '取 消',
okText: '确 定',
closable: true,
class: 'custom-modal custom-modal-title-confirm-notice',
onOk: async () => {
confirmModal.update({
cancelButtonProps: { disabled: true }
})
const res = await postTemplates(templateSn.value, params).catch(() => '')
if (res.code === 0) {
store.commit('common/M_COMMON_SET_SURVEY_STATUS', 0)
router.push({
path: '/survey/planet/design',
query: { sn: res.data.sn }
})
}
confirmModal.update({
cancelButtonProps: { disabled: false }
})
},
onCancel: () => {}
})
}
</script>
<template>
<TemplateModal
v-model:visible="shown"
:scheme-type="schemeEnum.taste"
:test-type="testType"
@save="onSave"
>
<BasicConfig
ref="basicConfigRef"
:scheme-type="schemeEnum.taste"
:scene-code="sceneCode"
suffix-title="口味测试"
@testtypechange="onTestTypeChange"
/>
<a-divider style="margin: 18px 0; border-top: 1px solid #e8e8e8" />
<SectionTitle class="mb-10">
<div class="flex full-width">
<div>
<span>口味配置</span>
<a-tooltip
placement="top"
title="您在此处勾选拟调研的口味,问卷将针对每个概念分别询问被访者相关问题"
:overlayStyle="{ width: '250px', 'font-weight': 400 }"
:getPopupContainer="getPopupContainer"
>
<i class="iconfont icon-xingzhuangjiehe2"></i>
</a-tooltip>
</div>
<div class="flex-auto flex-end">
<a-button
v-if="[testTypeEnum.standard, testTypeEnum.quickTest].includes(testType)"
class="custom-button"
type="primary"
@click="onAddItem"
>
<PlusOutlined />
新增口味
</a-button>
</div>
</div>
</SectionTitle>
<!-- 标准版 -->
<draggable
v-show="testType === testTypeEnum.standard"
v-model="list1"
item-key="key"
animation="300"
:scroll="true"
class="drag-box"
handle=".draggable-1"
>
<template #item="{ element }">
<TasteTestItem
v-model="element.data"
:key="element.key"
:item-key="element.key"
:list="list1"
need-type
@changeType="onChangeType"
>
<div class="actions">
<i class="iconfont draggable-1">&#xe71b;</i>
<i class="iconfont" @click="onDeleteItem(element.key)">&#xe6b5;</i>
</div>
</TasteTestItem>
</template>
</draggable>
<!-- 快测版 -->
<draggable
v-show="testType === testTypeEnum.quickTest"
v-model="list2"
item-key="key"
animation="300"
:scroll="true"
class="drag-box"
handle=".draggable-2"
>
<template #item="{ element }">
<TasteTestItem
v-model="element.data"
:key="element.key"
:item-key="element.key"
:list="list2"
need-type
>
<div class="actions">
<i class="iconfont draggable-2">&#xe71b;</i>
<i class="iconfont" @click="onDeleteItem(element.key)">&#xe6b5;</i>
</div>
</TasteTestItem>
</template>
</draggable>
<!-- 配对版 -->
<draggable
v-show="testType === testTypeEnum.pair"
v-model="list3"
item-key="key"
animation="300"
:scroll="true"
class="drag-box"
handle=".draggable-3"
>
<template #item="{ element }">
<TasteTestItem
v-model="element.data"
:key="element.key"
:item-key="element.key"
:list="list3"
>
<div class="actions">
<i class="iconfont draggable-3">&#xe71b;</i>
</div>
</TasteTestItem>
</template>
</draggable>
</TemplateModal>
</template>
<style scoped lang="scss">
.mb-10 {
margin-bottom: 10px;
}
.iconfont {
height: 16px;
line-height: 16px;
margin-left: 9px;
color: #70b936;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,265 @@
<script setup>
import { defineEmits, defineProps, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'
import draggable from 'vuedraggable'
import MultipleLineInputModal from '@/components/input/MultipleLineInputModal.vue'
import { tasteTypeEnum } from './consts'
const emits = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: { type: Object, default: () => Object.assign({}) },
list: { type: Array, default: () => [] },
itemKey: { type: String, default: '' },
needType: { type: Boolean, default: false } // 显示 “标杆口味/新品口味” 切换按钮
})
const item = ref({ ...props.modelValue })
watch(
item,
() => {
emits('update:modelValue', item.value)
},
{ deep: true }
)
if (!item.value.taste_attr_indicator?.length) {
item.value.taste_attr_indicator = ['']
}
function onChangeType(type) {
const standardList = []
const newestList = []
const newList = JSON.parse(JSON.stringify(props.list))
newList.forEach((i) => {
if (i.key === props.itemKey) {
i.data.taste_type = type
}
switch (i.data.taste_type) {
case tasteTypeEnum.standard:
standardList.push(i)
break
case tasteTypeEnum.newest:
newestList.push(i)
break
}
})
if (!standardList.length) {
message.error('至少配置一个标杆口味')
return
}
if (!newestList.length) {
message.error('至少配置一个新品口味')
return
}
item.value.taste_type = type
}
function onAddAttr() {
item.value.taste_attr_indicator.push('')
}
const batchManagementShown = ref(false)
function onBatchManagement() {
batchManagementShown.value = true
}
function onDeleteIndicator(index) {
item.value.taste_attr_indicator.splice(index, 1)
}
</script>
<template>
<div class="taste-test-item mb-10">
<div class="header flex">
<div class="tabs">
<template v-if="props.needType">
<div
v-if="!conceptItem?.noType"
class="tab-btn"
:class="{ active: item.taste_type === tasteTypeEnum.standard }"
@click="onChangeType(tasteTypeEnum.standard)"
>
标杆口味
</div>
<div
v-if="!conceptItem?.noType"
class="tab-btn"
:class="{ active: item.taste_type === tasteTypeEnum.newest }"
@click="onChangeType(tasteTypeEnum.newest)"
>
新品口味
</div>
</template>
</div>
<div class="actions">
<slot></slot>
</div>
</div>
<div class="flex item-row mb-10">
<div class="flex-none label">口味名称</div>
<a-input
v-model:value="item.taste_name"
:maxlength="30"
placeholder="请输入口味名称例如“AMX常温益生菌酸奶 青梅味”"
class="custom-input"
>
<template #suffix>
<span class="suffix">
{{ `${item.taste_name?.length || 0} / 30` }}
</span>
</template>
</a-input>
</div>
<div class="flex item-row">
<div class="flex-none label align-start pt-5">关键属性指标</div>
<div class="flex-auto">
<draggable
v-model="item.taste_attr_indicator"
item-key="index"
animation="300"
:scroll="true"
class="drag-box"
:handle="`.drag-handler-indicator-${props.itemKey}`"
>
<template #item="{ index }">
<div class="flex">
<a-input
v-model:value="item.taste_attr_indicator[index]"
:maxlength="30"
placeholder="请输入关键属性指标,例如“奶香味(喝起来奶香浓醇)”"
class="custom-input mb-10"
>
<template #suffix>
<span class="suffix">
{{ `${indicator?.length || 0} / 30` }}
</span>
</template>
</a-input>
<i class="iconfont" :class="[`drag-handler-indicator-${props.itemKey}`]">&#xe71b;</i>
<i class="iconfont" @click="onDeleteIndicator(index)">&#xe6b5;</i>
</div>
</template>
</draggable>
</div>
</div>
<div class="flex item-row">
<div class="flex-none label"></div>
<div class="flex-auto flex-start">
<a-button
type="text"
:disabled="item?.taste_attr_indicator?.length >= 20"
class="custom-button"
@click="onAddAttr"
>
<PlusOutlined />
<span>添加属性</span>
</a-button>
<a-button
type="text"
:disabled="item?.taste_attr_indicator?.length >= 20"
class="custom-button"
@click="onBatchManagement"
>
<UnorderedListOutlined />
<span>批量管理</span>
</a-button>
</div>
</div>
<MultipleLineInputModal
v-model:visible="batchManagementShown"
title="批量管理"
:min-line="1"
:max-line="20"
:text-arr="item.taste_attr_indicator"
@change="item.taste_attr_indicator = $event"
>
<template #messageTop>
<div class="mb-10">每行一个口味关键属性指标每个口味的关键属性指标最少一个最多20个</div>
</template>
</MultipleLineInputModal>
</div>
</template>
<style scoped lang="scss">
.mb-10 {
margin-bottom: 10px;
}
.align-start {
align-self: flex-start;
}
.pt-5 {
padding-top: 5px;
}
.iconfont {
height: 16px;
line-height: 16px;
margin-left: 9px;
color: #70b936;
cursor: pointer;
}
.taste-test-item {
position: relative;
padding: 14px;
border-radius: 8px;
background: #f9f9f9;
.header {
margin-bottom: 14px;
}
.tabs {
display: flex;
justify-content: flex-start;
.tab-btn {
width: 68px;
height: 26px;
line-height: 26px;
text-align: center;
background-color: #ffffff;
cursor: pointer;
&:first-child {
border-radius: 4px 0 0 4px;
}
&:last-child {
border-radius: 0 4px 4px 0;
}
}
.active {
color: #ffffff;
background-color: #70b936;
}
}
.label {
width: 100px;
white-space: nowrap;
padding-right: 10px;
text-align: right;
}
.custom-input {
border: none;
}
}
</style>

View File

@@ -0,0 +1,4 @@
export const tasteTypeEnum = {
standard: 0,
newest: 1
}

View File

@@ -63,7 +63,7 @@
<a-dropdown>
<a
class="ant-dropdown-link"
:class="{ 'ant-dropdown-link-disable': [1, 100, 101, 200, 201, 300, 301, 302].includes(record.type) || (record.type === 0 && record.more_actions === 0)}"
:class="{ 'ant-dropdown-link-disable': [1, 100, 101, 200, 201, 300, 301, 302, 500, 501, 502].includes(record.type) || (record.type === 0 && record.more_actions === 0)}"
style="display: inline-block;"
@click.prevent
>
@@ -71,7 +71,7 @@
<!-- <DownOutlined /> -->
<i class="icon iconfont">&#xe80c;</i>
</a>
<template #overlay v-if="![1, 100, 101, 200, 201, 300, 301, 302].includes(record.type) && (record.type === 0 && record.more_actions === 1)">
<template #overlay v-if="![1, 100, 101, 200, 201, 300, 301, 302, 500, 501, 502].includes(record.type) && (record.type === 0 && record.more_actions === 1)">
<a-menu>
<a-menu-item @click="handleRemove(record)">重命名模板</a-menu-item>
<a-menu-item @click="handleDel(record)">删除模板</a-menu-item>
@@ -180,9 +180,8 @@
/>
<!-- 概念测试 -->
<ConceptTesting
ref="conceptTestingRef"
/>
<ConceptTesting ref="conceptTestingRef" />
<TasteTest ref="tasteTestRef"/>
</div>
</template>
<script>
@@ -197,6 +196,10 @@ import { getGroupList } from "@/api/template-market";
import CreateSurvey from "@views/ProjectManage/components/NewCreateSurvey.vue";
import ConceptTesting from "@/views/Concept/ConceptTesting.vue"
import TasteTest from '@/views/ProjectManage/create/presets/taste/TasteTest.vue'
import { testTypeEnum } from '@/views/ProjectManage/create/presets/consts'
import {
nextTick,
@@ -283,6 +286,7 @@ export default defineComponent({
CreateSurveyProduct,
Search,
ConceptTesting,
TasteTest,
},
props: {
groupId: { type: Number, value: 0 },
@@ -313,6 +317,7 @@ export default defineComponent({
const createSurveySellRef = ref();
const createSurveyProductRef = ref();
const conceptTestingRef = ref(null); /** 概念测试 */
const tasteTestRef = ref(null)
const editLabelVisible = ref(false);
const editLabelItem = ref({});
const preview_visible = ref(false);
@@ -570,6 +575,17 @@ export default defineComponent({
conceptTestingRef.value.openModal(2, record.other?.split?.(',')?.[1], record.other?.split?.(','));
} else if (record.type === 302) {
conceptTestingRef.value.openModal(3, record.other?.split?.(',')?.[2], record.other?.split?.(','));
} else if ([500, 501, 502].includes(record.type)) {
tasteTestRef.value.openModal({
type: {
500: testTypeEnum.standard,
501: testTypeEnum.quickTest,
502: testTypeEnum.pair
}[record.type],
sn: record.sn,
snList: record.other?.split?.(',') || [],
sceneCode: '23'
})
} else {
groupInfo.value.sn = "";
groupInfo.value.group_id = 0;
@@ -702,6 +718,7 @@ export default defineComponent({
createSurveySellRef,
createSurveyProductRef,
conceptTestingRef,
tasteTestRef,
handlePreview,
handleMove,
handleRemove,