洞察报告;
This commit is contained in:
@@ -22,7 +22,7 @@ npm config set registry %customizeRegistry% && ^
|
|||||||
echo ====clear npm cache==== && ^
|
echo ====clear npm cache==== && ^
|
||||||
npm cache clear --force && ^
|
npm cache clear --force && ^
|
||||||
echo ====install dependency package==== && ^
|
echo ====install dependency package==== && ^
|
||||||
npm i --force && ^
|
npm ci --force && ^
|
||||||
echo ====finished install, switch back to original npm registry==== && ^
|
echo ====finished install, switch back to original npm registry==== && ^
|
||||||
npm config set registry %originalRegistry% && ^
|
npm config set registry %originalRegistry% && ^
|
||||||
echo ====finish and exit==== && ^
|
echo ====finish and exit==== && ^
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function json_to_array(key, jsonData) {
|
|||||||
* @param data Array,表体数据
|
* @param data Array,表体数据
|
||||||
* @param key Array,字段名
|
* @param key Array,字段名
|
||||||
* @param title String,标题(会居中显示),即excel表格第一行
|
* @param title String,标题(会居中显示),即excel表格第一行
|
||||||
|
* @param sheetName
|
||||||
* @param filename String,文件名
|
* @param filename String,文件名
|
||||||
* @param autoWidth Boolean,是否自动根据key自定义列宽度
|
* @param autoWidth Boolean,是否自动根据key自定义列宽度
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +47,7 @@ export const exportJsonToExcel = ({
|
|||||||
data,
|
data,
|
||||||
key,
|
key,
|
||||||
title,
|
title,
|
||||||
|
sheetName,
|
||||||
filename,
|
filename,
|
||||||
autoWidth
|
autoWidth
|
||||||
}) => {
|
}) => {
|
||||||
@@ -64,7 +66,7 @@ export const exportJsonToExcel = ({
|
|||||||
const arr = json_to_array(key, data)
|
const arr = json_to_array(key, data)
|
||||||
auto_width(ws, arr)
|
auto_width(ws, arr)
|
||||||
}
|
}
|
||||||
XLSX.utils.book_append_sheet(wb, ws, filename)
|
XLSX.utils.book_append_sheet(wb, ws, sheetName)
|
||||||
XLSX.writeFile(wb, filename + '.xlsx')
|
XLSX.writeFile(wb, filename + '.xlsx')
|
||||||
}
|
}
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import TemplateMarket from './route.templateMarket'
|
|||||||
import Contact from './route.contact'
|
import Contact from './route.contact'
|
||||||
import DocumentLibrary from './route.documentLibrary'
|
import DocumentLibrary from './route.documentLibrary'
|
||||||
import DataStatistics from './route.datastatistics'
|
import DataStatistics from './route.datastatistics'
|
||||||
|
import SharedRoutes from './route.shared'
|
||||||
import { jsonp } from "vue-jsonp";
|
import { jsonp } from "vue-jsonp";
|
||||||
import { jsonpUrl } from "../config.js";
|
import { jsonpUrl } from "../config.js";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
@@ -24,6 +25,7 @@ const constantRoutes = [
|
|||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
// },
|
// },
|
||||||
|
...SharedRoutes,
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)',
|
path: '/:catchAll(.*)',
|
||||||
redirect: '/error/404',
|
redirect: '/error/404',
|
||||||
@@ -471,7 +473,7 @@ const router = createRouter({
|
|||||||
})
|
})
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
// token
|
// token
|
||||||
if (!router.options.history.state.back) {
|
if (!to.meta.shared && !router.options.history.state.back) {
|
||||||
await getToken();
|
await getToken();
|
||||||
}
|
}
|
||||||
if (!to.meta.noRedirectLogin) {
|
if (!to.meta.noRedirectLogin) {
|
||||||
|
|||||||
20
src/router/route.shared.js
Normal file
20
src/router/route.shared.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const SharedIndex = () => import(/* webpackChunkName: 'shared' */ '@views/Shared/Index.vue')
|
||||||
|
|
||||||
|
const SharedInsightReport = () => import(/* webpackChunkName: 'shared' */ '@views/DataAnalyse/insight/SharedInsightReport.vue')
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/shared',
|
||||||
|
name: 'Shared',
|
||||||
|
meta: { title: '分享', shared: true },
|
||||||
|
component: SharedIndex,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'insight/:secret',
|
||||||
|
name: 'SharedInsight',
|
||||||
|
component: SharedInsightReport
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
export default routes
|
||||||
@@ -10,7 +10,7 @@ import InsightShare from './components/InsightShare.vue'
|
|||||||
import Report from './report/Report.vue'
|
import Report from './report/Report.vue'
|
||||||
|
|
||||||
|
|
||||||
import { getInsightReport } from './api'
|
import { getInsightReport, updateInsightReport } from './api'
|
||||||
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -53,25 +53,53 @@ function onGenerateReport(isInit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onConfirmGenerateReport(isInit) {
|
async function onConfirmGenerateReport(isInit) {
|
||||||
|
if(loading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
// todo
|
// todo
|
||||||
|
const params = {}
|
||||||
|
const data = await updateInsightReport(params)
|
||||||
|
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="insight-page">
|
<a-spin :spinning="loading" tip="加载中" class="spinning">
|
||||||
<InsightEmpty v-if="false" @generate="onGenerateReport" />
|
<div v-if="!loading" class="insight-page">
|
||||||
|
<InsightEmpty v-if="!report?.id" @generate="onGenerateReport" />
|
||||||
|
|
||||||
<InsightShare @regenerate="onGenerateReport" />
|
<template v-if="report?.id">
|
||||||
|
<InsightShare :report="report" @regenerate="onGenerateReport" />
|
||||||
|
|
||||||
<Report :report="report" />
|
<Report :report="report" />
|
||||||
</div>
|
</template>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.insight-page {
|
.spinning {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
:deep(.ant-spin-text) {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.insight-page {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,100 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
|
||||||
|
import SharedAccess from './components/SharedAccess.vue'
|
||||||
|
import SharedExpired from './components/SharedExpired.vue'
|
||||||
|
import Report from './report/Report.vue'
|
||||||
|
|
||||||
|
import { getInsightReportBySecret, getInsightReportInfoSecret } from './api'
|
||||||
|
|
||||||
|
document.title = '伊调研|问卷'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const secret = route.params.secret || ''
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const isExpired = ref(false) // 默认 false
|
||||||
|
const isPassword = ref(true) // 默认 true
|
||||||
|
|
||||||
|
const accessRef = ref(null)
|
||||||
|
|
||||||
|
const report = ref({})
|
||||||
|
|
||||||
|
// getReportInfo()
|
||||||
|
|
||||||
|
async function getReportInfo() {
|
||||||
|
if(loading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const params = { secret }
|
||||||
|
const data = await getInsightReportInfoSecret(params)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getReport(password) {
|
||||||
|
if(loading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const params = { password, secret }
|
||||||
|
const data = await getInsightReportBySecret(params)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div class="shared-insight-report">
|
||||||
|
<SharedExpired v-if="!isExpired" />
|
||||||
|
<SharedAccess v-else-if="isPassword" ref="accessRef" @password="getReport" />
|
||||||
|
|
||||||
|
<div v-else class="wrapper">
|
||||||
|
<Report :readonly="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin v-if="loading" :spinning="loading" tip="加载中" class="spinning" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.spinning {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
|
||||||
|
:deep(.ant-spin-text) {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-insight-report {
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增/修改洞察报告详情
|
||||||
|
* @param data
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
export function updateInsightReport(data) {
|
||||||
|
return request({
|
||||||
|
url: `/console/insightReport`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取洞察报告详情
|
||||||
|
* @param params
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
export function getInsightReport(params) {
|
export function getInsightReport(params) {
|
||||||
return request({
|
return request({
|
||||||
url: `/console/insightReport/${ params.sn }`,
|
url: `/console/insightReport/${ params.sn }`,
|
||||||
@@ -11,3 +29,46 @@ export function getInsightReport(params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分享洞察报告
|
||||||
|
* @param data
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
export function shareReport(data) {
|
||||||
|
return request({
|
||||||
|
url: `/console/shareReport`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取洞察报告基本信息
|
||||||
|
* @param params
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
export function getInsightReportInfoSecret(params) {
|
||||||
|
return request({
|
||||||
|
url: `/console/insightReport/${ params.sn }`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取洞察报告详情 secret
|
||||||
|
* @param params
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
export function getInsightReportBySecret(params) {
|
||||||
|
return request({
|
||||||
|
url: `/console/insightReport/${ params.sn }`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineEmits, ref } from 'vue'
|
import { defineEmits, defineProps, ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
import { shareReport } from '../api'
|
||||||
|
|
||||||
|
|
||||||
const emits = defineEmits(['regenerate'])
|
const emits = defineEmits(['regenerate'])
|
||||||
|
const props = defineProps({
|
||||||
|
report: { type: Object, default: () => Object.assign({}) }
|
||||||
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const editable = ref(false)
|
const editable = ref(false)
|
||||||
const password = ref('') // 密码
|
const password = ref(props.report.password || getRandomStr(4) || '') // 密码
|
||||||
const expiredForDays = ref('0') // 有效期天数
|
const expiredForDays = ref(+props.report.expireAt || 0) // 有效期天数
|
||||||
|
|
||||||
|
function getRandomStr(bit) {
|
||||||
|
const random = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPSDFGHJKLZXCVBNM'
|
||||||
|
let str = ''
|
||||||
|
for(let i = 0; i < bit; i += 1) {
|
||||||
|
str += random[Math.floor(Math.random() * random.length)]
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
function onEdit(type) {
|
function onEdit(type) {
|
||||||
if(loading.value) {
|
if(loading.value) {
|
||||||
@@ -24,8 +40,34 @@ function getPopupContainer(el) {
|
|||||||
return el?.parentNode || document.body
|
return el?.parentNode || document.body
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCopy() {
|
async function onCopy() {
|
||||||
// todo
|
if(loading.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
id: props.report.id || '',
|
||||||
|
passWord: password.value || '',
|
||||||
|
expireDay: expiredForDays.value ?? 0
|
||||||
|
}
|
||||||
|
const data = await shareReport(params)
|
||||||
|
console.log('data', data)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
|
||||||
|
|
||||||
|
copyText()
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyText(text) {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.value = text || ''
|
||||||
|
document.body.appendChild(input)
|
||||||
|
input.select()
|
||||||
|
document.execCommand('Copy')
|
||||||
|
document.body.removeChild(input)
|
||||||
|
message.success('复制成功')
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUpdateReport() {
|
function onUpdateReport() {
|
||||||
@@ -65,10 +107,10 @@ function onUpdateReport() {
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="label">链接有效期</span>
|
<span class="label">链接有效期</span>
|
||||||
<a-radio-group v-model:value="expiredForDays" :disabled="loading">
|
<a-radio-group v-model:value="expiredForDays" :disabled="loading">
|
||||||
<a-radio value="0">永久</a-radio>
|
<a-radio :value="0">永久</a-radio>
|
||||||
<a-radio value="30">30天</a-radio>
|
<a-radio :value="30">30天</a-radio>
|
||||||
<a-radio value="7">7天</a-radio>
|
<a-radio :value="7">7天</a-radio>
|
||||||
<a-radio value="1">1天</a-radio>
|
<a-radio :value="1">1天</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="row flex-end">
|
<div class="row flex-end">
|
||||||
|
|||||||
102
src/views/DataAnalyse/insight/components/SharedAccess.vue
Normal file
102
src/views/DataAnalyse/insight/components/SharedAccess.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineExpose, reactive, ref } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const emits = defineEmits(['password'])
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const formState = reactive({ password: '' })
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ pattern: /^[0-9a-zA-Z]{4,4}$/g, message: '请输入4位数字或字母', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
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('、'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ reset })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="shared-access">
|
||||||
|
<img src="../img/icon_lock.png" alt="" class="icon">
|
||||||
|
|
||||||
|
<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="请输入密码" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item class="no-margin">
|
||||||
|
<a-button type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
block
|
||||||
|
class="custom-button"
|
||||||
|
@click="onSubmit()">
|
||||||
|
确定
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.shared-access {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: 315px;
|
||||||
|
height: 180px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 358px;
|
||||||
|
padding: 20px 22px 20px 42px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #FAFAFA;
|
||||||
|
|
||||||
|
:deep(.ant-form-item-required)::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-margin {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
src/views/DataAnalyse/insight/components/SharedExpired.vue
Normal file
38
src/views/DataAnalyse/insight/components/SharedExpired.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="shared-expired">
|
||||||
|
<img src="../img/icon_expire.png" alt="" class="icon">
|
||||||
|
|
||||||
|
<div class="message">抱歉,您访问的链接已过期</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.shared-expired {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: 554px;
|
||||||
|
height: 307px;
|
||||||
|
margin: 0 auto 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
line-height: 28px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #8C8C8C;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
src/views/DataAnalyse/insight/img/icon_expire.png
Normal file
BIN
src/views/DataAnalyse/insight/img/icon_expire.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
BIN
src/views/DataAnalyse/insight/img/icon_lock.png
Normal file
BIN
src/views/DataAnalyse/insight/img/icon_lock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -36,6 +36,7 @@ const readonly = computed(() => props.readonly || false)
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.insight-report {
|
.insight-report {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding-bottom: 28px;
|
||||||
|
|
||||||
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
|
font-family: "Alibaba PuHuiTi 3.0", sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { computed, defineEmits, defineProps, ref } from 'vue'
|
|||||||
const emits = defineEmits(['active'])
|
const emits = defineEmits(['active'])
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
columns: { type: Array, default: () => [] },
|
columns: { type: Array, default: () => [] },
|
||||||
dataSource: { type: Array, default: () => [] }
|
dataSource: { type: Array, default: () => [] },
|
||||||
|
rowSelectable: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
@@ -33,6 +34,10 @@ function customRow(record = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onActiveRow(record) {
|
function onActiveRow(record) {
|
||||||
|
if (!props.rowSelectable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
activatedRecord.value = record
|
activatedRecord.value = record
|
||||||
emits('active', record)
|
emits('active', record)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,23 +4,96 @@ import { computed, defineProps, ref, watch } from 'vue'
|
|||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: { type: Object, default: () => Object.assign({}) }
|
data: { type: Object, default: () => Object.assign({}) },
|
||||||
|
barTable: { type: Boolean, default: false },
|
||||||
|
standardStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => Object.assign({
|
||||||
|
groupColor: '#FFAA00',
|
||||||
|
groupBackgroundColor: 'rgba(255, 178, 0, 0.2)',
|
||||||
|
columnColor: '#434343',
|
||||||
|
columnBackgroundColor: 'rgba(255, 178, 0, 0.05)'
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = computed(() => (props.data.headerVOS || []).map((column, columnIndex) => {
|
const tableData = ref([])
|
||||||
|
|
||||||
|
const headers = computed(() => {
|
||||||
|
const list = JSON.parse(JSON.stringify(props.data.headerVOS || []))
|
||||||
|
const rowTitle = list[0]
|
||||||
|
|
||||||
|
// 判断是否需要拆分列。(表格内容的第一列文字,是否包含下划线,根据下划线拆分列)
|
||||||
|
if(!tableData.value.some((data) => {
|
||||||
|
return data[rowTitle.dataIndex].indexOf('_') > -1
|
||||||
|
})) {
|
||||||
|
// 不需要拆分列,返回当前配置即可
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 把含有下划线的文字拆分为两列显示,并且合并列的表头。并且合并显示具有相同文字的连续的行。
|
||||||
|
* 例: 把 “女_非常喜欢+比较喜欢【TOP2】” 拆分未 “女” 和 “非常喜欢+比较喜欢【TOP2】” 两列
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 拆分出来的一列
|
||||||
|
const addedFirstRow = {
|
||||||
|
key: rowTitle.key,
|
||||||
|
dataIndex: rowTitle.dataIndex,
|
||||||
|
title: rowTitle.title,
|
||||||
|
colSpan: 2, // 合并并显示第一列
|
||||||
|
customRender: ({ text, index }) => {
|
||||||
|
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
|
||||||
|
|
||||||
|
return {
|
||||||
|
children: text.split('_')[0],
|
||||||
|
props: {
|
||||||
|
colSpan: text.split('_').length > 1 ? 1 : 2, // 由于拆分为两列,所以需要合并没有拆分的列
|
||||||
|
rowSpan // 合并连续行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rowTitle.colSpan = 0 // 合并并隐藏第二列
|
||||||
|
rowTitle.customRender = ({ text, index }) => {
|
||||||
|
return {
|
||||||
|
children: text.split('_')[1],
|
||||||
|
props: {
|
||||||
|
colSpan: Math.max(0, text.split('_').length - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
addedFirstRow,
|
||||||
|
...list
|
||||||
|
]
|
||||||
|
|
||||||
|
})
|
||||||
|
const columns = computed(() => headers.value.map((column, columnIndex) => {
|
||||||
column.align = 'center'
|
column.align = 'center'
|
||||||
column.slots = { customRender: column.dataIndex }
|
column.slots = { customRender: column.dataIndex }
|
||||||
column.customHeaderCell = function() {
|
column.customHeaderCell = function() {
|
||||||
const style = {}
|
const style = {}
|
||||||
if(columnIndex === 1) { // 新品表现,一级表头
|
if(column.title === '新品表现') { // 新品表现,一级表头
|
||||||
style.color = '#70B936'
|
style.color = '#70B936'
|
||||||
style.backgroundColor = 'rgba(112, 185, 54, 0.2)'
|
style.backgroundColor = 'rgba(112, 185, 54, 0.2)'
|
||||||
}
|
}
|
||||||
if(columnIndex === 2) { // 标杆表现,一级表头
|
if(column.title === '标杆表现') { // 标杆表现,一级表头
|
||||||
style.color = '#FFAA00'
|
style.color = props.standardStyle.groupColor
|
||||||
style.backgroundColor = 'rgba(255, 178, 0, 0.2)'
|
style.backgroundColor = props.standardStyle.groupBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表头(第一行)显示不同的文字颜色及背景色
|
||||||
return {
|
return {
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
@@ -30,33 +103,38 @@ const columns = computed(() => (props.data.headerVOS || []).map((column, columnI
|
|||||||
column.children.forEach((child) => {
|
column.children.forEach((child) => {
|
||||||
child.align = 'center'
|
child.align = 'center'
|
||||||
child.slots = { customRender: child.dataIndex }
|
child.slots = { customRender: child.dataIndex }
|
||||||
|
child.parentTitle = column.title
|
||||||
|
child.width = '120px'
|
||||||
child.customHeaderCell = function() {
|
child.customHeaderCell = function() {
|
||||||
const style = {}
|
const style = {}
|
||||||
if(columnIndex === 1) { // 新品表现,二级表头
|
if(column.title === '新品表现') { // 新品表现,二级表头
|
||||||
style.color = '#434343'
|
style.color = '#434343'
|
||||||
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
|
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
|
||||||
}
|
}
|
||||||
if(columnIndex === 2) { // 标杆表现,二级表头
|
if(column.title === '标杆表现') { // 标杆表现,二级表头
|
||||||
style.color = '#434343'
|
style.color = props.standardStyle.columnColor
|
||||||
style.backgroundColor = 'rgba(255, 178, 0, 0.05)'
|
style.backgroundColor = props.standardStyle.columnBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表头(第二行)显示不同的文字颜色及背景色
|
||||||
return {
|
return {
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
child.customCell = function(record, rowIndex) {
|
child.customCell = function(record, rowIndex) {
|
||||||
|
const rowClass = rowClassName(record, rowIndex)
|
||||||
const style = {}
|
const style = {}
|
||||||
if(columnIndex === 1) { // 新品表现,内容
|
if(column.title === '新品表现') { // 新品表现,内容
|
||||||
style.color = '#434343'
|
style.color = rowClass === 'gray-row' ? '#818181' : '#434343'
|
||||||
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
|
style.backgroundColor = 'rgba(112, 185, 54, 0.05)'
|
||||||
}
|
}
|
||||||
if(columnIndex === 2) { // 标杆表现,内容
|
if(column.title === '标杆表现') { // 标杆表现,内容
|
||||||
style.color = '#434343'
|
style.color = rowClass === 'gray-row' ? '#818181' : props.standardStyle.columnColor
|
||||||
style.backgroundColor = 'rgba(255, 178, 0, 0.05)'
|
style.backgroundColor = props.standardStyle.columnBackgroundColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表格内容显示不同的文字颜色及背景色
|
||||||
return {
|
return {
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
@@ -65,24 +143,113 @@ const columns = computed(() => (props.data.headerVOS || []).map((column, columnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
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))
|
const flatColumns = computed(() => columns.value.flatMap((column) => column.children?.length ? column.children : column))
|
||||||
const tableData = ref([])
|
|
||||||
|
const barMax = ref(0)
|
||||||
|
const barMin = ref(0)
|
||||||
|
|
||||||
watch(() => props.data, () => {
|
watch(() => props.data, () => {
|
||||||
tableData.value = props.data.dataVOS || []
|
tableData.value = props.data.dataVOS || []
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData.value = tableList
|
||||||
|
} else {
|
||||||
|
tableData.value = list
|
||||||
|
}
|
||||||
|
|
||||||
}, { immediate: true, deep: true })
|
}, { immediate: true, deep: true })
|
||||||
|
|
||||||
|
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 })
|
||||||
|
|
||||||
|
const barStyle = computed(() => (record, column) => {
|
||||||
|
const value = record[column.dataIndex] ? record[column.dataIndex] : 0
|
||||||
|
const width = (+value * 100 / barMax.value) + '%'
|
||||||
|
|
||||||
|
// 计算条形图的长度,根据最大值计算百分比即可
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
backgroundColor: column.parentTitle === '新品表现' ? '#70B936' : '#FFB200'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function rowClassName(record) {
|
||||||
|
// 表格中 '概念编码' 和 '样本基数' , 这两行要显示灰色
|
||||||
|
return Object.keys(record).some((key) => ['概念编码', '样本基数'].some((item) => record[key]?.indexOf?.(item) > -1)) ? 'gray-row' : ''
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="styled-table-wrapper">
|
<div class="styled-table-wrapper" :class="{'bar-table': props.barTable}">
|
||||||
<a-table :columns="columns" :data-source="tableData" bordered :pagination="false" class="table">
|
<a-table :columns="columns" :data-source="tableData" bordered :pagination="false"
|
||||||
<template v-for="(column) in flatColumns"
|
:row-class-name="rowClassName" class="table">
|
||||||
|
<template v-for="(column, columnIndex) in flatColumns"
|
||||||
:key="column.dataIndex"
|
:key="column.dataIndex"
|
||||||
#[column.dataIndex]="{text, record}">
|
#[column.dataIndex]="{record}">
|
||||||
<template v-if="true">{{ text }}</template>
|
<template v-if="props.barTable" data-desc="显示条形图">
|
||||||
<template v-if="false">{{ record[column.dataIndex] }}</template>
|
<span
|
||||||
|
v-if="[0].includes(columnIndex) || ['概念编码', '样本基数'].includes(record[headers[0]?.dataIndex])">
|
||||||
|
{{ record[column.dataIndex] }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div v-else class="cell-bar" :style="barStyle(record, column)">
|
||||||
|
<span class="text">{{ record[column.dataIndex] }}</span>
|
||||||
|
<span class="danger-text">{{ record[column.dataIndex + 'Type'] || '' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="record[headers[0].dataIndex] === '是否通过概念行动标准'"
|
||||||
|
data-desc="显示是否选择框">
|
||||||
|
<span v-if="record[column.dataIndex] === '是否通过概念行动标准'">
|
||||||
|
{{ record[column.dataIndex] }}
|
||||||
|
</span>
|
||||||
|
<a-select v-else placeholder="请选择" class="custom-select" style="width: 90px;">
|
||||||
|
<a-select-option key="1" value="1">
|
||||||
|
<span style="color: #70B936;">是</span>
|
||||||
|
</a-select-option>
|
||||||
|
<a-select-option key="0" value="0">
|
||||||
|
<span style="color: #FC4545;">否</span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
<template v-else data-desc="显示文字">
|
||||||
|
<span>{{ record[column.dataIndex] }}</span>
|
||||||
|
<span class="danger-text">{{ record[column.dataIndex + 'Type'] || '' }}</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +259,32 @@ watch(() => props.data, () => {
|
|||||||
.styled-table-wrapper {
|
.styled-table-wrapper {
|
||||||
$radius: 6px;
|
$radius: 6px;
|
||||||
|
|
||||||
|
.cell-bar {
|
||||||
|
min-width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: right;
|
||||||
|
color: #FFFFFF;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bar-table :deep(.ant-table-wrapper) {
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ant-table-wrapper) {
|
:deep(.ant-table-wrapper) {
|
||||||
.ant-table-thead > tr > th, .ant-table-tbody > tr > td {
|
.ant-table-thead > tr > th, .ant-table-tbody > tr > td {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
@@ -110,6 +303,16 @@ watch(() => props.data, () => {
|
|||||||
.ant-table-thead > tr:first-child > th:last-child {
|
.ant-table-thead > tr:first-child > th:last-child {
|
||||||
border-top-right-radius: $radius;
|
border-top-right-radius: $radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-table-body tr.gray-row td {
|
||||||
|
&:not([rowspan="2"]) {
|
||||||
|
color: #818181;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-text {
|
||||||
|
color: #FC4545;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps } from 'vue'
|
import { defineProps, ref, watch } from 'vue'
|
||||||
|
|
||||||
import Tinymce from '@/components/Tinymce.vue'
|
import Tinymce from '@/components/Tinymce.vue'
|
||||||
|
|
||||||
@@ -13,12 +13,19 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const richText = ref('')
|
||||||
|
|
||||||
|
watch(() => props.report.coreConclusion, (val) => {
|
||||||
|
richText.value = val || ''
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SectionTitle>核心结论</SectionTitle>
|
<SectionTitle>核心结论</SectionTitle>
|
||||||
<Section class="section">
|
<Section class="section">
|
||||||
<Tinymce :curtail="false"
|
<Tinymce v-model:editorData="richText"
|
||||||
|
:curtail="false"
|
||||||
:curtail-min-height="140"
|
:curtail-min-height="140"
|
||||||
show
|
show
|
||||||
:open3-d-icon="false"
|
:open3-d-icon="false"
|
||||||
|
|||||||
@@ -18,55 +18,66 @@ const activeKey = ref('0')
|
|||||||
const tabList = ref([])
|
const tabList = ref([])
|
||||||
|
|
||||||
watch(() => props.report, () => {
|
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 = [
|
tabList.value = [
|
||||||
{
|
{
|
||||||
key: '0',
|
key: '0',
|
||||||
name: '总体',
|
name: getTableTypeStr(total),
|
||||||
canHide: false,
|
canHide: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
tableData: props.report?.chatVOS?.[0] || {},
|
tableData: total,
|
||||||
codeStr: getTableCodeRow(props.report?.chatVOS?.[0] || {})
|
codeStr: getTableCodeRow(total)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
name: '性别',
|
name: getTableTypeStr(gender),
|
||||||
canHide: true,
|
canHide: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
tableData: props.report?.chatVOS?.[1] || {},
|
tableData: gender,
|
||||||
codeStr: getTableCodeRow(props.report?.chatVOS?.[1] || {})
|
codeStr: getTableCodeRow(gender)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
name: '年龄段',
|
name: getTableTypeStr(age),
|
||||||
canHide: true,
|
canHide: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
tableData: props.report?.chatVOS?.[2] || {},
|
tableData: age,
|
||||||
codeStr: getTableCodeRow(props.report?.chatVOS?.[2] || {})
|
codeStr: getTableCodeRow(age)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
name: '家庭月收入',
|
name: getTableTypeStr(income),
|
||||||
canHide: true,
|
canHide: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
tableData: props.report?.chatVOS?.[3] || {},
|
tableData: income,
|
||||||
codeStr: getTableCodeRow(props.report?.chatVOS?.[3] || {})
|
codeStr: getTableCodeRow(income)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '4',
|
key: '4',
|
||||||
name: '城市级别',
|
name: getTableTypeStr(city),
|
||||||
canHide: true,
|
canHide: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
tableData: props.report?.chatVOS?.[4] || {},
|
tableData: city,
|
||||||
codeStr: getTableCodeRow(props.report?.chatVOS?.[4] || {})
|
codeStr: getTableCodeRow(city)
|
||||||
}
|
}
|
||||||
]
|
].filter((item) => !!item.tableData)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
|
||||||
|
function getTableTypeStr(tableData) {
|
||||||
|
return tableData?.type?.split('-')?.[1] || ''
|
||||||
|
}
|
||||||
|
|
||||||
function getTableCodeRow(tableData) {
|
function getTableCodeRow(tableData) {
|
||||||
const codeRow = tableData?.dataVOS?.find?.((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
|
if(!tableData?.dataVOS?.length) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const codeRow = tableData.dataVOS.find((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
|
||||||
if(!codeRow) {
|
if(!codeRow) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ const props = defineProps({
|
|||||||
readonly: { type: Boolean, default: false }
|
readonly: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
const tableData = computed(() => [])
|
const tableData = computed(() => props.report?.chatVOS?.find?.((data) => data.type === '其他关键指标-喜欢程度') || {})
|
||||||
const codeStr = computed(() => getTableCodeRow(tableData.value?.chatVOS?.[4] || {}))
|
const codeStr = computed(() => getTableCodeRow(tableData.value))
|
||||||
|
|
||||||
function getTableCodeRow(tableData) {
|
function getTableCodeRow(tableData) {
|
||||||
const codeRow = tableData?.dataVOS?.find?.((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
|
if(!tableData?.dataVOS?.length) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const codeRow = tableData.dataVOS.find((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
|
||||||
if(!codeRow) {
|
if(!codeRow) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -38,6 +41,7 @@ function getTableCodeRow(tableData) {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
padding-top: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #434343;
|
color: #434343;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineProps } from 'vue'
|
import { computed, defineProps, ref } from 'vue'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
import { EyeInvisibleOutlined } from '@ant-design/icons-vue'
|
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
|
||||||
import SectionTitle from '../../components/SectionTitle.vue'
|
import SectionTitle from '../../components/SectionTitle.vue'
|
||||||
@@ -17,14 +17,16 @@ const props = defineProps({
|
|||||||
const createdAt = computed(() => props.report?.createdAt ? moment(props.report.createdAt).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 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 sampleNum = computed(() => props.report?.sampleNum ?? '--')
|
||||||
|
|
||||||
const surveyVersion = computed(() => props.report?.surveyVersion ?? '--')
|
const surveyVersion = computed(() => props.report?.surveyVersion ?? '--')
|
||||||
const reportVersion = computed(() => props.report?.reportVersion ?? '--')
|
const reportVersion = computed(() => props.report?.reportVersion ?? '--')
|
||||||
|
|
||||||
function toggleVisibility() {
|
const visible = ref(false)
|
||||||
|
|
||||||
|
function toggleVisibility() {
|
||||||
|
visible.value = !visible.value
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -32,7 +34,8 @@ function toggleVisibility() {
|
|||||||
<div class="insight-overview">
|
<div class="insight-overview">
|
||||||
<SectionTitle>
|
<SectionTitle>
|
||||||
<span class="text">报告概览</span>
|
<span class="text">报告概览</span>
|
||||||
<EyeInvisibleOutlined class="icon" @click="toggleVisibility" />
|
<EyeOutlined v-if="visible" class="icon" @click="toggleVisibility" />
|
||||||
|
<EyeInvisibleOutlined v-else class="icon" @click.stop="toggleVisibility" />
|
||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
|
|
||||||
<Section class="section">
|
<Section class="section">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps } from 'vue'
|
import { defineProps, ref, watch } from 'vue'
|
||||||
|
|
||||||
import Tinymce from '@/components/Tinymce.vue'
|
import Tinymce from '@/components/Tinymce.vue'
|
||||||
|
|
||||||
@@ -10,6 +10,14 @@ const props = defineProps({
|
|||||||
report: { type: Object, default: () => Object.assign({}) },
|
report: { type: Object, default: () => Object.assign({}) },
|
||||||
readonly: { type: Boolean, default: false }
|
readonly: { type: Boolean, default: false }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const richText = ref('')
|
||||||
|
|
||||||
|
watch(() => props.report.decisionIndicators, (val) => {
|
||||||
|
richText.value = val || ''
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -22,7 +30,8 @@ const props = defineProps({
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="label">决策标准</div>
|
<div class="label">决策标准</div>
|
||||||
<div class="value tinymce-wrapper">
|
<div class="value tinymce-wrapper">
|
||||||
<Tinymce :curtail="false"
|
<Tinymce v-model:editorData="richText"
|
||||||
|
:curtail="false"
|
||||||
:curtail-min-height="140"
|
:curtail-min-height="140"
|
||||||
show
|
show
|
||||||
:open3-d-icon="false"
|
:open3-d-icon="false"
|
||||||
|
|||||||
@@ -14,57 +14,26 @@ const props = defineProps({
|
|||||||
|
|
||||||
const conceptTypeEnum = {
|
const conceptTypeEnum = {
|
||||||
newest: 1,
|
newest: 1,
|
||||||
standard: 2,
|
standard: 0,
|
||||||
|
|
||||||
1: 'newest',
|
1: 'newest',
|
||||||
2: 'standard'
|
0: 'standard'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const list = computed(() => props.data?.list || [
|
const list = computed(() => props.report?.config || [
|
||||||
{
|
// {
|
||||||
id: 1,
|
// id: 41,
|
||||||
name: '零食跨界-屏幕最佳搭档',
|
// concept_name: '极致健康-一站式早餐解决方案',
|
||||||
img: '',
|
// concept_url: 'https://test-cxp-public-web-1302259445.cos.ap-beijing.myqcloud.com/uat-yls/theme/undefined/1724639839680_902_%E6%B0%B4%E5%BA%93.jpg',
|
||||||
type: conceptTypeEnum.newest
|
// concept_type: conceptTypeEnum.newest
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 2,
|
// id: 3,
|
||||||
name: '极致颜值-满杯布灵',
|
// concept_name: '标杆概念-馋酸奶',
|
||||||
img: '',
|
// concept_url: 'https://test-cxp-public-web-1302259445.cos.ap-beijing.myqcloud.com/uat-yls/theme/undefined/1724639839680_902_%E6%B0%B4%E5%BA%93.jpg',
|
||||||
type: conceptTypeEnum.newest
|
// concept_type: conceptTypeEnum.standard
|
||||||
},
|
// },
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: '极致健康-一站式早餐解决方案',
|
|
||||||
img: '',
|
|
||||||
type: conceptTypeEnum.newest
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 11,
|
|
||||||
name: '零食跨界-屏幕最佳搭档',
|
|
||||||
img: '',
|
|
||||||
type: conceptTypeEnum.newest
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 21,
|
|
||||||
name: '极致颜值-满杯布灵',
|
|
||||||
img: '',
|
|
||||||
type: conceptTypeEnum.newest
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 41,
|
|
||||||
name: '极致健康-一站式早餐解决方案',
|
|
||||||
img: '',
|
|
||||||
type: conceptTypeEnum.newest
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: '标杆概念-馋酸奶',
|
|
||||||
img: '',
|
|
||||||
type: conceptTypeEnum.standard
|
|
||||||
},
|
|
||||||
|
|
||||||
])
|
])
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -76,13 +45,13 @@ const list = computed(() => props.data?.list || [
|
|||||||
<div v-for="(item) in list"
|
<div v-for="(item) in list"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="item"
|
class="item"
|
||||||
:class="{[conceptTypeEnum[item.type]]: true}">
|
:class="{[conceptTypeEnum[item.concept_type]]: true}">
|
||||||
|
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<div class="text">{{ item.name || '' }}</div>
|
<div class="text">{{ item.concept_name || '' }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="item.img" :src="item.img" alt="" class="img">
|
<img v-if="item.concept_url" :src="item.concept_url" alt="" class="img">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -103,6 +72,7 @@ const list = computed(() => props.data?.list || [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
overflow: hidden;
|
||||||
flex: none;
|
flex: none;
|
||||||
width: 198px;
|
width: 198px;
|
||||||
height: 152px;
|
height: 152px;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ watch(() => props.report, () => {
|
|||||||
<div class="tab-container">
|
<div class="tab-container">
|
||||||
<PeopleLike />
|
<PeopleLike />
|
||||||
<PeopleDislike />
|
<PeopleDislike />
|
||||||
<ProductImage />
|
<ProductImage :report="props.report"/>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
|||||||
@@ -1,28 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
|
|
||||||
import Section from '../../../components/Section.vue'
|
import PeopleLike from './PeopleLike.vue'
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Section class="section">
|
<PeopleLike title="消费者不喜欢的方面">
|
||||||
<div class="section-header">
|
<template #title-icon>
|
||||||
<img src="../../../img/icon_like.png" alt="" class="icon">
|
<img src="../../../img/icon_dislike.png" alt="" class="icon">
|
||||||
<span>消费者喜欢的方面</span>
|
</template>
|
||||||
</div>
|
</PeopleLike>
|
||||||
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="area-wrapper"></div>
|
|
||||||
<div class="table-wrapper"></div>
|
|
||||||
<div class="table-wrapper"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "./style.scss";
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineProps, ref } from 'vue'
|
import { computed, defineProps, ref } from 'vue'
|
||||||
import { AlignRightOutlined, CloudOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
import { AlignRightOutlined, CloudOutlined, DownloadOutlined } from '@ant-design/icons-vue'
|
||||||
|
import moment from 'moment'
|
||||||
|
|
||||||
import Section from '../../../components/Section.vue'
|
import Section from '../../../components/Section.vue'
|
||||||
import SmallTable from '../../components/SmallTable.vue'
|
import SmallTable from '../../components/SmallTable.vue'
|
||||||
|
|
||||||
|
import { exportJsonToExcel } from '@/composables/exportExcel'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, default: '消费者喜欢的方面' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartShown = ref(true)
|
||||||
|
|
||||||
|
const activeRow = ref({})
|
||||||
|
|
||||||
|
|
||||||
const columns = ref([
|
const columns = ref([
|
||||||
{
|
{
|
||||||
@@ -31,13 +42,30 @@ const tableData = ref([
|
|||||||
{ id: '7', seq: '7', original: 'xxx' }
|
{ id: '7', seq: '7', original: 'xxx' }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
function showChart(show) {
|
||||||
|
chartShown.value = !!show
|
||||||
|
}
|
||||||
|
|
||||||
|
function onActiveRow(record) {
|
||||||
|
activeRow.value = record || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadDetailTable() {
|
||||||
|
exportJsonToExcel({
|
||||||
|
data: [['xxx1', 'xxx111'], ['xxx2', 'xxx2']],
|
||||||
|
filename: `关键词比例_${ props.title }_屏幕最佳搭档_${ moment().format('YYYYMMDDHHmm') }`
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Section class="section">
|
<Section class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<img src="../../../img/icon_like.png" alt="" class="title-icon">
|
<slot name="title-icon">
|
||||||
<span>消费者喜欢的方面</span>
|
<img src="../../../img/icon_like.png" alt="" class="title-icon">
|
||||||
|
</slot>
|
||||||
|
<span class="title-text">{{ props.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
@@ -46,12 +74,6 @@ const tableData = ref([
|
|||||||
<div class="action-button">
|
<div class="action-button">
|
||||||
<DownloadOutlined class="icon" />
|
<DownloadOutlined class="icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="action-button">
|
|
||||||
<CloudOutlined class="icon" />
|
|
||||||
</div>
|
|
||||||
<div class="action-button">
|
|
||||||
<AlignRightOutlined class="icon" style="rotate: 180deg;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -63,29 +85,27 @@ const tableData = ref([
|
|||||||
<div class="action-button">
|
<div class="action-button">
|
||||||
<DownloadOutlined class="icon" />
|
<DownloadOutlined class="icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="action-button">
|
<div class="action-button" @click="showChart(!chartShown)">
|
||||||
<CloudOutlined class="icon" />
|
<CloudOutlined v-if="!chartShown" class="icon" />
|
||||||
</div>
|
<AlignRightOutlined v-else class="icon" style="rotate: 180deg;" />
|
||||||
<div class="action-button">
|
|
||||||
<AlignRightOutlined class="icon" style="rotate: 180deg;" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="chartShown"></div>
|
||||||
|
<SmallTable v-else
|
||||||
|
:data-source="tableData"
|
||||||
|
:columns="columns"
|
||||||
|
:row-selectable="true"
|
||||||
|
@active="onActiveRow" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-divider type="vertical" class="dashed-divider" />
|
<a-divider type="vertical" class="dashed-divider" />
|
||||||
|
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<div class="button-wrapper">
|
<div class="button-wrapper">
|
||||||
<div class="action-button">
|
<div class="action-button" @click="downloadDetailTable">
|
||||||
<DownloadOutlined class="icon" />
|
<DownloadOutlined class="icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="action-button">
|
|
||||||
<CloudOutlined class="icon" />
|
|
||||||
</div>
|
|
||||||
<div class="action-button">
|
|
||||||
<AlignRightOutlined class="icon" style="rotate: 180deg;" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SmallTable :data-source="tableData" :columns="columns" />
|
<SmallTable :data-source="tableData" :columns="columns" />
|
||||||
|
|||||||
@@ -4,16 +4,22 @@ import { computed, defineProps } from 'vue'
|
|||||||
import Section from '../../../components/Section.vue'
|
import Section from '../../../components/Section.vue'
|
||||||
import StyledTable from '../../components/StyledTable.vue'
|
import StyledTable from '../../components/StyledTable.vue'
|
||||||
|
|
||||||
const props = defineProps({})
|
const props = defineProps({
|
||||||
|
report: { type: Object, default: () => Object.assign({}) }
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const codeStr = computed(() => getTableCodeRow())
|
const tableData = computed(() => props.report?.chatVOS?.find?.((data) => data.type === '概念形象') || {})
|
||||||
|
const codeStr = computed(() => getTableCodeRow(tableData.value))
|
||||||
|
|
||||||
|
|
||||||
function getTableCodeRow(tableData) {
|
function getTableCodeRow(tableData) {
|
||||||
const codeRow = tableData?.dataVOS?.find?.((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
|
if(!tableData?.dataVOS?.length) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const codeRow = tableData.dataVOS.find((row) => Object.keys(row).find((key) => row[key] === '概念编码'))
|
||||||
if(!codeRow) {
|
if(!codeRow) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -24,15 +30,17 @@ function getTableCodeRow(tableData) {
|
|||||||
<template>
|
<template>
|
||||||
<Section class="section">
|
<Section class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<img src="../../../img/icon_like.png" alt="" class="title-icon">
|
<img src="../../../img/icon_light.png" alt="" class="title-icon">
|
||||||
<span>消费者喜欢的方面</span>
|
<span class="title-text">概念传递产品形象</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StyledTable></StyledTable>
|
<div class="main-wrapper">
|
||||||
|
<StyledTable :data="tableData" :bar-table="true" />
|
||||||
|
|
||||||
<div class="message">
|
<div class="message">
|
||||||
<span class="emphasize">{{ codeStr }}</span>
|
<span class="emphasize">{{ codeStr }}</span>
|
||||||
<span>意为该数值在90%的显著水平下显著高</span>
|
<span>意为该数值在90%的显著水平下显著高</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Section>
|
</Section>
|
||||||
@@ -41,8 +49,12 @@ function getTableCodeRow(tableData) {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "./style.scss";
|
@import "./style.scss";
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
padding: 10px 16px 16px;
|
padding-top: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #434343;
|
color: #434343;
|
||||||
|
|||||||
@@ -17,7 +17,10 @@
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border: none;
|
border: none;
|
||||||
margin-right: 4px;
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
src/views/Shared/Index.vue
Normal file
11
src/views/Shared/Index.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user