feat: 新增口味测试洞察报告
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = '报告更新中,不能修改'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
123
src/views/DataAnalyse/insight/report/section/TestingTaste.vue
Normal file
123
src/views/DataAnalyse/insight/report/section/TestingTaste.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user