feat: 新增包装测试

This commit is contained in:
钱冠学
2024-11-05 10:28:32 +08:00
parent 22e1831ade
commit fc8384a74e
13 changed files with 291 additions and 168 deletions

View File

@@ -7,7 +7,10 @@
// 600=包装测试-标准(600_)601=包装测试-快速(601_)602=包装测试-配对(602_)
export const showInsightTemplateType = [300, 301, 500, 501, 600, 601]; // 显示洞察报告 tab 的 template_type
// 看板类型:1=概念标准版,2=概念快测版,3=概念配对版,4=口味标准版,5=口味快测版,6=口味配对版
// 看板类型:
// 1=概念标准版,2=概念快测版,3=概念配对版,
// 4=口味标准版,5=口味快测版,6=口味配对版
// 7=包装标准版,8=包装快测版,9=包装配对版
export const reportTypeEnum = {
concept_standard: 1,
concept_quick: 2,
@@ -32,11 +35,6 @@ export const reportTypeLabelMap = {
[reportTypeEnum.package_pair]: '包装'
};
export const sectionShownTypeEnum = {
show: 2,
hide: 1
};
// 检查是否需要显示 “洞察报告” 这个标签页
// 仅标准版和快测版问卷显示该菜单
export function checkShowInsightTab({ templateType } = {}) {
@@ -59,8 +57,29 @@ export function getInsightTypeStr(templateType) {
return labelMap[templateType] || '';
}
export const sectionShownTypeEnum = {
show: 2,
hide: 1
};
export const testItemTypeEnum = {
newest: 1,
standard: 0,
1: 'newest',
0: 'standard'
};
export const testItemTypeLabelMap = {
[testItemTypeEnum.newest]: '新品',
[testItemTypeEnum.standard]: '标杆'
};
export const reportUpdatingMessageText = '报告更新中,不能修改';
export const codeList = ['概念编码', '口味编码', '包装编码'];
export const italicCodeList = [...codeList, '样本基数'];
// 报告表格某些行需要显示选择框,并且可以修改,此列表为区分此功能的文字
export const selectionList = [
'是否通过概念行动标准',

View File

@@ -5,6 +5,7 @@ import Overview from './section/Overview.vue';
import ProjectNameAndDecisionCriteria from './section/ProjectNameAndDecisionCriteria.vue';
import TestingConcept from './section/TestingConcept.vue';
import TestingTaste from './section/TestingTaste.vue';
import TestingPackage from './section/TestingPackage.vue';
import GroupTab from './section/GroupTab.vue';
import CoreConclusion from './section/CoreConclusion.vue';
@@ -43,8 +44,10 @@ const readonly = computed(() => props.readonly || false);
const updating = computed(() => props.updating || false);
// 快测版报告内容和标准版基本一致,区别为快测版没有概念诊断部分
// 看板类型:1=标准版,2=快测版,3=配对版,添加了口味,请参考下一行
// 看板类型:1=概念标准版,2=概念快测版,3=概念配对版,4=口味标准版,5=口味快测版,6=口味配对版
// 看板类型:
// 1=概念标准版,2=概念快测版,3=概念配对版,
// 4=口味标准版,5=口味快测版,6=口味配对版
// 7=包装标准版,8=包装快测版,9=包装配对版
const type = computed(() => +props.report?.type);
const testCom = computed(() => {
@@ -57,6 +60,10 @@ const testCom = computed(() => {
case reportTypeEnum.taste_quick:
case reportTypeEnum.taste_pair:
return TestingTaste;
case reportTypeEnum.package_standard:
case reportTypeEnum.package_quick:
case reportTypeEnum.package_pair:
return TestingPackage;
}
});
@@ -69,8 +76,8 @@ const comList = computed(() => {
list.push(ProjectNameAndDecisionCriteria); // 项目名称及概念决策标准
list.push(testCom.value); // 测试概念/测试口味/测试包装
list.push(GroupTab); // tab 切换
list.push(CoreConclusion); // 核心结论
list.push(GroupTab); // tab 切换
list.push(DecisionIndicators); // 决策指标
if (

View File

@@ -1,6 +1,6 @@
<script setup>
import { computed, defineEmits, defineProps, ref, watch } from 'vue';
import { selectionList } from '../../consts';
import { selectionList, italicCodeList } from '../../consts';
import { sortListForTableColSpan } from '../../util';
const emits = defineEmits(['change']);
@@ -74,6 +74,7 @@ const headers = computed(() => {
text = text || '';
}
let idx = index;
let rowSpan = 0;
const tempForEqStr = tableData.value[index - 1]?.[rowTitle.dataIndex];
const tempForEqArr = tempForEqStr?.split?.('_')?.slice?.(0, i + 1);
@@ -88,7 +89,11 @@ const headers = computed(() => {
?.split?.('_')
?.slice?.(0, i + 1)
?.join?.('_');
return !!(index <= recordIndex && temp && fieldForEq === temp);
if (idx === recordIndex && temp && fieldForEq === temp) {
idx += 1;
return true;
}
return false;
}).length;
}
@@ -154,12 +159,12 @@ const columns = computed(() =>
column.customCell = function (record) {
const style = {};
if (columnIndex === 0) {
if (['概念编码', '口味编码', '样本基数'].includes(record[column.dataIndex])) {
if (italicCodeList.includes(record[column.dataIndex])) {
style['font-style'] = 'italic';
}
}
if (columnIndex > 0) {
if (['概念编码', '口味编码', '样本基数'].some((key) => record[column.dataIndex]?.indexOf?.(key) > -1)) {
if (italicCodeList.some((key) => record[column.dataIndex]?.indexOf?.(key) > -1)) {
style['font-style'] = 'italic';
}
}
@@ -254,7 +259,7 @@ watch(
const numericalColumns = flatColumns.value.map((key) => key.dataIndex).slice(1);
tableData.value.forEach((record) => {
if (['概念编码', '口味编码', '样本基数'].includes(record[headers.value[0]?.dataIndex])) {
if (italicCodeList.includes(record[headers.value[0]?.dataIndex])) {
return;
}
@@ -284,10 +289,10 @@ const barStyle = computed(() => (record, column) => {
});
function rowClassName(record) {
// 表格中 '概念编码'、 '口味编码' 和 '样本基数' 这几行要显示灰色
return Object.keys(record).some((key) =>
['概念编码', '口味编码', '样本基数'].some((item) => record[key]?.indexOf?.(item) > -1)
)
// 表格中 '概念编码'、 '口味编码'、 '包装编码' 和 '样本基数' 这几行要显示灰色
return Object.keys(record).some((key) => {
return italicCodeList.some((item) => record[key]?.indexOf?.(item) > -1);
})
? 'gray-row'
: '';
}
@@ -308,9 +313,11 @@ function getPopupContainer(el) {
}
function cellClass(record, column, cols) {
const columnIndex = cols.flatMap((i) => i.children || [i]).findIndex((col) => col.dataIndex === column.dataIndex);
const columnIndex = cols
.flatMap((i) => i.children || [i])
.findIndex((col) => col.dataIndex === column.dataIndex);
const isStaticCell = ['概念编码', '口味编码', '样本基数'].some((key) => record[cols[0]?.dataIndex].indexOf(key) > -1);
const isStaticCell = italicCodeList.some((key) => record[cols[0]?.dataIndex].indexOf(key) > -1);
const isStrokeCell = record[cols[0]?.dataIndex].toLowerCase().indexOf('top') > -1 && columnIndex > 0;
@@ -340,8 +347,7 @@ function cellClass(record, column, cols) {
<template v-if="props.barTable" data-desc="显示条形图">
<span
v-if="
[0].includes(columnIndex) ||
['概念编码', '口味编码', '样本基数'].includes(record[headers[0]?.dataIndex])
[0].includes(columnIndex) || italicCodeList.includes(record[headers[0]?.dataIndex])
"
:class="cellClass(record, column, columns)"
>

View File

@@ -1,13 +1,20 @@
<script setup>
import { defineEmits, defineProps, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue';
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons-vue';
import SectionTitle from '../../components/SectionTitle.vue';
import Section from '../../components/Section.vue';
import SectionTitle from '../../components/SectionTitle.vue';
import StyledTable from '../components/StyledTable.vue';
import { defaultSectionEmits, defaultSectionProps, reportUpdatingMessageText } from '../../consts';
import {
codeList,
defaultSectionEmits,
defaultSectionProps,
reportUpdatingMessageText
} from '../../consts';
import { getRowTitleColumnWidth } from '../../util';
const emits = defineEmits(defaultSectionEmits);
const props = defineProps(defaultSectionProps);
@@ -42,7 +49,7 @@ watch(
canHide: true,
visible: props.report.genderQuoteHidden === 2,
visibleField: 'genderQuoteHidden',
rowTitleColumnWidthList: [120, 280],
rowTitleColumnWidthList: getRowTitleColumnWidth(gender),
tableData: gender,
codeStr: getTableCodeRow(gender)
},
@@ -52,7 +59,7 @@ watch(
canHide: true,
visible: props.report.ageQuotaHidden === 2,
visibleField: 'ageQuotaHidden',
rowTitleColumnWidthList: [120, 280],
rowTitleColumnWidthList: getRowTitleColumnWidth(age),
tableData: age,
codeStr: getTableCodeRow(age)
},
@@ -62,7 +69,7 @@ watch(
canHide: true,
visible: props.report.incomeQuotaHidden === 2,
visibleField: 'incomeQuotaHidden',
rowTitleColumnWidthList: [160, 280],
rowTitleColumnWidthList: getRowTitleColumnWidth(income),
tableData: income,
codeStr: getTableCodeRow(income)
},
@@ -72,7 +79,7 @@ watch(
canHide: true,
visible: props.report.cityLevelQuotaHidden === 2,
visibleField: 'cityLevelQuotaHidden',
rowTitleColumnWidthList: [120, 280],
rowTitleColumnWidthList: getRowTitleColumnWidth(city),
tableData: city,
codeStr: getTableCodeRow(city)
}
@@ -90,7 +97,7 @@ function getTableCodeRow(tableData) {
return '';
}
const codeRow = tableData.dataVOS.find((row) =>
Object.keys(row).find((key) => ['概念编码', '口味编码'].includes(row[key]))
Object.keys(row).find((key) => codeList.includes(row[key]))
);
if (!codeRow) {
return '';

View File

@@ -5,7 +5,7 @@ import SectionTitle from '../../components/SectionTitle.vue';
import Section from '../../components/Section.vue';
import StyledTable from '../components/StyledTable.vue';
import { defaultSectionEmits, defaultSectionProps } from '../../consts';
import { defaultSectionEmits, defaultSectionProps, codeList } from '../../consts';
const emits = defineEmits(defaultSectionEmits);
const props = defineProps(defaultSectionProps);
@@ -20,7 +20,7 @@ function getTableCodeRow(tableData) {
return '';
}
const codeRow = tableData.dataVOS.find((row) =>
Object.keys(row).find((key) => ['概念编码', '口味编码'].includes(row[key]))
Object.keys(row).find((key) => codeList.includes(row[key]))
);
if (!codeRow) {
return '';
@@ -35,7 +35,7 @@ function getTableCodeRow(tableData) {
<template>
<SectionTitle>其他关键指标</SectionTitle>
<Section class="section">
<StyledTable :data="tableData" :row-title-column-width-list="[140, 260]" />
<StyledTable :data="tableData" :row-title-column-width-list="[140, 280]" />
<div class="message">
<span class="emphasize">{{ codeStr }}</span>

View File

@@ -0,0 +1,128 @@
<script setup>
import { computed, defineEmits, defineProps } from 'vue';
import SectionTitle from '../../components/SectionTitle.vue';
import Section from '../../components/Section.vue';
import { defaultSectionEmits, defaultSectionProps, testItemTypeEnum } from '../../consts';
const emits = defineEmits(defaultSectionEmits);
const props = defineProps(defaultSectionProps);
const list = computed(() => {
const result = [];
props.report?.config?.forEach((item) => {
const current = result.find((record) => record.group === item.group);
if (current) {
current.children.push(item);
} else {
result.push({ group: item.group, children: [item] });
}
});
return result;
});
</script>
<template>
<SectionTitle><slot name="name">测试</slot></SectionTitle>
<Section class="section">
<div v-for="(group, index) in list" :key="index" class="list scrollbar">
<div
v-for="item in group.children"
:key="item.id"
class="item"
:class="{ [testItemTypeEnum[item.concept_type]]: true }"
>
<div class="name">
<a-tooltip>
<template #title>{{ item.concept_name || '' }}</template>
<div class="text">{{ item.concept_name || '' }}</div>
</a-tooltip>
</div>
<img v-if="item.concept_url" :src="item.concept_url" alt="" class="img" />
</div>
</div>
</Section>
</template>
<style scoped lang="scss">
.section {
padding-bottom: 0;
}
.list {
overflow-x: auto;
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
padding-bottom: 16px;
}
.item {
overflow: hidden;
flex: none;
width: 198px;
height: 152px;
margin-right: 2px;
border-radius: 6px;
border: 1px solid #dfe0e3;
&:not(:last-child) {
margin-right: 18px;
}
&.newest .name {
color: #70b936;
background: rgba(112, 185, 54, 0.16);
&::before {
background-color: #70b936;
}
}
&.standard .name {
color: #ffaa00;
background: rgba(255, 170, 0, 0.16);
&::before {
background-color: #ffaa00;
}
}
.name {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 32px;
border: none;
padding: 0 10px;
&::before {
flex: none;
display: block;
content: '';
width: 6px;
height: 6px;
margin-right: 4px;
border-radius: 50%;
}
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.img {
width: 100%;
height: 120px;
border: none;
object-fit: contain;
}
}
</style>

View File

@@ -1,124 +1,18 @@
<script setup>
import { computed, defineEmits, defineProps } from 'vue';
import SectionTitle from '../../components/SectionTitle.vue';
import Section from '../../components/Section.vue';
import { defineEmits, defineProps } from 'vue';
import { defaultSectionEmits, defaultSectionProps } from '../../consts';
const emits = defineEmits(defaultSectionEmits);
const props = defineProps(defaultSectionProps);
const conceptTypeEnum = {
newest: 1,
standard: 0,
1: 'newest',
0: 'standard'
};
const list = computed(() => props.report?.config || []);
import TestingBase from './TestingBase.vue';
</script>
<template>
<SectionTitle>测试概念</SectionTitle>
<Section class="section">
<div class="list scrollbar">
<div
v-for="item in list"
:key="item.id"
class="item"
:class="{ [conceptTypeEnum[item.concept_type]]: true }"
>
<div class="name">
<a-tooltip>
<template #title>{{ item.concept_name || '' }}</template>
<div class="text">{{ item.concept_name || '' }}</div>
</a-tooltip>
</div>
<img v-if="item.concept_url" :src="item.concept_url" alt="" class="img" />
</div>
</div>
</Section>
<TestingBase :report="props.report">
<template #name>测试概念</template>
</TestingBase>
</template>
<style scoped lang="scss">
.section {
padding-bottom: 0;
}
.list {
overflow-x: auto;
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
padding-bottom: 16px;
}
.item {
overflow: hidden;
flex: none;
width: 198px;
height: 152px;
margin-right: 2px;
border-radius: 6px;
border: 1px solid #dfe0e3;
&:not(:last-child) {
margin-right: 18px;
}
&.newest .name {
color: #70b936;
background: rgba(112, 185, 54, 0.16);
&::before {
background-color: #70b936;
}
}
&.standard .name {
color: #ffaa00;
background: rgba(255, 170, 0, 0.16);
&::before {
background-color: #ffaa00;
}
}
.name {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 32px;
border: none;
padding: 0 10px;
&::before {
flex: none;
display: block;
content: '';
width: 6px;
height: 6px;
margin-right: 4px;
border-radius: 50%;
}
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.img {
width: 100%;
height: 120px;
border: none;
object-fit: contain;
}
}
</style>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,18 @@
<script setup>
import { defineEmits, defineProps } from 'vue';
import { defaultSectionEmits, defaultSectionProps } from '../../consts';
const emits = defineEmits(defaultSectionEmits);
const props = defineProps(defaultSectionProps);
import TestingBase from './TestingBase.vue';
</script>
<template>
<TestingBase :report="props.report">
<template #name>测试包装</template>
</TestingBase>
</template>
<style scoped lang="scss"></style>

View File

@@ -4,19 +4,11 @@ import { computed, defineEmits, defineProps } from 'vue';
import SectionTitle from '../../components/SectionTitle.vue';
import Section from '../../components/Section.vue';
import { defaultSectionEmits, defaultSectionProps } from '../../consts';
import { defaultSectionEmits, defaultSectionProps, testItemTypeEnum } from '../../consts';
const emits = defineEmits(defaultSectionEmits);
const props = defineProps(defaultSectionProps);
const typeEnum = {
newest: 1,
standard: 0,
1: 'newest',
0: 'standard'
};
const typeLabelMap = {
1: '新品口味',
0: '标杆口味'
@@ -33,7 +25,7 @@ const list = computed(() => props.report?.config || []);
v-for="item in list"
:key="item.id"
class="item"
:class="{ [typeEnum[item.concept_type]]: true }"
:class="{ [testItemTypeEnum[item.concept_type]]: true }"
>
<div class="code">
{{ typeLabelMap[item.concept_type] || '' }}{{ item.concept_encode || '' }}

View File

@@ -195,7 +195,9 @@ function downloadHotAreaImage() {
ctx.fillStyle = '#FFFFFF';
ctx.roundRect(
textLeft - 4,
textTop - (textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) * 0.5 - 6,
textTop
- (textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) * 0.5
- 6,
textMetrics.width + 8,
textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent + 8,
4

View File

@@ -4,6 +4,8 @@ import { computed, defineProps } from 'vue';
import Section from '../../../components/Section.vue';
import StyledTable from '../../components/StyledTable.vue';
import { codeList } from '../../../consts';
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) }
});
@@ -16,7 +18,7 @@ function getTableCodeRow(tableData) {
return '';
}
const codeRow = tableData.dataVOS.find((row) =>
Object.keys(row).find((key) => ['概念编码', '口味编码'].includes(row[key]))
Object.keys(row).find((key) => codeList.includes(row[key]))
);
if (!codeRow) {
return '';

View File

@@ -56,3 +56,51 @@ export function sortListForTableColSpan(options = {}) {
return list;
}
/**
* 计算表格行的表头的宽度
* @param data
* @param data.headerVOS {array} 表格的列的表头
* @param data.dataVOS {array} 表格的数据
* @returns {*|[]|number[]|undefined}
*/
export function getRowTitleColumnWidth(data) {
if (!data) {
return undefined;
}
const header = data.headerVOS || [];
const tableData = data.dataVOS || [];
const rowTitleIndex = header[0]?.dataIndex;
if (!rowTitleIndex) {
return [280, 280];
}
const widthList = tableData.map((item) => {
const rowTitleList = item[rowTitleIndex].split('_');
if (rowTitleList.length > 1) {
return rowTitleList.map((str) => str.length * 14 + 36);
}
return [];
});
return widthList.reduce((prev, curr) => {
if (!prev?.length) {
return curr;
}
if (!curr?.length) {
return prev;
}
curr.forEach((currWidth, index) => {
prev[index] = Math.max(currWidth, prev[index], 120);
prev[index] = Math.ceil(prev[index] * 0.1) * 10;
});
return prev;
}, []);
}