feat: 新增口味测试
This commit is contained in:
124
src/components/input/MultipleLineInputModal.vue
Normal file
124
src/components/input/MultipleLineInputModal.vue
Normal 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
37
src/style/box.scss
Normal 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%;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
18
src/views/ProjectManage/create/presets/api.js
Normal file
18
src/views/ProjectManage/create/presets/api.js
Normal 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`
|
||||
})
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"></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>
|
||||
29
src/views/ProjectManage/create/presets/consts.js
Normal file
29
src/views/ProjectManage/create/presets/consts.js
Normal 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] }
|
||||
]
|
||||
1062
src/views/ProjectManage/create/presets/json/concept.js
Normal file
1062
src/views/ProjectManage/create/presets/json/concept.js
Normal file
File diff suppressed because it is too large
Load Diff
1063
src/views/ProjectManage/create/presets/json/package.js
Normal file
1063
src/views/ProjectManage/create/presets/json/package.js
Normal file
File diff suppressed because it is too large
Load Diff
1133
src/views/ProjectManage/create/presets/json/taste.js
Normal file
1133
src/views/ProjectManage/create/presets/json/taste.js
Normal file
File diff suppressed because it is too large
Load Diff
403
src/views/ProjectManage/create/presets/taste/TasteTest.vue
Normal file
403
src/views/ProjectManage/create/presets/taste/TasteTest.vue
Normal 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"></i>
|
||||
<i class="iconfont" @click="onDeleteItem(element.key)"></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"></i>
|
||||
<i class="iconfont" @click="onDeleteItem(element.key)"></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"></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>
|
||||
265
src/views/ProjectManage/create/presets/taste/TasteTestItem.vue
Normal file
265
src/views/ProjectManage/create/presets/taste/TasteTestItem.vue
Normal 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}`]"></i>
|
||||
<i class="iconfont" @click="onDeleteIndicator(index)"></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>
|
||||
4
src/views/ProjectManage/create/presets/taste/consts.js
Normal file
4
src/views/ProjectManage/create/presets/taste/consts.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const tasteTypeEnum = {
|
||||
standard: 0,
|
||||
newest: 1
|
||||
}
|
||||
@@ -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"></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,
|
||||
|
||||
Reference in New Issue
Block a user