feat: 新增口味测试洞察报告

This commit is contained in:
钱冠学
2024-10-09 16:29:56 +08:00
parent 6697187d33
commit f0abcd22f4
28 changed files with 1328 additions and 894 deletions

View File

@@ -3,16 +3,13 @@ import { onBeforeUnmount, ref } from 'vue'
import { useRoute } from 'vue-router'
import { message, Modal } from 'ant-design-vue'
import InsightEmpty from './components/InsightEmpty.vue'
import InsightShare from './components/InsightShare.vue'
import Report from './report/Report.vue'
import { editInsightReport, getInsightReport, checkReportStatus, updateInsightReport } from './api'
const route = useRoute()
const sn = route.query.sn || ''
@@ -26,11 +23,10 @@ const report = ref({})
const saving = ref(0)
const saveParams = ref({})
getReport(true)
async function getReport(fullLoading) {
if(loading.value) {
if (loading.value) {
return
}
loading.value = true
@@ -51,17 +47,20 @@ async function getReport(fullLoading) {
}
function initSaveParams() {
const totalData = report.value.chatVOS?.find?.((data) => data.type.startsWith('决策指标-总体')) || {}
const totalData =
report.value.chatVOS?.find?.((data) => data.type.startsWith('决策指标-总体')) || {}
const headersFirstIndex = totalData.headerVOS?.[0]?.dataIndex || ''
const standardRow = totalData.dataVOS?.find?.((record) => record[headersFirstIndex] === '是否通过概念行动标准') || {}
const standardRow =
totalData.dataVOS?.find?.((record) =>
['是否通过概念行动标准', '是否通过口味行动标准'].includes(record[headersFirstIndex])
) || {}
const actions = {}
Object.keys(standardRow).forEach((key) => {
if(standardRow[key] !== '是否通过概念行动标准') {
if (!['是否通过概念行动标准', '是否通过口味行动标准'].includes(standardRow[key])) {
actions[key] = standardRow[key]
}
})
const params = {
id: report.value.id,
decisionCriteria: report.value.decisionIndicators, // 决策标准
@@ -74,7 +73,7 @@ function initSaveParams() {
actions // 是否通过行动标准 1 是 2 否
}
Object.keys(params).forEach((key) => saveParams.value[key] = params[key])
Object.keys(params).forEach((key) => (saveParams.value[key] = params[key]))
}
async function onInitReport() {
@@ -82,7 +81,7 @@ async function onInitReport() {
const params = { surveySn: sn }
const data = await updateInsightReport(params).catch((e) => e)
if(!data?.data?.code) {
if (!data?.data?.code) {
await getReport(false)
}
@@ -104,7 +103,7 @@ function onUpdateReport() {
}
async function onConfirmUpdateReport() {
if(updating.value) {
if (updating.value) {
return
}
updating.value = true
@@ -115,11 +114,10 @@ async function onConfirmUpdateReport() {
await getReport()
}
async function editReport(evt) {
saving.value += 1
Object.keys(evt || {}).forEach((key) => saveParams.value[key] = evt[key])
Object.keys(evt || {}).forEach((key) => (saveParams.value[key] = evt[key]))
await editInsightReport(saveParams.value).catch(() => '')
saving.value -= 1
@@ -137,15 +135,19 @@ function mergeReport(params) {
report.value.cityLevelQuotaHidden = params.cityLevelQuota_Hidden // 购买意愿-省市等级 是否隐藏 1是 2否
const total = report.value?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-总体'))
if(total && params.actions) {
const result = total.dataVOS?.find?.((data) => '是否通过概念行动标准' === data[total.headerVOS?.[0]?.dataIndex]) || {}
Object.keys(params.actions).forEach((key) => result[key] = params.actions[key])
if (total && params.actions) {
const result =
total.dataVOS?.find?.((data) =>
['是否通过概念行动标准', '是否通过口味行动标准'].includes(
data[total.headerVOS?.[0]?.dataIndex]
)
) || {}
Object.keys(params.actions).forEach((key) => (result[key] = params.actions[key]))
}
}
function startLooping() {
if(!timer) {
if (!timer) {
return
}
stopLooping()
@@ -161,7 +163,6 @@ function stopLooping() {
onBeforeUnmount(stopLooping)
async function getReportStatus() {
const params = {
sn,
@@ -176,7 +177,7 @@ async function getReportStatus() {
// 2=有新的报告产生,已完成,需要重新请求报告展示接口
// 3=老报告的词云更新完了,需要重新请求报告展示接口
switch(data?.data?.code) {
switch (data?.data?.code) {
case 0:
updating.value = false
break
@@ -195,19 +196,18 @@ async function getReportStatus() {
</script>
<template>
<a-spin v-if="showFullLoading"
:spinning="true"
tip="加载中"
class="spinning" />
<a-spin v-if="showFullLoading" :spinning="true" tip="加载中" class="spinning" />
<div v-else class="insight-page">
<InsightEmpty v-if="!report?.id" @generate="onInitReport()" />
<template v-if="report?.id">
<InsightShare :report="report"
:saving="saving"
:updating="updating"
@regenerate="onUpdateReport()" />
<InsightShare
:report="report"
:saving="saving"
:updating="updating"
@regenerate="onUpdateReport()"
/>
<Report :report="report" :updating="updating" @change="editReport" />
</template>

View File

@@ -2,7 +2,6 @@
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import SharedInvalid from './components/SharedInvalid.vue'
import SharedExpired from './components/SharedExpired.vue'
import SharedAccess from './components/SharedAccess.vue'
@@ -28,7 +27,7 @@ const report = ref({})
getReportInfo()
async function getReportInfo() {
if(loading.value) {
if (loading.value) {
return
}
loading.value = true
@@ -38,14 +37,14 @@ async function getReportInfo() {
loading.value = false
if(!data?.data) {
if (!data?.data) {
isInvalid.value = true
return
}
document.title = '伊调研|' + (data?.data?.surveyName || '')
switch(data.data?.status) {
switch (data.data?.status) {
case 1: // 链接无效
isInvalid.value = true
break
@@ -59,7 +58,7 @@ async function getReportInfo() {
}
async function getReport(password) {
if(loading.value) {
if (loading.value) {
return
}
loading.value = true
@@ -69,13 +68,12 @@ async function getReport(password) {
loading.value = false
report.value = data?.data || {}
if(report.value.id) {
if (report.value.id) {
isPassword.value = false
}
accessRef.value.reset()
}
</script>
<template>

View File

@@ -1,6 +1,5 @@
import request from '@/utils/request'
/**
* 新增/更新洞察报告详情
* @param data
@@ -14,7 +13,6 @@ export function updateInsightReport(data) {
})
}
/**
* 获取洞察报告详情
* @param params
@@ -22,13 +20,12 @@ export function updateInsightReport(data) {
*/
export function getInsightReport(params) {
return request({
url: `/console/insightReport/${ params.sn }`,
url: `/console/insightReport/${params.sn}`,
method: 'get',
params
})
}
/**
* 检查状态
* @param data
@@ -36,13 +33,12 @@ export function getInsightReport(params) {
*/
export function checkReportStatus(data) {
return request({
url: `/console/insightReport/${ data.sn }/status`,
url: `/console/insightReport/${data.sn}/status`,
method: 'post',
data
})
}
/**
* 修改洞察报告
* @param data
@@ -56,8 +52,6 @@ export function editInsightReport(data) {
})
}
/**
* 分享洞察报告
* @param data
@@ -71,7 +65,6 @@ export function shareReport(data) {
})
}
/**
* 获取洞察报告基本信息
* @param params
@@ -85,8 +78,6 @@ export function getInsightReportInfoSecret(params) {
})
}
/**
* 获取洞察报告详情 secret
* @param params
@@ -99,4 +90,3 @@ export function getInsightReportBySecret(params) {
params
})
}

View File

@@ -10,12 +10,12 @@ function generateReport() {
<template>
<div class="insight-empty">
<img src="@/assets/img/publish/no-data.png" alt="" class="img">
<img src="@/assets/img/publish/no-data.png" alt="" class="img" />
<div class="message">请点击下方按钮生成洞察报告需满足每个概念有效样本量均60</div>
<a-button type="primary" class="custom-button generate-button" @click="generateReport()">
<img src="../img/icon_generate_report.png" alt="" class="icon">
<img src="../img/icon_generate_report.png" alt="" class="icon" />
<span>生成报告</span>
</a-button>
</div>
@@ -40,12 +40,12 @@ function generateReport() {
.message {
height: 40px;
font-family: "Alibaba PuHuiTi 2.0", sans-serif;
font-family: 'Alibaba PuHuiTi 2.0', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 20px;
text-align: center;
color: #70B937;
color: #70b937;
}
.generate-button {

View File

@@ -9,7 +9,6 @@ import { shareReport } from '../api'
import useCopy from '@/composables/useCopy'
const route = useRoute()
const emits = defineEmits(['regenerate'])
const props = defineProps({
@@ -28,7 +27,7 @@ const oldPassword = ref(password.value)
const oldValidity = ref(validity.value)
watch(shown, () => {
if(!shown.value) {
if (!shown.value) {
password.value = oldPassword.value
validity.value = oldValidity.value
}
@@ -39,14 +38,14 @@ const secret = ref(props.report.shareRandom || '') // 分享随机字符串
function getRandomStr(bit) {
const random = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPSDFGHJKLZXCVBNM'
let str = ''
for(let i = 0; i < bit; i += 1) {
for (let i = 0; i < bit; i += 1) {
str += random[Math.floor(Math.random() * random.length)]
}
return str
}
function onEdit(type) {
if(loading.value) {
if (loading.value) {
return
}
editable.value = type
@@ -61,11 +60,11 @@ function getPopupContainer(el) {
}
function onShowShare() {
if(props.saving) {
if (props.saving) {
message.warning('报告保存中,请稍后再试')
return
}
if(checkIntegrity()) {
if (checkIntegrity()) {
shown.value = true
}
}
@@ -88,16 +87,19 @@ function disabledTime(date) {
const isNextMinute = date.isAfter(moment(), 'minute')
return {
disabledHours: () => isBeforeDate ? range(0, 24) : (isNextDate ? [] : range(0, moment().hour())),
disabledMinutes: () => isBeforeHour ? range(0, 60) : (isNextHour ? [] : range(0, moment().minute())),
disabledSeconds: () => isBeforeMinute ? range(0, 60) : (isNextMinute ? [] : range(0, moment().second()))
disabledHours: () =>
isBeforeDate ? range(0, 24) : isNextDate ? [] : range(0, moment().hour()),
disabledMinutes: () =>
isBeforeHour ? range(0, 60) : isNextHour ? [] : range(0, moment().minute()),
disabledSeconds: () =>
isBeforeMinute ? range(0, 60) : isNextMinute ? [] : range(0, moment().second())
}
}
function range(start, end) {
const result = []
for(let i = start; i < end; i++) {
for (let i = start; i < end; i++) {
result.push(i)
}
@@ -107,21 +109,28 @@ function range(start, end) {
function checkIntegrity() {
const rp = props.report || {}
if(!rp.decisionIndicators) {
if (!rp.decisionIndicators) {
message.warning('决策标准未填写,请检查后重试!')
return false
}
if(!rp.coreConclusion) {
if (!rp.coreConclusion) {
message.warning('核心结论未填写,请检查后重试!')
return false
}
const total = rp?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-总体'))
if(total) {
const result = total.dataVOS?.find?.((data) => '是否通过概念行动标准' === data[total.headerVOS?.[0]?.dataIndex]) || {}
const validateKeys = total.headerVOS?.filter((header) => header.title?.includes?.('新品')).flatMap((header) => header.children.map((subHeader) => subHeader.dataIndex))
if(Object.keys(result).some((key) => validateKeys.includes(key) && !result[key])) {
if (total) {
const result =
total.dataVOS?.find?.((data) =>
['是否通过概念行动标准', '是否通过口味行动标准'].includes(
data[total.headerVOS?.[0]?.dataIndex]
)
) || {}
const validateKeys = total.headerVOS
?.filter((header) => header.title?.includes?.('新品'))
.flatMap((header) => header.children.map((subHeader) => subHeader.dataIndex))
if (Object.keys(result).some((key) => validateKeys.includes(key) && !result[key])) {
message.warning('是否通过概念行动标准未填写完整,请检查后重试!')
return false
}
@@ -131,7 +140,7 @@ function checkIntegrity() {
}
function checkShareIntegrity() {
if(!/^[0-9a-zA-Z]{4,4}$/g.test(password.value)) {
if (!/^[0-9a-zA-Z]{4,4}$/g.test(password.value)) {
message.warning('请输入4位大小写字母或数字')
return false
}
@@ -145,30 +154,30 @@ function checkShareIntegrity() {
}
async function onCopy() {
if(!checkShareIntegrity()) {
if (!checkShareIntegrity()) {
return
}
if(!secret.value) {
if (!secret.value) {
message.warning('请先保存')
return
}
if(loading.value) {
if (loading.value) {
message.warning('保存中,请稍候重试')
return
}
let link = `报告链接:${ window.location.origin }/#/shared/insight/${ secret.value }`
link += ` \n密码${ password.value }`
link += ` \n有效期至${ validity.value || '永久' }`
let link = `报告链接:${window.location.origin}/#/shared/insight/${secret.value}`
link += ` \n密码${password.value}`
link += ` \n有效期至${validity.value || '永久'}`
useCopy(link)
message.success('复制成功')
}
async function onSave() {
if(loading.value || !checkIntegrity() || !checkShareIntegrity()) {
if (loading.value || !checkIntegrity() || !checkShareIntegrity()) {
return
}
loading.value = true
@@ -182,7 +191,7 @@ async function onSave() {
loading.value = false
if(!data?.data?.randomNum) {
if (!data?.data?.randomNum) {
message.error(data?.data?.message || '保存失败,请重试')
return
}
@@ -199,10 +208,14 @@ async function onSave() {
}
const updating = ref(false)
watch(() => props.updating, (val) => updating.value = !!val, { immediate: true })
watch(
() => props.updating,
(val) => (updating.value = !!val),
{ immediate: true }
)
function onUpdateReport() {
if(updating.value) {
if (updating.value) {
message.info('问卷更新中')
return
}
@@ -213,28 +226,34 @@ function onUpdateReport() {
<template>
<div class="insight-share">
<a-tooltip v-model:visible="shown"
:arrowPointAtCenter="false"
:getPopupContainer="getPopupContainer"
trigger="click"
placement="bottomLeft"
overlayClassName="share-popover">
<a-tooltip
v-model:visible="shown"
:arrowPointAtCenter="false"
:getPopupContainer="getPopupContainer"
trigger="click"
placement="bottomLeft"
overlayClassName="share-popover"
>
<template #title>
<div class="share-popover-content" @click.stop>
<div class="title">分享报告</div>
<div class="row">
<span class="label">访问密码</span>
<a-input v-model:value="password"
class="custom-input password-input"
:disabled="!editable || loading"
placeholder="访问密码"
@change="onPasswordChanged"
@blur="onSave()" />
<a-button v-if="!editable"
type="text"
:disabled="loading"
class="custom-button edit-password-button"
@click="onEdit(true)">
<a-input
v-model:value="password"
class="custom-input password-input"
:disabled="!editable || loading"
placeholder="访问密码"
@change="onPasswordChanged"
@blur="onSave()"
/>
<a-button
v-if="!editable"
type="text"
:disabled="loading"
class="custom-button edit-password-button"
@click="onEdit(true)"
>
修改
</a-button>
</div>
@@ -244,16 +263,26 @@ function onUpdateReport() {
</div>
<div class="row">
<span class="label">有效期至</span>
<a-date-picker v-model:value="validity"
valueFormat="YYYY-MM-DD HH:mm:mm"
format="YYYY-MM-DD HH:mm:mm"
show-time
:disabled-date="disabledDate"
:disabled-time="disabledTime"
placeholder="请选择日期"
class="custom-date-picker"
@change="() => {!validity ? onSave() : ''}"
@openChange="(status) => {!status ? onSave() : ''}" />
<a-date-picker
v-model:value="validity"
valueFormat="YYYY-MM-DD HH:mm:mm"
format="YYYY-MM-DD HH:mm:mm"
show-time
:disabled-date="disabledDate"
:disabled-time="disabledTime"
placeholder="请选择日期"
class="custom-date-picker"
@change="
() => {
!validity ? onSave() : ''
}
"
@openChange="
(status) => {
!status ? onSave() : ''
}
"
/>
</div>
<div class="row space-between mt-18 mb-0">
<a-spin :spinning="loading">
@@ -272,7 +301,7 @@ function onUpdateReport() {
</div>
</template>
<a-button style="width: 0; padding: 0;"></a-button>
<a-button style="width: 0; padding: 0"></a-button>
</a-tooltip>
<a-button class="custom-button button mr-20" @click.stop="onShowShare">
@@ -280,9 +309,11 @@ function onUpdateReport() {
<span>分享报告</span>
</a-button>
<a-button class="custom-button button mr-10"
:class="{'updating': updating}"
@click="onUpdateReport">
<a-button
class="custom-button button mr-10"
:class="{ updating: updating }"
@click="onUpdateReport"
>
<span class="iconfont icon-gengxinbaogao" />
<span>{{ updating ? '更新中' : '更新报告' }}</span>
</a-button>
@@ -290,7 +321,9 @@ function onUpdateReport() {
<div class="message">
<InfoCircleOutlined class="icon" />
<span>问卷题目调整或样本数据变化后,请点击按钮更新报告结果如仅修改自主填报内容分享链接中的报告内容将自动更新无需点击更新报告按钮</span>
<span
>问卷题目调整或样本数据变化后,请点击按钮更新报告结果如仅修改自主填报内容分享链接中的报告内容将自动更新无需点击更新报告按钮</span
>
</div>
<div class="saving-wrapper">
@@ -337,7 +370,7 @@ function onUpdateReport() {
width: 104px;
height: 32px;
border-radius: 4px;
background: #FFFFFF;
background: #ffffff;
.icon {
display: block;
@@ -394,7 +427,7 @@ function onUpdateReport() {
font-family: Source Han Sans, sans-serif;
font-size: 14px;
font-weight: normal;
color: #FC4545;
color: #fc4545;
}
:deep(.share-popover) {
@@ -421,8 +454,8 @@ function onUpdateReport() {
width: 389px;
padding: 16px;
border-radius: 10px;
background: #FFFFFF;
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
background: #ffffff;
font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
.title {
height: 24px;
@@ -453,20 +486,20 @@ function onUpdateReport() {
padding: 0;
font-size: 12px;
font-weight: normal;
color: #81B74C;
color: #81b74c;
}
.password-input {
width: 128px;
height: 32px;
border: 1px solid #DFE0E3;
border: 1px solid #dfe0e3;
border-radius: 4px;
font-size: 14px;
font-weight: normal;
color: #979797;
&.ant-input-disabled {
background: #F5F5F5;
background: #f5f5f5;
}
}
@@ -489,7 +522,7 @@ function onUpdateReport() {
padding-left: 16px;
padding-right: 16px;
border-radius: 4px;
background: #70B936;
background: #70b936;
}
}
</style>

View File

@@ -1,6 +1,4 @@
<script setup>
</script>
<script setup></script>
<template>
<div class="insight-section">
@@ -11,7 +9,7 @@
<style scoped lang="scss">
.insight-section {
padding: 16px;
border: 1px solid #DFE0E3;
border: 1px solid #dfe0e3;
border-radius: 8px;
}
</style>

View File

@@ -17,19 +17,20 @@ const rules = {
}
function onSubmit() {
if(loading.value) {
if (loading.value) {
return
}
loading.value = true
formRef.value.validate().then(() => {
emits('password', formState.password)
}).catch((error) => {
loading.value = false
message.warning(error.errorFields.map((err) => err.errors?.join?.('、')).join('、'))
})
formRef.value
.validate()
.then(() => {
emits('password', formState.password)
})
.catch((error) => {
loading.value = false
message.warning(error.errorFields.map((err) => err.errors?.join?.('、')).join('、'))
})
}
function reset() {
@@ -41,27 +42,21 @@ defineExpose({ reset })
<template>
<div class="shared-access">
<img src="../img/icon_lock.png" alt="" class="icon">
<img src="../img/icon_lock.png" alt="" class="icon" />
<a-form ref="formRef"
:model="formState"
:rules="rules"
layout="vertical"
class="form">
<a-form ref="formRef" :model="formState" :rules="rules" layout="vertical" class="form">
<a-form-item label="请输入密码访问" name="password">
<a-input v-model:value="formState.password"
:disabled="loading"
class="custom-input"
placeholder="请输入密码"
@keydown.enter="onSubmit()" />
<a-input
v-model:value="formState.password"
:disabled="loading"
class="custom-input"
placeholder="请输入密码"
@keydown.enter="onSubmit()"
/>
</a-form-item>
<a-form-item class="no-margin">
<a-button type="primary"
:loading="loading"
block
class="custom-button"
@click="onSubmit()">
<a-button type="primary" :loading="loading" block class="custom-button" @click="onSubmit()">
确定
</a-button>
</a-form-item>
@@ -94,7 +89,7 @@ defineExpose({ reset })
width: 358px;
padding: 20px 22px 20px 42px;
border-radius: 10px;
background: #FAFAFA;
background: #fafafa;
:deep(.ant-form-item-required)::before {
display: none !important;

View File

@@ -1,10 +1,8 @@
<script setup>
</script>
<script setup></script>
<template>
<div class="shared-expired">
<img src="../img/icon_expire.png" alt="" class="icon">
<img src="../img/icon_expire.png" alt="" class="icon" />
<div class="message">抱歉您访问的链接已过期</div>
</div>
@@ -30,9 +28,9 @@
.message {
line-height: 28px;
text-align: center;
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
font-size: 20px;
font-weight: normal;
color: #8C8C8C;
color: #8c8c8c;
}
</style>

View File

@@ -1,10 +1,8 @@
<script setup>
</script>
<script setup></script>
<template>
<div class="shared-expired">
<img src="../img/icon_expire.png" alt="" class="icon">
<img src="../img/icon_expire.png" alt="" class="icon" />
<div class="message">抱歉您访问的链接已失效</div>
</div>
@@ -30,9 +28,9 @@
.message {
line-height: 28px;
text-align: center;
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
font-size: 20px;
font-weight: normal;
color: #8C8C8C;
color: #8c8c8c;
}
</style>

View File

@@ -3,7 +3,8 @@
// 100=产品口味(100_)101=产品包装(101_)
// 200=共创诊断(200_)201=共创筛选(201_)
// 300=概念诊断-标准(300_)301=概念诊断-快速(301_)302=概念诊断-配对(302_)
export const showInsightTemplateType = [300, 301] // 显示洞察报告 tab 的 template_type
// 500=口味测试-标准(500_)501=口味测试-快速(501_)502=口味测试-配对(502_)
export const showInsightTemplateType = [300, 301, 500, 501] // 显示洞察报告 tab 的 template_type
// 检查是否需要显示 “洞察报告” 这个标签页
// 仅标准版和快测版问卷显示该菜单
@@ -11,6 +12,4 @@ export function checkShowInsightTab({ templateType } = {}) {
return templateType && showInsightTemplateType.includes(templateType)
}
export const reportUpdatingMessageText = '报告更新中,不能修改'

View File

@@ -1,15 +1,15 @@
<script setup>
import { computed, defineEmits, defineProps } from 'vue'
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 CoreConclusion from './section/CoreConclusion.vue'
import DecisionIndicators from './section/DecisionIndicators.vue'
import OtherKeyIndicators from './section/OtherKeyIndicators.vue'
import ConceptDiagnosis from './section/conceptDiagnosis/ConceptDiagnosis.vue'
import ConceptDiagnosis from './section/diagnosis/ConceptDiagnosis.vue'
import TasteDiagnosis from './section/diagnosis/TasteDiagnosis.vue'
const emits = defineEmits(['change'])
const props = defineProps({
@@ -18,30 +18,58 @@ const props = defineProps({
updating: { type: Boolean, default: false } // 报告更新中
})
const report = computed(() => props.report || {})
const typeLabelMap = {
1: '概念',
2: '概念',
3: '概念',
4: '口味',
5: '口味',
6: '口味'
}
const report = computed(() =>
Object.assign({}, props.report || {}, { typeStr: typeLabelMap[props.report.type] || '' })
)
const readonly = computed(() => props.readonly || false)
const updating = computed(() => props.updating || false)
// 快测版报告内容和标准版基本一致,区别为快测版没有概念诊断部分
// 看板类型1=标准版,2=快测版,3=配对版
// 看板类型1=标准版,2=快测版,3=配对版,添加了口味,请参考下一行
// 看板类型1=概念标准版,2=概念快测版,3=概念配对版,4=口味标准版,5=口味快测版,6=口味配对版
const type = computed(() => +props.report?.type)
const testCom = computed(() => {
switch (type.value) {
case 1:
case 2:
case 3:
return TestingConcept
case 4:
case 5:
case 6:
return TestingTaste
}
})
const comList = computed(() => {
const list = [
ProjectNameAndDecisionCriteria, // 项目名称及概念决策标准
TestingConcept, // 测试概念
testCom.value, // 测试概念/测试口味
CoreConclusion, // 核心结论
DecisionIndicators, // 决策指标
OtherKeyIndicators // 其他关键指标
]
if(!props.readonly || props.report.overviewHidden === 2) {
if (!props.readonly || props.report.overviewHidden === 2) {
list.unshift(Overview) // 报告概览
}
if([1, 3].includes(type.value)) {
if ([1, 3].includes(type.value)) {
list.push(ConceptDiagnosis) // 概念诊断
}
if ([4].includes(type.value)) {
list.push(TasteDiagnosis)
}
return list
})
@@ -53,13 +81,15 @@ function onChange(evt) {
<template>
<div class="insight-report">
<component v-for="(com, index) in comList"
:key="index"
:is="com"
:report="report"
:readonly="readonly"
:updating="updating"
@change="onChange" />
<component
v-for="(com, index) in comList"
:key="index"
:is="com"
:report="report"
:readonly="readonly"
:updating="updating"
@change="onChange"
/>
</div>
</template>
@@ -68,6 +98,6 @@ function onChange(evt) {
width: 100%;
padding-bottom: 28px;
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
}
</style>

View File

@@ -11,7 +11,6 @@ const props = defineProps({
rowSelected: { type: Object, default: undefined }
})
const locale = ref({
emptyText: <Empty />
})
@@ -22,31 +21,32 @@ const total = computed(() => (props.dataSource || []).length || 0)
const maxPage = computed(() => Math.ceil(total.value / pageSize.value))
const columns = computed(() => props.columns || [])
const tableData = computed(() => (props.dataSource || []).slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
const tableData = computed(() =>
(props.dataSource || []).slice((page.value - 1) * pageSize.value, page.value * pageSize.value)
)
const activatedRecord = ref(undefined)
watch(() => props.rowSelected, () => {
if(activatedRecord.value?.seq === props.rowSelected?.seq) {
return
}
onActiveRow((props.dataSource || []).find((item) => item.seq === props.rowSelected?.seq))
}, { immediate: true, deep: true })
watch(
() => props.rowSelected,
() => {
if (activatedRecord.value?.seq === props.rowSelected?.seq) {
return
}
onActiveRow((props.dataSource || []).find((item) => item.seq === props.rowSelected?.seq))
},
{ immediate: true, deep: true }
)
const smallTableWrapperRef = ref(null)
const height = ref(260)
onMounted(calcSize)
function calcSize() {
console.log(smallTableWrapperRef.value.clientHeight, smallTableWrapperRef.value.clientHeight - 102)
height.value = Math.max(260, smallTableWrapperRef.value.clientHeight - 102)
}
function onPrev() {
page.value = Math.max(1, page.value - 1)
}
@@ -63,7 +63,7 @@ function customRow(record = {}) {
}
function onActiveRow(record) {
if(!props.rowSelectable) {
if (!props.rowSelectable) {
return
}
@@ -74,27 +74,27 @@ function onActiveRow(record) {
<template>
<div ref="smallTableWrapperRef" class="small-table-wrapper">
<a-table :data-source="tableData"
:columns="columns"
:scroll="{y: height}"
:locale="locale"
:border="false"
:pagination="false"
:custom-row="customRow"
class="small-table">
<a-table
:data-source="tableData"
:columns="columns"
:scroll="{ y: height }"
:locale="locale"
:border="false"
:pagination="false"
:custom-row="customRow"
class="small-table"
>
</a-table>
<div class="pager">
<div class="pager-jump" :class="{'disabled': page <= 1}" @click="onPrev">
<img src="@/assets/img/customize/num_up.png" alt="" class="pager-icon left">
<div class="pager-jump" :class="{ disabled: page <= 1 }" @click="onPrev">
<img src="@/assets/img/customize/num_up.png" alt="" class="pager-icon left" />
</div>
<span>{{ page }} / {{ maxPage }}</span>
<div class="pager-jump" :class="{'disabled': page >= maxPage}" @click="onNext">
<img src="@/assets/img/customize/num_up.png" alt="" class="pager-icon right">
<div class="pager-jump" :class="{ disabled: page >= maxPage }" @click="onNext">
<img src="@/assets/img/customize/num_up.png" alt="" class="pager-icon right" />
</div>
</div>
</div>
</template>
@@ -106,15 +106,16 @@ function onActiveRow(record) {
:deep(.small-table.ant-table-wrapper) {
min-height: 294px;
tr th, tr td {
tr th,
tr td {
padding: 10px 24px;
border: none;
}
.ant-table-thead tr th {
font-size: 14px;
font-family: "Source Han Sans", sans-serif;
color: #646A73;
font-family: 'Source Han Sans', sans-serif;
color: #646a73;
background-color: rgba(112, 185, 54, 0.2);
&:first-child {
@@ -137,7 +138,7 @@ function onActiveRow(record) {
}
tr.active td {
color: #81B74C;
color: #81b74c;
}
}

View File

@@ -1,12 +1,14 @@
<script setup>
import { computed, defineEmits, defineProps, ref, watch } from 'vue'
const emits = defineEmits(['change'])
const props = defineProps({
data: { type: Object, default: () => Object.assign({}) },
barTable: { type: Boolean, default: false },
selectionRowTitle: { type: Array, default: () => ['是否通过概念行动标准'] }, // 表格第一列的值为这几个值的时候,这一样其余列要显示“是”“否”选择框
selectionRowTitle: {
type: Array,
default: () => ['是否通过概念行动标准', '是否通过口味行动标准']
}, // 表格第一列的值为这几个值的时候,这一样其余列要显示“是”“否”选择框
readonly: { type: Boolean, default: false },
updating: { type: Boolean, default: false }, // 报告更新中
@@ -14,12 +16,13 @@ const props = defineProps({
rowSecondTitleColumnWidth: { type: Number, default: 280 }, // 表格行头部第二列的宽度
standardStyle: {
type: Object,
default: () => Object.assign({
groupColor: '#FFAA00',
groupBackgroundColor: 'rgba(255, 178, 0, 0.2)',
columnColor: '#434343',
columnBackgroundColor: 'rgba(255, 178, 0, 0.05)'
})
default: () =>
Object.assign({
groupColor: '#FFAA00',
groupBackgroundColor: 'rgba(255, 178, 0, 0.2)',
columnColor: '#434343',
columnBackgroundColor: 'rgba(255, 178, 0, 0.05)'
})
}
})
@@ -30,12 +33,14 @@ const headers = computed(() => {
const rowTitle = list[0]
// 判断是否需要拆分列。(表格内容的第一列文字,是否包含下划线,根据下划线拆分列)
if(!tableData.value.some((data) => {
return data[rowTitle.dataIndex].indexOf('_') > -1
})) {
if (
!tableData.value.some((data) => {
return data[rowTitle.dataIndex].indexOf('_') > -1
})
) {
// 不需要拆分列,返回当前配置即可
return list.map((item, index) => {
if(!index) {
if (!index) {
item.width = props.rowTitleColumnWidth || null
} else {
item.minWidth = 120
@@ -58,24 +63,31 @@ const headers = computed(() => {
width: props.rowTitleColumnWidth || null,
colSpan: 2, // 合并并显示第一列
customRender: ({ text, index }) => {
if(!text) {
if (!text) {
text = text || ''
}
let idx = index
const rowSpan = text?.split?.('_')?.[0] === tableData.value[index - 1]?.[rowTitle.dataIndex]?.split?.('_')?.[0] ? 0 : tableData.value.filter((record, recordIndex) => {
if(idx === recordIndex && text?.split?.('_')[0] === record[rowTitle.dataIndex]?.split?.('_')?.[0]) {
idx++
return true
}
return false
}).length
const rowSpan =
text?.split?.('_')?.[0] ===
tableData.value[index - 1]?.[rowTitle.dataIndex]?.split?.('_')?.[0]
? 0
: tableData.value.filter((record, recordIndex) => {
if (
idx === recordIndex &&
text?.split?.('_')[0] === record[rowTitle.dataIndex]?.split?.('_')?.[0]
) {
idx++
return true
}
return false
}).length
return {
children: text?.split?.('_')[0],
props: {
colSpan: text?.split?.('_')?.length > 1 ? 1 : 2, // 由于拆分为两列,所以需要合并没有拆分的列
rowSpan // 合并连续行
rowSpan // 合并连续行
}
}
}
@@ -83,7 +95,7 @@ const headers = computed(() => {
rowTitle.colSpan = 0 // 合并并隐藏第二列
rowTitle.customRender = ({ text, index }) => {
if(!text) {
if (!text) {
text = text || ''
}
@@ -95,11 +107,10 @@ const headers = computed(() => {
}
}
return [
addedFirstRow,
...list.map((item, index) => {
if(!index) {
if (!index) {
item.width = props.rowSecondTitleColumnWidth || null
} else {
item.minWidth = 120
@@ -108,142 +119,170 @@ const headers = computed(() => {
return item
})
]
})
const columns = computed(() => headers.value.map((column, columnIndex) => {
column.align = 'center'
column.slots = { customRender: column.dataIndex }
column.customHeaderCell = function() {
const style = {}
if(column.title === '新品表现') { // 新品表现,一级表头
style.color = '#70B936'
style.backgroundColor = 'rgba(112, 185, 54, 0.2)'
}
if(column.title === '标杆表现') { // 标杆表现,一级表头
style.color = props.standardStyle.groupColor
style.backgroundColor = props.standardStyle.groupBackgroundColor
}
// 表头(第一行)显示不同的文字颜色及背景色
return {
style
}
}
column.customCell = function(record) {
const style = {}
if(
columnIndex === 0 && ['概念编码', '样本基数'].includes(record[column.dataIndex])
|| columnIndex > 0 && ['概念编码', '样本基数'].some((key) => record[column.dataIndex]?.indexOf?.(key) > -1)
) {
style['font-style'] = 'italic'
}
return {
style
}
}
if(column.children?.length) {
column.children.forEach((child) => {
child.align = 'center'
child.slots = { customRender: child.dataIndex }
child.parentTitle = column.title
child.customHeaderCell = function() {
const columns = computed(() =>
headers.value
.map((column, columnIndex) => {
column.align = 'center'
column.slots = { customRender: column.dataIndex }
column.customHeaderCell = function () {
const style = {}
if(column.title === '新品表现') { // 新品表现,二级表头
style.color = '#434343'
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
if (column.title === '新品表现') {
// 新品表现,一级表头
style.color = '#70B936'
style.backgroundColor = 'rgba(112, 185, 54, 0.2)'
}
if(column.title === '标杆表现') { // 标杆表现,二级表头
style.color = props.standardStyle.columnColor
style.backgroundColor = props.standardStyle.columnBackgroundColor
if (column.title === '标杆表现') {
// 标杆表现,一级表头
style.color = props.standardStyle.groupColor
style.backgroundColor = props.standardStyle.groupBackgroundColor
}
// 表头(第行)显示不同的文字颜色及背景色
// 表头(第行)显示不同的文字颜色及背景色
return {
style
}
}
child.customCell = function(record, rowIndex) {
const rowClass = rowClassName(record, rowIndex)
column.customCell = function (record) {
const style = {}
if(column.title === '新品表现') { // 新品表现,内容
style.color = rowClass === 'gray-row' ? '#818181' : '#434343'
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
}
if(column.title === '标杆表现') { // 标杆表现,内容
style.color = rowClass === 'gray-row' ? '#818181' : props.standardStyle.columnColor
style.backgroundColor = props.standardStyle.columnBackgroundColor
if (
(columnIndex === 0 &&
['概念编码', '口味编码', '样本基数'].includes(record[column.dataIndex])) ||
(columnIndex > 0 &&
['概念编码', '口味编码', '样本基数'].some(
(key) => record[column.dataIndex]?.indexOf?.(key) > -1
))
) {
style['font-style'] = 'italic'
}
// 表格内容显示不同的文字颜色及背景色
return {
style
}
}
if (column.children?.length) {
column.children.forEach((child) => {
child.align = 'center'
child.slots = { customRender: child.dataIndex }
child.parentTitle = column.title
child.customHeaderCell = function () {
const style = {}
if (column.title === '新品表现') {
// 新品表现,二级表头
style.color = '#434343'
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
}
if (column.title === '标杆表现') {
// 标杆表现,二级表头
style.color = props.standardStyle.columnColor
style.backgroundColor = props.standardStyle.columnBackgroundColor
}
// 表头(第二行)显示不同的文字颜色及背景色
return {
style
}
}
child.customCell = function (record, rowIndex) {
const rowClass = rowClassName(record, rowIndex)
const style = {}
if (column.title === '新品表现') {
// 新品表现,内容
style.color = rowClass === 'gray-row' ? '#818181' : '#434343'
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
}
if (column.title === '标杆表现') {
// 标杆表现,内容
style.color = rowClass === 'gray-row' ? '#818181' : props.standardStyle.columnColor
style.backgroundColor = props.standardStyle.columnBackgroundColor
}
// 表格内容显示不同的文字颜色及背景色
return {
style
}
}
})
}
return column
})
}
return column
}).filter((column) => {
// 过滤掉空列('新品表现' 和 '标杆表现',只有第一列表头但是没有第二列表头的情况,表格内容为空,隐藏掉)
return !['新品表现', '标杆表现'].includes(column.title) || column?.children?.length
}))
const flatColumns = computed(() => columns.value.flatMap((column) => column.children?.length ? column.children : column))
.filter((column) => {
// 过滤掉空列('新品表现' 和 '标杆表现',只有第一列表头但是没有第二列表头的情况,表格内容为空,隐藏掉)
return !['新品表现', '标杆表现'].includes(column.title) || column?.children?.length
})
)
const flatColumns = computed(() =>
columns.value.flatMap((column) => (column.children?.length ? column.children : column))
)
const barMax = ref(0)
const barMin = ref(0)
watch(() => props.data, () => {
tableData.value = props.data.dataVOS || []
watch(
() => props.data,
() => {
tableData.value = props.data.dataVOS || []
const list = JSON.parse(JSON.stringify(props.data.dataVOS || []))
const tableList = []
const list = JSON.parse(JSON.stringify(props.data.dataVOS || []))
const tableList = []
const rowTitle = props.data.headerVOS?.[0]
if(rowTitle) {
// 重排序, 把具有相同开头文字用下划线区分“开头文字_xxxxx”的放到一起便于后续表格跨行合并。
// 例“女_样本基数”和“女_非常喜欢+比较喜欢【TOP2】”
for(let i = 0; i < list.length; i += 1) {
const row = list[i]
tableList.push(row)
const titleArr = row[rowTitle.dataIndex]?.split?.('_')
if(titleArr?.length > 1) {
for(let j = i + 1; j < list.length; j += 1) {
if(list[j][rowTitle.dataIndex]?.split?.('_')?.[0] === titleArr[0]) {
tableList.push(list[j])
list.splice(j, 1)
const rowTitle = props.data.headerVOS?.[0]
if (rowTitle) {
// 重排序, 把具有相同开头文字用下划线区分“开头文字_xxxxx”的放到一起便于后续表格跨行合并。
// 例“女_样本基数”和“女_非常喜欢+比较喜欢【TOP2】”
for (let i = 0; i < list.length; i += 1) {
const row = list[i]
tableList.push(row)
const titleArr = row[rowTitle.dataIndex]?.split?.('_')
if (titleArr?.length > 1) {
for (let j = i + 1; j < list.length; j += 1) {
if (list[j][rowTitle.dataIndex]?.split?.('_')?.[0] === titleArr[0]) {
tableList.push(list[j])
list.splice(j, 1)
}
}
}
}
tableData.value = tableList
} else {
tableData.value = list
}
},
{ immediate: true, deep: true }
)
tableData.value = tableList
} else {
tableData.value = list
}
watch(
[flatColumns, tableData],
() => {
const numericalColumns = flatColumns.value.map((key) => key.dataIndex).slice(1)
}, { immediate: true, deep: true })
tableData.value.forEach((record) => {
if (['概念编码', '口味编码', '样本基数'].includes(record[headers.value[0]?.dataIndex])) {
return
}
watch([flatColumns, tableData], () => {
const numericalColumns = flatColumns.value.map((key) => key.dataIndex).slice(1)
tableData.value.forEach((record) => {
if(['概念编码', '样本基数'].includes(record[headers.value[0]?.dataIndex])) {
return
}
// 计算条形图的最大值
barMax.value = Math.max(barMax.value, ...numericalColumns.map((key) => +record[key]).filter((value) => !isNaN(value)))
barMin.value = Math.min(barMin.value, ...numericalColumns.map((key) => +record[key]).filter((value) => !isNaN(value)))
})
}, { deep: true, immediate: true })
// 计算条形图的最大值
barMax.value = Math.max(
barMax.value,
...numericalColumns.map((key) => +record[key]).filter((value) => !isNaN(value))
)
barMin.value = Math.min(
barMin.value,
...numericalColumns.map((key) => +record[key]).filter((value) => !isNaN(value))
)
})
},
{ deep: true, immediate: true }
)
const barStyle = computed(() => (record, column) => {
const value = record[column.dataIndex] ? record[column.dataIndex] : 0
const width = (+value * 100 / barMax.value) + '%'
const width = (+value * 100) / barMax.value + '%'
// 计算条形图的长度,根据最大值计算百分比即可
return {
@@ -253,14 +292,18 @@ const barStyle = computed(() => (record, column) => {
})
function rowClassName(record) {
// 表格中 '概念编码' 和 '样本基数' 行要显示灰色
return Object.keys(record).some((key) => ['概念编码', '样本基数'].some((item) => record[key]?.indexOf?.(item) > -1)) ? 'gray-row' : ''
// 表格中 '概念编码'、 '口味编码' 和 '样本基数' 行要显示灰色
return Object.keys(record).some((key) =>
['概念编码', '口味编码', '样本基数'].some((item) => record[key]?.indexOf?.(item) > -1)
)
? 'gray-row'
: ''
}
function updateSelectionChange(record) {
const actions = {}
Object.keys(record).forEach((key) => {
if(props.selectionRowTitle.every((title) => title !== record[key])) {
if (props.selectionRowTitle.every((title) => title !== record[key])) {
actions[key] = record[key]
}
})
@@ -273,14 +316,16 @@ 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 isStrokeCell = record[cols[0]?.dataIndex].toLowerCase().indexOf('top') > -1 && columnIndex > 0
// console.log('====', record[cols[0]?.dataIndex], columnIndex, column)
const isStaticCell = ['概念编码', '口味编码', '样本基数'].some(
(key) => record[cols[0]?.dataIndex].indexOf(key) > -1
)
const isStrokeCell =
record[cols[0]?.dataIndex].toLowerCase().indexOf('top') > -1 && columnIndex > 0
return {
italic: isStaticCell,
@@ -290,36 +335,51 @@ function cellClass(record, column, cols) {
</script>
<template>
<div class="styled-table-wrapper" :class="{'bar-table': props.barTable}">
<a-table :columns="columns"
:data-source="tableData"
bordered
:pagination="false"
:scroll="{ x: '100%', y: 'auto' }"
:row-class-name="rowClassName"
class="table">
<template v-for="(column, columnIndex) in flatColumns"
:key="column.dataIndex"
#[column.dataIndex]="{record}">
<div class="styled-table-wrapper" :class="{ 'bar-table': props.barTable }">
<a-table
:columns="columns"
:data-source="tableData"
bordered
:pagination="false"
:scroll="{ x: '100%', y: 'auto' }"
:row-class-name="rowClassName"
class="table"
>
<template
v-for="(column, columnIndex) in flatColumns"
:key="column.dataIndex"
#[column.dataIndex]="{ record }"
>
<template v-if="props.barTable" data-desc="显示条形图">
<span
v-if="[0].includes(columnIndex) || ['概念编码', '样本基数'].includes(record[headers[0]?.dataIndex])"
:class="cellClass(record, column, columns)">
v-if="
[0].includes(columnIndex) ||
['概念编码', '口味编码', '样本基数'].includes(record[headers[0]?.dataIndex])
"
:class="cellClass(record, column, columns)"
>
{{ record[column.dataIndex] }}
</span>
<div v-else class="cell-bar" :style="barStyle(record, column)">
<div class="cell-bar-position"
:class="+record[column.dataIndex] <= 50 ? 'outer' : 'inner'"
:style="{color: +record[column.dataIndex] <= 50 ? barStyle(record, column)?.backgroundColor : null}">
<div
class="cell-bar-position"
:class="+record[column.dataIndex] <= 50 ? 'outer' : 'inner'"
:style="{
color:
+record[column.dataIndex] <= 50 ? barStyle(record, column)?.backgroundColor : null
}"
>
<span class="text">{{ record[column.dataIndex] }}</span>
<span class="danger-text">{{ record[column.dataIndex + 'Type'] || '' }}</span>
</div>
</div>
</template>
<template v-else-if="props.selectionRowTitle.includes(record[headers[0].dataIndex])"
data-desc="显示是否选择框">
<template
v-else-if="props.selectionRowTitle.includes(record[headers[0].dataIndex])"
data-desc="显示是否选择框"
>
<template v-if="false" data-desc="标杆不需要选择框,显示横线即可(-"></template>
<span v-if="column.parentTitle?.includes?.('标杆')">-</span>
<span v-else-if="props.selectionRowTitle.includes(record[column.dataIndex])">
@@ -330,14 +390,16 @@ function cellClass(record, column, cols) {
<span v-else-if="+record[column.dataIndex] === 1" class="color-green">是</span>
<span v-else>--</span>
</template>
<a-select v-else
v-model:value="record[column.dataIndex]"
:disabled="props.updating"
:getPopupContainer="getPopupContainer"
placeholder="请选择"
class="custom-select"
style="width: 90px;"
@change="updateSelectionChange(record)">
<a-select
v-else
v-model:value="record[column.dataIndex]"
:disabled="props.updating"
:getPopupContainer="getPopupContainer"
placeholder="请选择"
class="custom-select"
style="width: 90px"
@change="updateSelectionChange(record)"
>
<a-select-option :key="1" :value="1">
<span class="color-green">是</span>
</a-select-option>
@@ -364,12 +426,14 @@ function cellClass(record, column, cols) {
.styled-table-wrapper {
$radius: 6px;
.color-red, :deep(.color-red) {
color: #FC4545;
.color-red,
:deep(.color-red) {
color: #fc4545;
}
.color-green, :deep(.color-green) {
color: #70B936;
.color-green,
:deep(.color-green) {
color: #70b936;
}
.italic {
@@ -389,7 +453,7 @@ function cellClass(record, column, cols) {
padding: 0;
border-radius: 4px;
text-align: right;
color: #FFFFFF;
color: #ffffff;
.text {
padding-left: 5px;
@@ -436,7 +500,8 @@ function cellClass(record, column, cols) {
border-radius: $radius $radius 0 0;
}
.ant-table-thead > tr > th, .ant-table-tbody > tr > td {
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
padding-top: 10px;
padding-right: 6px;
padding-bottom: 10px;
@@ -458,14 +523,14 @@ function cellClass(record, column, cols) {
}
tr.gray-row td {
&:not([rowspan="2"]) {
&:not([rowspan='2']) {
color: #818181;
}
}
}
.danger-text {
color: #FC4545;
color: #fc4545;
}
}
}

View File

@@ -8,7 +8,6 @@ import Empty from '@/components/layout/empty/Empty.vue'
import useDownload from '@/composables/useDownload'
const download = useDownload()
const emits = defineEmits(['change'])
@@ -16,7 +15,6 @@ const props = defineProps({
dataSource: { type: Object, default: () => Object.assign({}) }
})
let chart = null
let maskImage = null
const wordCloudRef = ref(null)
@@ -29,16 +27,14 @@ function onResize() {
chart?.resize?.()
}
watch(() => props.dataSource, initWordCloud)
function initWordCloud() {
if(!wordCloudRef.value || !props.dataSource?.length) {
if (!wordCloudRef.value || !props.dataSource?.length) {
return
}
if(!chart) {
if (!chart) {
chart = echarts.init(wordCloudRef.value)
// maskImage = new Image()
// maskImage.crossOrigin = 'Anonymous'
@@ -68,51 +64,54 @@ function setOption() {
trigger: 'item',
formatter: '{b} {c}%'
},
series: [{
type: 'wordCloud',
shape: 'circle',
keepAspect: false,
maskImage: maskImage,
left: 'center',
top: 'center',
width: '95%',
height: '95%',
sizeRange: [18, 28],
rotationRange: [-0, 0],
rotationStep: 1,
gridSize: 12,
drawOutOfBound: false,
shrinkToFit: true,
layoutAnimation: true,
textStyle: {
fontFamily: 'Source Han Sans, sans-serif',
fontWeight: 'normal',
color: function(param) {
const colors = ['#70B936', '#FCCA46', '#3D3D3D']
return colors[param.dataIndex % colors.length]
}
},
emphasis: {
focus: 'self',
series: [
{
type: 'wordCloud',
shape: 'circle',
keepAspect: false,
maskImage: maskImage,
left: 'center',
top: 'center',
width: '95%',
height: '95%',
sizeRange: [18, 28],
rotationRange: [-0, 0],
rotationStep: 1,
gridSize: 12,
drawOutOfBound: false,
shrinkToFit: true,
layoutAnimation: true,
textStyle: {
// textShadowBlur: 10,
// textShadowColor: '#333333'
}
},
data: props.dataSource?.map?.((item) => {
return {
...item,
value: item.num
}
}) || []
}]
fontFamily: 'Source Han Sans, sans-serif',
fontWeight: 'normal',
color: function (param) {
const colors = ['#70B936', '#FCCA46', '#3D3D3D']
return colors[param.dataIndex % colors.length]
}
},
emphasis: {
focus: 'self',
textStyle: {
// textShadowBlur: 10,
// textShadowColor: '#333333'
}
},
data:
props.dataSource?.map?.((item) => {
return {
...item,
value: item.num
}
}) || []
}
]
}
chart.setOption(option)
}
function downloadChart(name) {
if(!props.dataSource?.length) {
if (!props.dataSource?.length) {
message.warning('暂无数据')
return
}
@@ -131,12 +130,10 @@ defineExpose({ downloadChart })
<template>
<div class="word-cloud-main">
<div ref="wordCloudRef"
v-show="props.dataSource?.length"
class="word-cloud-chart" />
<div ref="wordCloudRef" v-show="props.dataSource?.length" class="word-cloud-chart" />
<template v-if="!props.dataSource?.length">
<div style="height: 57px;"></div>
<div style="height: 57px"></div>
<Empty />
</template>
</div>

View File

@@ -3,11 +3,9 @@ import { defineEmits, defineProps, ref, watch } from 'vue'
import Tinymce from '@/components/Tinymce.vue'
import SectionTitle from '../../components/SectionTitle.vue'
import Section from '../../components/Section.vue'
const emits = defineEmits(['change'])
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
@@ -15,15 +13,18 @@ const props = defineProps({
updating: { type: Boolean, default: false } // 报告更新中
})
const richText = ref('')
watch(() => props.report.coreConclusion, (val) => {
richText.value = val || ''
}, { immediate: true })
watch(
() => props.report.coreConclusion,
(val) => {
richText.value = val || ''
},
{ immediate: true }
)
function onBlur() {
if(richText.value === props.report.coreConclusion) {
if (richText.value === props.report.coreConclusion) {
return
}
emits('change', { coreConclusion: richText.value || '' })
@@ -36,19 +37,21 @@ function onBlur() {
<div v-html="richText" />
</Section>
<Section v-else class="section">
<Tinymce v-model:editorData="richText"
:disabled="props.updating"
:curtail="false"
:curtail-min-height="140"
show
:show-toolbar-separator="false"
:open3-d-icon="false"
:open-quote-icon="false"
:open-more-dialog="false"
:creative-icon="false"
:is-link-content="false"
placeholder="请输入核心结论"
@blur="onBlur" />
<Tinymce
v-model:editorData="richText"
:disabled="props.updating"
:curtail="false"
:curtail-min-height="140"
show
:show-toolbar-separator="false"
:open3-d-icon="false"
:open-quote-icon="false"
:open-more-dialog="false"
:creative-icon="false"
:is-link-content="false"
placeholder="请输入核心结论"
@blur="onBlur"
/>
</Section>
</template>
@@ -65,12 +68,12 @@ function onBlur() {
.tox-tinymce {
overflow: hidden;
min-height: 140px !important;
border: 1px solid #D9D9D9;
border: 1px solid #d9d9d9;
border-radius: 8px;
}
.tox-edit-area__iframe {
background-color: #FFFFFF !important;
background-color: #ffffff !important;
}
}
</style>

View File

@@ -3,14 +3,12 @@ import { defineEmits, defineProps, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue'
import SectionTitle from '../../components/SectionTitle.vue'
import Section from '../../components/Section.vue'
import StyledTable from '../components/StyledTable.vue'
import { reportUpdatingMessageText } from '../../consts'
const emits = defineEmits(['change'])
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
@@ -18,95 +16,104 @@ const props = defineProps({
updating: { type: Boolean, default: false } // 报告更新中
})
const activeKey = ref('0')
const tabList = ref([])
watch(() => props.report, () => {
const total = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-总体'))
const gender = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-性别'))
const age = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-年龄'))
const income = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-家庭月收入'))
const city = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-城市级别'))
tabList.value = [
{
key: '0',
name: getTableTypeStr(total),
canHide: false,
visible: true,
visibleField: 'overviewHidden',
rowTitleColumnWidth: 280,
rowSecondTitleColumnWidth: 280,
tableData: total,
codeStr: getTableCodeRow(total)
},
{
key: '1',
name: getTableTypeStr(gender),
canHide: true,
visible: props.report.genderQuoteHidden === 2,
visibleField: 'genderQuoteHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: gender,
codeStr: getTableCodeRow(gender)
},
{
key: '2',
name: getTableTypeStr(age),
canHide: true,
visible: props.report.ageQuotaHidden === 2,
visibleField: 'ageQuotaHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: age,
codeStr: getTableCodeRow(age)
},
{
key: '3',
name: getTableTypeStr(income),
canHide: true,
visible: props.report.incomeQuotaHidden === 2,
visibleField: 'incomeQuotaHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: income,
codeStr: getTableCodeRow(income)
},
{
key: '4',
name: getTableTypeStr(city),
canHide: true,
visible: props.report.cityLevelQuotaHidden === 2,
visibleField: 'cityLevelQuotaHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: city,
codeStr: getTableCodeRow(city)
}
].filter((item) => !!item.tableData && (!props.readonly || item.visible))
}, { immediate: true })
watch(
() => props.report,
() => {
const total = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-总体'))
const gender = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-性别'))
const age = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-年龄'))
const income = props.report?.chatVOS?.find?.((data) =>
data.type.startsWith('决策指标-家庭月收入')
)
const city = props.report?.chatVOS?.find?.((data) => data.type.startsWith('决策指标-城市级别'))
tabList.value = [
{
key: '0',
name: getTableTypeStr(total),
canHide: false,
visible: true,
visibleField: 'overviewHidden',
rowTitleColumnWidth: 280,
rowSecondTitleColumnWidth: 280,
tableData: total,
codeStr: getTableCodeRow(total)
},
{
key: '1',
name: getTableTypeStr(gender),
canHide: true,
visible: props.report.genderQuoteHidden === 2,
visibleField: 'genderQuoteHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: gender,
codeStr: getTableCodeRow(gender)
},
{
key: '2',
name: getTableTypeStr(age),
canHide: true,
visible: props.report.ageQuotaHidden === 2,
visibleField: 'ageQuotaHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: age,
codeStr: getTableCodeRow(age)
},
{
key: '3',
name: getTableTypeStr(income),
canHide: true,
visible: props.report.incomeQuotaHidden === 2,
visibleField: 'incomeQuotaHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: income,
codeStr: getTableCodeRow(income)
},
{
key: '4',
name: getTableTypeStr(city),
canHide: true,
visible: props.report.cityLevelQuotaHidden === 2,
visibleField: 'cityLevelQuotaHidden',
rowTitleColumnWidth: 120,
rowSecondTitleColumnWidth: 280,
tableData: city,
codeStr: getTableCodeRow(city)
}
].filter((item) => !!item.tableData && (!props.readonly || item.visible))
},
{ immediate: true }
)
function getTableTypeStr(tableData) {
return tableData?.type?.split('-')?.[1] || ''
}
function getTableCodeRow(tableData) {
if(!tableData?.dataVOS?.length) {
if (!tableData?.dataVOS?.length) {
return ''
}
const codeRow = tableData.dataVOS.find((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
if(!codeRow) {
const codeRow = tableData.dataVOS.find((row) =>
Object.keys(row).find((key) => ['概念编码', '口味编码'].includes(row[key]))
)
if (!codeRow) {
return ''
}
return Object.keys(codeRow).filter((key) => /^[a-zA-Z]*$/g.test(codeRow[key])).map((key) => codeRow[key]).join('/')
return Object.keys(codeRow)
.filter((key) => /^[a-zA-Z]*$/g.test(codeRow[key]))
.map((key) => codeRow[key])
.join('/')
}
function toggleVisible(item) {
if(props.updating) {
if (props.updating) {
message.warning(reportUpdatingMessageText)
return
}
@@ -117,7 +124,7 @@ function toggleVisible(item) {
}
function onChange(evt) {
if(props.updating) {
if (props.updating) {
message.warning(reportUpdatingMessageText)
return
}
@@ -130,13 +137,15 @@ function onChange(evt) {
<SectionTitle>决策指标</SectionTitle>
<Section class="section">
<div v-if="tabList.length === 1" class="tab-container none-tab">
<template v-for="(item) in tabList" :key="item.key">
<StyledTable :data="item.tableData"
:row-title-column-width="item.rowTitleColumnWidth"
:row-second-title-column-width="item.rowSecondTitleColumnWidth"
:readonly="props.readonly"
:updating="props.updating"
@change="onChange" />
<template v-for="item in tabList" :key="item.key">
<StyledTable
:data="item.tableData"
:row-title-column-width="item.rowTitleColumnWidth"
:row-second-title-column-width="item.rowSecondTitleColumnWidth"
:readonly="props.readonly"
:updating="props.updating"
@change="onChange"
/>
<div class="message">
<span class="emphasize">{{ item.codeStr }}</span>
@@ -146,28 +155,34 @@ function onChange(evt) {
</div>
<a-tabs v-else v-model:activeKey="activeKey" :animated="false">
<a-tab-pane v-for="(item) in tabList" :key="item.key">
<a-tab-pane v-for="item in tabList" :key="item.key">
<template #tab>
<span>{{ item.name }}</span>
<template v-if="item.canHide && !props.readonly">
<EyeOutlined v-if="item.visible"
class="icon"
:class="{'disabled': props.updating}"
@click.stop="toggleVisible(item)" />
<EyeInvisibleOutlined v-else
class="icon"
:class="{'disabled': props.updating}"
@click.stop="toggleVisible(item)" />
<EyeOutlined
v-if="item.visible"
class="icon"
:class="{ disabled: props.updating }"
@click.stop="toggleVisible(item)"
/>
<EyeInvisibleOutlined
v-else
class="icon"
:class="{ disabled: props.updating }"
@click.stop="toggleVisible(item)"
/>
</template>
</template>
<div class="tab-container">
<StyledTable :data="item.tableData"
:row-title-column-width="item.rowTitleColumnWidth"
:row-second-title-column-width="item.rowSecondTitleColumnWidth"
:readonly="props.readonly"
:updating="props.updating"
@change="onChange" />
<StyledTable
:data="item.tableData"
:row-title-column-width="item.rowTitleColumnWidth"
:row-second-title-column-width="item.rowSecondTitleColumnWidth"
:readonly="props.readonly"
:updating="props.updating"
@change="onChange"
/>
<div class="message">
<span class="emphasize">{{ item.codeStr }}</span>
@@ -203,7 +218,7 @@ function onChange(evt) {
.icon {
margin-left: 6px;
font-size: 14px;
color: #B9B9B9;
color: #b9b9b9;
&.disabled {
cursor: not-allowed;
@@ -218,7 +233,7 @@ function onChange(evt) {
.emphasize {
margin-right: 4px;
color: #FC4545;
color: #fc4545;
}
}
</style>

View File

@@ -1,7 +1,6 @@
<script setup>
import { computed, defineProps, ref } from 'vue'
import SectionTitle from '../../components/SectionTitle.vue'
import Section from '../../components/Section.vue'
import StyledTable from '../components/StyledTable.vue'
@@ -11,25 +10,36 @@ const props = defineProps({
readonly: { type: Boolean, default: false }
})
const tableData = computed(() => props.report?.chatVOS?.find?.((data) => data.type === '其他关键指标') || {})
const tableData = computed(
() => props.report?.chatVOS?.find?.((data) => data.type === '其他关键指标') || {}
)
const codeStr = computed(() => getTableCodeRow(tableData.value))
function getTableCodeRow(tableData) {
if(!tableData?.dataVOS?.length) {
if (!tableData?.dataVOS?.length) {
return ''
}
const codeRow = tableData.dataVOS.find((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
if(!codeRow) {
const codeRow = tableData.dataVOS.find((row) =>
Object.keys(row).find((key) => ['概念编码', '口味编码'].includes(row[key]))
)
if (!codeRow) {
return ''
}
return Object.keys(codeRow).filter((key) => /^[a-zA-Z]*$/g.test(codeRow[key])).map((key) => codeRow[key]).join('/')
return Object.keys(codeRow)
.filter((key) => /^[a-zA-Z]*$/g.test(codeRow[key]))
.map((key) => codeRow[key])
.join('/')
}
</script>
<template>
<SectionTitle>其他关键指标</SectionTitle>
<Section class="section">
<StyledTable :data="tableData" :row-title-column-width="140" :row-second-title-column-width="260" />
<StyledTable
:data="tableData"
:row-title-column-width="140"
:row-second-title-column-width="260"
/>
<div class="message">
<span class="emphasize">{{ codeStr }}</span>
@@ -39,7 +49,6 @@ function getTableCodeRow(tableData) {
</template>
<style scoped lang="scss">
.message {
padding-top: 10px;
font-size: 14px;
@@ -48,7 +57,7 @@ function getTableCodeRow(tableData) {
.emphasize {
margin-right: 4px;
color: #FC4545;
color: #fc4545;
}
}
</style>

View File

@@ -5,13 +5,11 @@ import moment from 'moment'
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue'
import SectionTitle from '../../components/SectionTitle.vue'
import Section from '../../components/Section.vue'
import { reportUpdatingMessageText } from '../../consts'
const emits = defineEmits(['change'])
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
@@ -19,10 +17,18 @@ const props = defineProps({
updating: { type: Boolean, default: false } // 报告更新中
})
const createdAt = computed(() => props.report?.createdAt ? moment(props.report.createdAt).format('YYYY年MM月DD日 HH:mm:ss') : '--')
const completeAt = computed(() => props.report?.completeAt ? moment(props.report.completeAt).format('YYYY年MM月DD日 HH:mm:ss') : '--')
const createdAt = computed(() =>
props.report?.createdAt ? moment(props.report.createdAt).format('YYYY年MM月DD日 HH:mm:ss') : '--'
)
const completeAt = computed(() =>
props.report?.completeAt
? moment(props.report.completeAt).format('YYYY年MM月DD日 HH:mm:ss')
: '--'
)
const statusStr = computed(() => ['待分析', '分析中', '分析完成', '分析失败'][props.report?.status] ?? '--')
const statusStr = computed(
() => ['待分析', '分析中', '分析完成', '分析失败'][props.report?.status] ?? '--'
)
const sampleNum = computed(() => props.report?.sampleNum ?? '--')
const surveyVersion = computed(() => props.report?.surveyVersion ?? '--')
@@ -31,7 +37,7 @@ const reportVersion = computed(() => props.report?.reportVersion ?? '--')
const visible = ref(+props.report.overviewHidden === 2)
function toggleVisibility() {
if(props.updating) {
if (props.updating) {
message.warning(reportUpdatingMessageText)
return
}
@@ -47,14 +53,18 @@ function toggleVisibility() {
<SectionTitle>
<span class="text">报告概览</span>
<template v-if="!props.readonly">
<EyeOutlined v-if="visible"
class="icon"
:class="{'disabled': props.updating}"
@click="toggleVisibility" />
<EyeInvisibleOutlined v-else
class="icon"
:class="{'disabled': props.updating}"
@click.stop="toggleVisibility" />
<EyeOutlined
v-if="visible"
class="icon"
:class="{ disabled: props.updating }"
@click="toggleVisibility"
/>
<EyeInvisibleOutlined
v-else
class="icon"
:class="{ disabled: props.updating }"
@click.stop="toggleVisibility"
/>
</template>
</SectionTitle>
@@ -66,8 +76,10 @@ function toggleVisibility() {
</a-col>
<a-col :span="8" class="rect">
<span class="label">分析状态</span>
<span class="value"
:class="{green: props.report?.status === 1, red: props.report?.status === 3}">
<span
class="value"
:class="{ green: props.report?.status === 1, red: props.report?.status === 3 }"
>
{{ statusStr }}
</span>
</a-col>
@@ -105,7 +117,7 @@ function toggleVisibility() {
.icon {
margin-left: 6px;
font-size: 14px;
color: #B9B9B9;
color: #b9b9b9;
cursor: pointer;
&.disabled {
@@ -116,22 +128,22 @@ function toggleVisibility() {
.label {
font-size: 14px;
font-weight: normal;
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
font-family: 'Alibaba PuHuiTi 3.0', sans-serif;
color: #262626;
}
.value {
font-family: "Source Han Sans", sans-serif;
font-family: 'Source Han Sans', sans-serif;
font-size: 14px;
font-weight: normal;
color: #5D5D5D;
color: #5d5d5d;
&.green {
color: #70B936;
color: #70b936;
}
&.red {
color: #FC4545;
color: #fc4545;
}
}

View File

@@ -6,7 +6,6 @@ import Tinymce from '@/components/Tinymce.vue'
import SectionTitle from '../../components/SectionTitle.vue'
import Section from '../../components/Section.vue'
const emits = defineEmits(['change'])
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
@@ -14,16 +13,18 @@ const props = defineProps({
updating: { type: Boolean, default: false } // 报告更新中
})
const richText = ref('')
watch(() => props.report.decisionIndicators, (val) => {
richText.value = val || ''
}, { immediate: true })
watch(
() => props.report.decisionIndicators,
(val) => {
richText.value = val || ''
},
{ immediate: true }
)
function onBlur() {
if(richText.value === props.report.decisionIndicators) {
if (richText.value === props.report.decisionIndicators) {
return
}
emits('change', { decisionCriteria: richText.value })
@@ -31,7 +32,7 @@ function onBlur() {
</script>
<template>
<SectionTitle>项目名称及概念决策标准</SectionTitle>
<SectionTitle>项目名称及{{ props.report.typeStr }}决策标准</SectionTitle>
<Section>
<div class="row mb-24">
<div class="label">项目名称</div>
@@ -43,19 +44,21 @@ function onBlur() {
<div v-html="richText" />
</div>
<div v-else class="value tinymce-wrapper">
<Tinymce v-model:editorData="richText"
:disabled="props.updating"
:curtail="false"
:curtail-min-height="140"
show
:show-toolbar-separator="false"
:open3-d-icon="false"
:open-quote-icon="false"
:open-more-dialog="false"
:creative-icon="false"
:is-link-content="false"
placeholder="请输入决策标准"
@blur="onBlur" />
<Tinymce
v-model:editorData="richText"
:disabled="props.updating"
:curtail="false"
:curtail-min-height="140"
show
:show-toolbar-separator="false"
:open3-d-icon="false"
:open-quote-icon="false"
:open-more-dialog="false"
:creative-icon="false"
:is-link-content="false"
placeholder="请输入决策标准"
@blur="onBlur"
/>
</div>
</div>
</Section>
@@ -92,12 +95,12 @@ function onBlur() {
.tox-tinymce {
overflow: hidden;
min-height: 140px !important;
border: 1px solid #D9D9D9;
border: 1px solid #d9d9d9;
border-radius: 8px;
}
.tox-edit-area__iframe {
background-color: #FFFFFF !important;
background-color: #ffffff !important;
}
}
}

View File

@@ -1,7 +1,6 @@
<script setup>
import { computed, defineProps } from 'vue'
import SectionTitle from '../../components/SectionTitle.vue'
import Section from '../../components/Section.vue'
@@ -10,8 +9,6 @@ const props = defineProps({
readonly: { type: Boolean, default: false }
})
const conceptTypeEnum = {
newest: 1,
standard: 0,
@@ -20,25 +17,24 @@ const conceptTypeEnum = {
0: 'standard'
}
const list = computed(() => props.report?.config || [])
</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
v-for="item in list"
:key="item.id"
class="item"
:class="{ [conceptTypeEnum[item.concept_type]]: true }"
>
<div class="name">
<div class="text">{{ item.concept_name || '' }}</div>
</div>
<img v-if="item.concept_url" :src="item.concept_url" alt="" class="img">
<img v-if="item.concept_url" :src="item.concept_url" alt="" class="img" />
</div>
</div>
</Section>
@@ -65,27 +61,27 @@ const list = computed(() => props.report?.config || [])
height: 152px;
margin-right: 2px;
border-radius: 6px;
border: 1px solid #DFE0E3;
border: 1px solid #dfe0e3;
&:not(:last-child) {
margin-right: 18px;
}
&.newest .name {
color: #70B936;
color: #70b936;
background: rgba(112, 185, 54, 0.16);
&::before {
background-color: #70B936;
background-color: #70b936;
}
}
&.standard .name {
color: #FFAA00;
color: #ffaa00;
background: rgba(255, 170, 0, 0.16);
&::before {
background-color: #FFAA00;
background-color: #ffaa00;
}
}
@@ -100,7 +96,7 @@ const list = computed(() => props.report?.config || [])
&::before {
display: block;
content: "";
content: '';
width: 6px;
height: 6px;
margin-right: 4px;

View File

@@ -0,0 +1,123 @@
<script setup>
import { computed, defineProps } from 'vue'
import SectionTitle from '../../components/SectionTitle.vue'
import Section from '../../components/Section.vue'
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
readonly: { type: Boolean, default: false }
})
const typeEnum = {
newest: 1,
standard: 0,
1: 'newest',
0: 'standard'
}
const typeLabelMap = {
1: '新品口味',
0: '标杆口味'
}
const list = computed(() => props.report?.config || [])
</script>
<template>
<SectionTitle>测试口味</SectionTitle>
<Section class="section">
<div class="list scrollbar">
<div
v-for="item in list"
:key="item.id"
class="item"
:class="{ [typeEnum[item.concept_type]]: true }"
>
<div class="code">
{{ typeLabelMap[item.concept_type] || '' }}{{ item.concept_encode || '' }}
</div>
<div class="name">{{ item.concept_name || '' }}</div>
</div>
</div>
</Section>
</template>
<style scoped lang="scss">
.section {
padding-bottom: 0;
}
.list {
overflow-x: auto;
width: 100%;
padding-bottom: 16px;
}
.item {
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
margin-bottom: 10px;
&.newest {
.code,
.name {
color: #70b936;
background: rgba(112, 185, 54, 0.16);
&::before {
background-color: #70b936;
}
}
}
&.standard {
.code,
.name {
color: #ffaa00;
background: rgba(255, 170, 0, 0.16);
&::before {
background-color: #ffaa00;
}
}
}
.code {
flex: none;
display: flex;
justify-content: flex-start;
align-items: center;
width: 120px;
height: 32px;
padding-left: 10px;
border-radius: 4px;
&::before {
display: block;
content: '';
width: 6px;
height: 6px;
margin-right: 4px;
border-radius: 50%;
}
}
.name {
flex: auto;
display: flex;
justify-content: flex-start;
align-items: center;
width: calc(100% - 132px);
height: 32px;
margin-left: 12px;
padding: 0 10px;
border: 1px solid #dfe0e3;
border-radius: 4px;
}
}
</style>

View File

@@ -1,155 +0,0 @@
<script setup>
import { defineProps, ref, watch } from 'vue'
import Section from '../../../components/Section.vue'
import SectionTitle from '../../../components/SectionTitle.vue'
import PeopleDislike from './PeopleDislike.vue'
import PeopleLike from './PeopleLike.vue'
import ProductImage from './ProductImage.vue'
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
readonly: { type: Boolean, default: false }
})
const activeKey = ref('0')
const tabList = ref([])
watch(() => props.report, () => {
const tabHeaders = []
const list = props.report?.hotChatVOS || []
list.forEach((item) => {
const headers = item.headerVOS || []
const hotData = item.hotDataVOS || []
headers.forEach((header) => {
const index = tabHeaders.findIndex((tabHeader) => tabHeader.key === header.key)
const child = {
type: item.type,
question: JSON.parse(JSON.stringify(item.questionVOS.find((item) => item.key === header.key)?.questionVO || {}))
}
child.question.list.forEach((group) => {
group.options?.forEach?.((option) => {
option.option_config.__rate__ = hotData.find((data) => {
if(data.key !== header.key) {
return false
}
if(data.index.startsWith('Q')) {
const [temp, questionIndex] = /Q(\d*)A(\d*)/g.exec(data.index)
if(group.relation_question_index === +questionIndex) {
return data.index === option.option_key
}
} else {
return +data.index === +option.option_key
}
}
)?.rate || 0
option.option_config.__rate__ += '%'
})
})
if(index > -1) {
if(!tabHeaders[index].children) {
tabHeaders[index].children = []
}
tabHeaders[index].children.push(child)
} else {
tabHeaders.push({
...header,
name: header.value,
children: [child]
})
}
})
})
const wordCloudList = props.report?.wordCloudVOS || []
wordCloudList.forEach((wordCloud) => {
const index = tabHeaders.findIndex((tabHeader) => tabHeader.key === wordCloud.headerVO.key)
if(index > -1) {
if(!tabHeaders[index].children) {
tabHeaders[index].children = []
}
const childIndex = tabHeaders[index].children.findIndex((child) => wordCloud.type.startsWith(child.type))
if(childIndex > -1) {
tabHeaders[index].children[childIndex].wordCloud = wordCloud.wordCloudVOS || []
} else {
tabHeaders[index].children.push({
type: wordCloud.type,
question: {},
wordCloud: wordCloud.wordCloudVOS || []
})
}
} else {
tabHeaders.push({
...wordCloud.headerVO,
name: wordCloud.headerVO.value,
children: [{
type: wordCloud.type,
question: {},
wordCloud: wordCloud.wordCloudVOS || []
}]
})
}
})
tabList.value = tabHeaders || []
activeKey.value = tabList.value?.[0]?.key
}, { immediate: true })
</script>
<template>
<SectionTitle>概念诊断</SectionTitle>
<Section class="section">
<a-tabs v-model:activeKey="activeKey" :animated="false">
<a-tab-pane v-for="(item) in tabList" :key="item.key" :tab="item.name">
<div class="tab-container">
<PeopleDislike :type-str="item.name"
:report="props.report"
:data="item.children.find((child) => child.type.indexOf('不喜欢') > -1)" />
<PeopleLike title="消费者喜欢的方面"
:type-str="item.name"
:report="props.report"
:data="item.children.find((child) => child.type.indexOf('喜欢') > -1 && child.type.indexOf('不喜欢') === -1)" />
<ProductImage :report="props.report" />
</div>
</a-tab-pane>
</a-tabs>
</Section>
</template>
<style scoped lang="scss">
.section {
padding: 0;
}
:deep(.ant-tabs) {
.ant-tabs-bar {
margin-bottom: 26px;
padding-left: 16px;
padding-right: 16px;
}
}
.tab-container {
padding: 0 16px;
}
.icon {
margin-left: 6px;
font-size: 14px;
color: #B9B9B9;
}
</style>

View File

@@ -0,0 +1,170 @@
<script setup>
import { defineProps, ref, watch } from 'vue'
import Section from '../../../components/Section.vue'
import SectionTitle from '../../../components/SectionTitle.vue'
import PeopleDislike from './PeopleDislike.vue'
import PeopleLike from './PeopleLike.vue'
import ProductImage from './ProductImage.vue'
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
readonly: { type: Boolean, default: false }
})
const activeKey = ref('0')
const tabList = ref([])
watch(
() => props.report,
() => {
const tabHeaders = []
const list = props.report?.hotChatVOS || []
list.forEach((item) => {
const headers = item.headerVOS || []
const hotData = item.hotDataVOS || []
headers.forEach((header) => {
const index = tabHeaders.findIndex((tabHeader) => tabHeader.key === header.key)
const child = {
type: item.type,
question: JSON.parse(
JSON.stringify(
item.questionVOS.find((item) => item.key === header.key)?.questionVO || {}
)
)
}
child.question.list.forEach((group) => {
group.options?.forEach?.((option) => {
option.option_config.__rate__ =
hotData.find((data) => {
if (data.key !== header.key) {
return false
}
if (data.index.startsWith('Q')) {
const [temp, questionIndex] = /Q(\d*)A(\d*)/g.exec(data.index)
if (group.relation_question_index === +questionIndex) {
return data.index === option.option_key
}
} else {
return +data.index === +option.option_key
}
})?.rate || 0
option.option_config.__rate__ += '%'
})
})
if (index > -1) {
if (!tabHeaders[index].children) {
tabHeaders[index].children = []
}
tabHeaders[index].children.push(child)
} else {
tabHeaders.push({
...header,
name: header.value,
children: [child]
})
}
})
})
const wordCloudList = props.report?.wordCloudVOS || []
wordCloudList.forEach((wordCloud) => {
const index = tabHeaders.findIndex((tabHeader) => tabHeader.key === wordCloud.headerVO.key)
if (index > -1) {
if (!tabHeaders[index].children) {
tabHeaders[index].children = []
}
const childIndex = tabHeaders[index].children.findIndex((child) =>
wordCloud.type.startsWith(child.type)
)
if (childIndex > -1) {
tabHeaders[index].children[childIndex].wordCloud = wordCloud.wordCloudVOS || []
} else {
tabHeaders[index].children.push({
type: wordCloud.type,
question: {},
wordCloud: wordCloud.wordCloudVOS || []
})
}
} else {
tabHeaders.push({
...wordCloud.headerVO,
name: wordCloud.headerVO.value,
children: [
{
type: wordCloud.type,
question: {},
wordCloud: wordCloud.wordCloudVOS || []
}
]
})
}
})
tabList.value = tabHeaders || []
activeKey.value = tabList.value?.[0]?.key
},
{ immediate: true }
)
</script>
<template>
<SectionTitle>概念诊断</SectionTitle>
<Section class="section">
<a-tabs v-model:activeKey="activeKey" :animated="false">
<a-tab-pane v-for="item in tabList" :key="item.key" :tab="item.name">
<div class="tab-container">
<PeopleDislike
:type-str="item.name"
:show-hot-area="true"
:report="props.report"
:data="item.children.find((child) => child.type.indexOf('不喜欢') > -1)"
/>
<PeopleLike
title="消费者喜欢的方面"
:show-hot-area="true"
:type-str="item.name"
:report="props.report"
:data="
item.children.find(
(child) => child.type.indexOf('喜欢') > -1 && child.type.indexOf('不喜欢') === -1
)
"
/>
<ProductImage :report="props.report" />
</div>
</a-tab-pane>
</a-tabs>
</Section>
</template>
<style scoped lang="scss">
.section {
padding: 0;
}
:deep(.ant-tabs) {
.ant-tabs-bar {
margin-bottom: 26px;
padding-left: 16px;
padding-right: 16px;
}
}
.tab-container {
padding: 0 16px;
}
.icon {
margin-left: 6px;
font-size: 14px;
color: #b9b9b9;
}
</style>

View File

@@ -1,19 +1,13 @@
<script setup>
import PeopleLike from './PeopleLike.vue'
</script>
<template>
<PeopleLike title="消费者不喜欢的方面" color="#FC4545">
<template #title-icon>
<img src="../../../img/icon_dislike.png" alt="" class="icon">
<img src="../../../img/icon_dislike.png" alt="" class="icon" />
</template>
</PeopleLike>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -17,6 +17,7 @@ import useDownload from '@/composables/useDownload'
const download = useDownload()
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
showHotArea: { type: Boolean, default: false },
title: { type: String, default: '消费者喜欢的方面' },
typeStr: { type: String, default: '' },
data: { type: Object, default: () => Object.assign({}) },
@@ -24,20 +25,20 @@ const props = defineProps({
})
const question = computed(() => {
return Object.assign(
{ associate: [] },
props.data.question || {},
{
config: {
...(props.data.question?.config || {}),
img_url: props.data.question?.config?.img_url || ''
},
options: props.data.question?.list.map((item) => item.options) || [],
permissions: {
disable_option_update: 1
}
if (!props.showHotArea) {
return {}
}
return Object.assign({ associate: [] }, props.data.question || {}, {
config: {
...(props.data.question?.config || {}),
img_url: props.data.question?.config?.img_url || ''
},
options: props.data.question?.list.map((item) => item.options) || [],
permissions: {
disable_option_update: 1
}
)
})
})
const formModal = computed(() => {
return {
@@ -47,13 +48,11 @@ const formModal = computed(() => {
}
})
const imageAreaWrapper = ref(null)
const chartShown = ref(true)
const activeRow = ref(props.data.wordCloud?.[0])
const wordCloudColumns = ref([
{
key: 'name',
@@ -74,10 +73,10 @@ const wordCloudTableData = computed(() => {
const total = props.data.wordCloud?.reduce((prev, curr) => prev + curr.num, 0)
return props.data.wordCloud?.map?.((item, index) => {
if(item.rate === undefined || item.rate === null) {
item.rate = +((100 * item.num / total).toFixed(0))
if (item.rate === undefined || item.rate === null) {
item.rate = +((100 * item.num) / total).toFixed(0)
}
if(!item.rate) {
if (!item.rate) {
item.rate = 0
}
item.seq = index + 1
@@ -100,20 +99,23 @@ const columns = ref([
minWidth: '100px'
}
])
const tableData = computed(() => activeRow.value?.originalName?.map?.((item, index) => {
return { id: index + 1, seq: index + 1, original: item }
}))
const tableData = computed(() =>
activeRow.value?.originalName?.map?.((item, index) => {
return { id: index + 1, seq: index + 1, original: item }
})
)
function downloadHotAreaImage() {
if(!imageAreaWrapper.value) {
if (!imageAreaWrapper.value) {
return
}
let img_url = props.data.question?.config?.img_url || ''
if(!img_url.startsWith('https://cxp-pubcos') && !img_url.startsWith('https://test-cxp-pubcos')) {
if (!img_url.startsWith('https://cxp-pubcos') && !img_url.startsWith('https://test-cxp-pubcos')) {
// cdn
const cdn = currentMode === 'prod' ? 'https://cxp-pubcos.yili.com' : 'https://test-cxp-pubcos.yili.com'
const cdn =
currentMode === 'prod' ? 'https://cxp-pubcos.yili.com' : 'https://test-cxp-pubcos.yili.com'
img_url = props.data.question?.config?.img_url.substring(8).split('/')
img_url.splice(0, 1, cdn)
img_url = img_url.join('/')
@@ -134,13 +136,13 @@ function downloadHotAreaImage() {
ctx.drawImage(image, 0, 0)
question.value.options?.forEach((group) => {
if(!group?.length) {
if (!group?.length) {
return
}
group.forEach((option) => {
const childArea = option.option_config?.child_area
if(!childArea?.length) {
if (!childArea?.length) {
return
}
@@ -161,10 +163,10 @@ function downloadHotAreaImage() {
ctx.strokeStyle = '#FFFFFF'
ctx.strokeRect(item.left, item.top, item.width, item.height)
if(item.rate) {
const fontSize = Math.max(18, Math.round(18 * width / 500))
if (item.rate) {
const fontSize = Math.max(18, Math.round((18 * width) / 500))
ctx.font = `bold ${ fontSize }px Source Han Sans, sans-serif`
ctx.font = `bold ${fontSize}px Source Han Sans, sans-serif`
ctx.textBaseline = 'middle'
const textMetrics = ctx.measureText(item.rate)
@@ -186,7 +188,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
@@ -203,7 +207,10 @@ function downloadHotAreaImage() {
const url = canvas.toDataURL('image/png', 1)
download.downloadBase64(url, `热区图_${ props.title }_${ props.typeStr || '' }_${ moment().format('YYYYMMDDHHmm') }.png`)
download.downloadBase64(
url,
`热区图_${props.title}_${props.typeStr || ''}_${moment().format('YYYYMMDDHHmm')}.png`
)
}
}
@@ -214,7 +221,7 @@ function onWordCloudChange(evt) {
}
function downloadKeywordList() {
if(chartShown.value) {
if (chartShown.value) {
downloadKeywordWordCloudChart()
} else {
downloadKeywordTable()
@@ -222,13 +229,18 @@ function downloadKeywordList() {
}
function downloadKeywordWordCloudChart() {
wordCloudRef.value?.downloadChart?.(`词云图_${ props.title }_${ props.typeStr || '' }_${ moment().format('YYYYMMDDHHmm') }.png`)
wordCloudRef.value?.downloadChart?.(
`词云图_${props.title}_${props.typeStr || ''}_${moment().format('YYYYMMDDHHmm')}.png`
)
}
function downloadKeywordTable() {
exportJsonToExcel({
data: [['关键词', '比例'], ...wordCloudTableData.value?.map?.((item) => [item.name, item.rate])],
filename: `关键词比例_${ props.title }_${ props.typeStr || '' }_${ moment().format('YYYYMMDDHHmm') }`
data: [
['关键词', '比例'],
...wordCloudTableData.value?.map?.((item) => [item.name, item.rate])
],
filename: `关键词比例_${props.title}_${props.typeStr || ''}_${moment().format('YYYYMMDDHHmm')}`
})
}
@@ -241,13 +253,15 @@ function onActiveRow(record) {
}
function downloadDetailTable() {
if(!activeRow.value) {
if (!activeRow.value) {
message.warning('请选择一个关键词')
return
}
exportJsonToExcel({
data: [['序号', '原文'], ...tableData.value?.map?.((item) => [item.seq, item.original])],
filename: `关键词原文_${ props.title }_${ props.typeStr || '' }_${ activeRow.value.name }_${ moment().format('YYYYMMDDHHmm') }`
filename: `关键词原文_${props.title}_${props.typeStr || ''}_${
activeRow.value.name
}_${moment().format('YYYYMMDDHHmm')}`
})
}
</script>
@@ -256,80 +270,88 @@ function downloadDetailTable() {
<Section class="section">
<div class="section-header">
<slot name="title-icon">
<img src="../../../img/icon_like.png" alt="" class="title-icon">
<img src="../../../img/icon_like.png" alt="" class="title-icon" />
</slot>
<span class="title-text">{{ props.title }}</span>
</div>
<div class="section-content">
<div class="area-wrapper">
<div class="button-wrapper">
<div class="action-button" @click="downloadHotAreaImage">
<DownloadOutlined class="icon" />
<template v-if="props.showHotArea">
<div class="area-wrapper">
<div class="button-wrapper">
<div class="action-button" @click="downloadHotAreaImage">
<DownloadOutlined class="icon" />
</div>
</div>
<div ref="imageAreaWrapper" class="image-area-wrapper">
<div class="image-area-wrapper-inner" :style="{ color: props.color }">
<ImageArea
:info="question"
:url="formModal.url"
:option="formModal.option"
:loading="formModal.loading"
:type="formModal.type"
:count="0"
:uploadCount="1"
:index="0"
:readonly="true"
:isAdd="false"
:edit="false"
:multiple="true"
:isHotArea="true"
/>
</div>
</div>
</div>
<div ref="imageAreaWrapper" class="image-area-wrapper">
<div class="image-area-wrapper-inner" :style="{color: props.color}">
<ImageArea :info="question"
:url="formModal.url"
:option="formModal.option"
:loading="formModal.loading"
:type="formModal.type"
:count="0"
:uploadCount="1"
:index="0"
:readonly="true"
:isAdd="false"
:edit="false"
:multiple="true"
:isHotArea="true" />
</div>
</div>
</div>
<a-divider type="vertical" class="solid-divider" />
<a-divider type="vertical" class="solid-divider" />
</template>
<div class="word-cloud-wrapper">
<a-spin v-if="props.report?.status === 1"
:spinning="props.report?.status === 1"
size="large"
tip="分析中"
class="word-cloud-loading" />
<a-spin
v-if="props.report?.status === 1"
:spinning="props.report?.status === 1"
size="large"
tip="分析中"
class="word-cloud-loading"
/>
<div class="button-wrapper">
<div class="action-button" @click="downloadKeywordList">
<DownloadOutlined class="icon" />
</div>
<div class="action-button" @click="showChart(!chartShown)">
<CloudOutlined v-if="!chartShown" class="icon" />
<AlignRightOutlined v-else class="icon" style="rotate: 180deg;" />
<AlignRightOutlined v-else class="icon" style="rotate: 180deg" />
</div>
</div>
<WordCloud ref="wordCloudRef"
v-show="chartShown"
:data-source="wordCloudTableData"
@change="onWordCloudChange" />
<SmallTable v-show="!chartShown"
:data-source="wordCloudTableData"
:columns="wordCloudColumns"
:row-selectable="true"
:row-selected="activeRow"
@active="onActiveRow" />
<WordCloud
ref="wordCloudRef"
v-show="chartShown"
:data-source="wordCloudTableData"
@change="onWordCloudChange"
/>
<SmallTable
v-show="!chartShown"
:data-source="wordCloudTableData"
:columns="wordCloudColumns"
:row-selectable="true"
:row-selected="activeRow"
@active="onActiveRow"
/>
</div>
<a-divider type="vertical" class="dashed-divider" />
<div class="table-wrapper">
<a-spin v-if="props.report?.status === 1"
:spinning="props.report?.status === 1"
size="large"
tip="分析中"
class="word-cloud-loading" />
<a-spin
v-if="props.report?.status === 1"
:spinning="props.report?.status === 1"
size="large"
tip="分析中"
class="word-cloud-loading"
/>
<div class="button-wrapper original-title">
{{ activeRow?.name }}
</div>
@@ -337,12 +359,11 @@ function downloadDetailTable() {
<SmallTable :data-source="tableData" :columns="columns" />
</div>
</div>
</Section>
</template>
<style scoped lang="scss">
@import "./style.scss";
@import './style.scss';
.image-area-wrapper {
height: calc(100% - 28px);
@@ -365,6 +386,6 @@ function downloadDetailTable() {
.original-title {
justify-content: flex-start !important;
color: #81B74C;
color: #81b74c;
}
</style>

View File

@@ -8,29 +8,32 @@ const props = defineProps({
report: { type: Object, default: () => Object.assign({}) }
})
const tableData = computed(() => props.report?.chatVOS?.find?.((data) => data.type === '概念形象') || {})
const tableData = computed(
() => props.report?.chatVOS?.find?.((data) => data.type === '概念形象') || {}
)
const codeStr = computed(() => getTableCodeRow(tableData.value))
function getTableCodeRow(tableData) {
if(!tableData?.dataVOS?.length) {
if (!tableData?.dataVOS?.length) {
return ''
}
const codeRow = tableData.dataVOS.find((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
if(!codeRow) {
const codeRow = tableData.dataVOS.find((row) =>
Object.keys(row).find((key) => ['概念编码', '口味编码'].includes(row[key]))
)
if (!codeRow) {
return ''
}
return Object.keys(codeRow).filter((key) => /^[a-zA-Z]*$/g.test(codeRow[key])).map((key) => codeRow[key]).join('/')
return Object.keys(codeRow)
.filter((key) => /^[a-zA-Z]*$/g.test(codeRow[key]))
.map((key) => codeRow[key])
.join('/')
}
</script>
<template>
<Section class="section">
<div class="section-header">
<img src="../../../img/icon_light.png" alt="" class="title-icon">
<img src="../../../img/icon_light.png" alt="" class="title-icon" />
<span class="title-text">概念传递产品形象</span>
</div>
@@ -42,12 +45,11 @@ function getTableCodeRow(tableData) {
<span>意为该数值在90%的显著水平下显著高</span>
</div>
</div>
</Section>
</template>
<style scoped lang="scss">
@import "./style.scss";
@import './style.scss';
.main-wrapper {
padding: 16px;
@@ -61,7 +63,7 @@ function getTableCodeRow(tableData) {
.emphasize {
margin-right: 4px;
color: #FC4545;
color: #fc4545;
}
}
</style>

View File

@@ -0,0 +1,140 @@
<script setup>
import { defineProps, ref, watch } from 'vue'
import Section from '../../../components/Section.vue'
import SectionTitle from '../../../components/SectionTitle.vue'
import PeopleDislike from './PeopleDislike.vue'
import PeopleLike from './PeopleLike.vue'
const props = defineProps({
report: { type: Object, default: () => Object.assign({}) },
readonly: { type: Boolean, default: false }
})
const activeKey = ref('0')
const tabList = ref([])
watch(
() => props.report,
() => {
const tabHeaders = []
const list = props.report?.hotChatVOS || []
list.forEach((item) => {
const headers = item.headerVOS || []
headers.forEach((header) => {
const index = tabHeaders.findIndex((tabHeader) => tabHeader.key === header.key)
const child = {
type: item.type,
question: {}
}
if (index > -1) {
if (!tabHeaders[index].children) {
tabHeaders[index].children = []
}
tabHeaders[index].children.push(child)
} else {
tabHeaders.push({
...header,
name: header.value,
children: [child]
})
}
})
})
const wordCloudList = props.report?.wordCloudVOS || []
wordCloudList.forEach((wordCloud) => {
const index = tabHeaders.findIndex((tabHeader) => tabHeader.key === wordCloud.headerVO.key)
if (index > -1) {
if (!tabHeaders[index].children) {
tabHeaders[index].children = []
}
const childIndex = tabHeaders[index].children.findIndex((child) =>
wordCloud.type.startsWith(child.type)
)
if (childIndex > -1) {
tabHeaders[index].children[childIndex].wordCloud = wordCloud.wordCloudVOS || []
} else {
tabHeaders[index].children.push({
type: wordCloud.type,
question: {},
wordCloud: wordCloud.wordCloudVOS || []
})
}
} else {
tabHeaders.push({
...wordCloud.headerVO,
name: wordCloud.headerVO.value,
children: [
{
type: wordCloud.type,
question: {},
wordCloud: wordCloud.wordCloudVOS || []
}
]
})
}
})
tabList.value = tabHeaders || []
activeKey.value = tabList.value?.[0]?.key
},
{ immediate: true }
)
</script>
<template>
<SectionTitle>口味诊断</SectionTitle>
<Section class="section">
<a-tabs v-model:activeKey="activeKey" :animated="false">
<a-tab-pane v-for="item in tabList" :key="item.key" :tab="item.name">
<div class="tab-container">
<PeopleDislike
:type-str="item.name"
:report="props.report"
:data="item.children.find((child) => child.type.indexOf('不喜欢') > -1)"
/>
<PeopleLike
title="消费者喜欢的方面"
:type-str="item.name"
:report="props.report"
:data="
item.children.find(
(child) => child.type.indexOf('喜欢') > -1 && child.type.indexOf('不喜欢') === -1
)
"
/>
</div>
</a-tab-pane>
</a-tabs>
</Section>
</template>
<style scoped lang="scss">
.section {
padding: 0;
}
:deep(.ant-tabs) {
.ant-tabs-bar {
margin-bottom: 26px;
padding-left: 16px;
padding-right: 16px;
}
}
.tab-container {
padding: 0 16px;
}
.icon {
margin-left: 6px;
font-size: 14px;
color: #b9b9b9;
}
</style>

View File

@@ -11,9 +11,9 @@
align-items: center;
height: 46px;
padding: 13px 18px;
border-bottom: 1px solid #D8D8D8;
border-bottom: 1px solid #d8d8d8;
font-size: 16px;
font-family: "Source Han Sans", sans-serif;
font-family: 'Source Han Sans', sans-serif;
.title-icon {
display: block;
@@ -54,12 +54,12 @@
width: 22px;
height: 22px;
border-radius: 6px;
color: #A8AAA6;
color: #a8aaa6;
background-color: rgba(216, 216, 216, 0.2);
&:hover {
color: #434343;
background-color: #D9D9D9;
background-color: #d9d9d9;
cursor: pointer;
}
@@ -70,12 +70,12 @@
.solid-divider {
height: 100%;
border-left: 1px solid #D8D8D8;
border-left: 1px solid #d8d8d8;
}
.dashed-divider {
height: calc(100% - 80px);
border-left: 1px dashed #D8D8D8;
border-left: 1px dashed #d8d8d8;
}
.area-wrapper {
@@ -123,5 +123,4 @@
margin-top: 8px;
}
}
}