Commit 6a3f72ad authored by 赵乐's avatar 赵乐

首页

parent 8628b9ca
import request from '@/config/axios'
// 客户信息 API
export const homesApi = {
// 查询卡片信息
getHomeInfoFirst: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoFirst`, params })
},
// 竖柱状图数据
getHomeInfoBfztj: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoBfztj`, params })
},
// 折线图数据
getHomeInfoBfrtj: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoBfrtj`, params })
},
// 客户性质等级占比情况
getHomeInfoKhxzdjzbqk: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoKhxzdjzbqk`, params })
},
// 拜访人均分布情况
getHomeInfoBfrjfbqk: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoBfrjfbqk`, params })
},
// 客户拜访方式占比情况
getHomeInfoKhbffszbqk: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoKhbffszbqk`, params })
},
// 客户部门占比情况
getHomeInfoKhbmzbqk: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoKhbmzbqk`, params })
},
// 客户拜访类型占比情况
getHomeInfoKhbflxzbqk: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoKhbflxzbqk`, params })
},
// 拜访产品类型占比情况
getHomeInfoBfcplxzbqk: async (params: any) => {
return await request.get({ url: `/visit/home/getHomeInfoBfcplxzbqk`, params })
},
}
<template>
<div>
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<el-row :gutter="16" justify="space-between">
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex items-center">
<el-avatar :src="avatar" :size="70" class="mr-16px">
<img src="@/assets/imgs/logo.png" alt="" />
</el-avatar>
<div>
<div class="text-20px">
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
</div>
<div class="mt-10px text-14px text-gray-500">
{{ t('workplace.toady') }},20℃ - 32℃!
</div>
</div>
</div>
</el-col>
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="h-70px flex items-center justify-end lt-sm:mt-10px">
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.project"
:duration="2600"
/>
</div>
<el-divider direction="vertical" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.todo"
:duration="2600"
/>
</div>
<el-divider direction="vertical" border-style="dashed" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.access"
:duration="2600"
/>
</div>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</div>
<el-row class="mt-8px" :gutter="8" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span>
<el-link
type="primary"
:underline="false"
href="https://github.com/yudaocode"
target="_blank"
>
{{ t('action.more') }}
</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col
v-for="(item, index) in projects"
:key="`card-${index}`"
:xl="8"
:lg="8"
:md="8"
:sm="24"
:xs="24"
>
<el-card
shadow="hover"
class="mr-5px mt-5px cursor-pointer"
@click="handleProjectClick(item.message)"
>
<div class="flex items-center">
<Icon
:icon="item.icon"
:size="25"
class="mr-8px"
:style="{ color: item.color }"
/>
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.shortcutOperation') }}</span>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-8px" :style="{ color: item.color }" />
<el-link type="default" :underline="false" @click="handleShortcutClick(item.url)">
{{ item.name }}
</el-link>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
<div class="flex items-center">
<el-avatar :src="avatar" :size="35" class="mr-16px">
<img src="@/assets/imgs/logo.png" alt="" />
</el-avatar>
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ item.type }} : {{ item.title }}
</Highlight>
</div>
<div class="mt-16px text-12px text-gray-400">
{{ formatTime(item.date, 'yyyy-MM-dd') }}
</div>
</div>
</div>
<el-divider />
</div>
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { formatTime } from '@/utils'
import { useUserStore } from '@/store/modules/user'
// import { useWatermark } from '@/hooks/web/useWatermark'
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
import { pieOptions, barOptions } from './echarts-data'
import { useRouter } from 'vue-router'
defineOptions({ name: 'Index' })
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
// const { setWatermark } = useWatermark()
const loading = ref(true)
const avatar = userStore.getUser.avatar
const username = userStore.getUser.nickname
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
// 获取统计数
let totalSate = reactive<WorkplaceTotal>({
project: 0,
access: 0,
todo: 0
})
const getCount = async () => {
const data = {
project: 40,
access: 2340,
todo: 10
}
totalSate = Object.assign(totalSate, data)
}
// 获取项目数
let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'ruoyi-vue-pro',
icon: 'simple-icons:springboot',
message: 'github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date('2025-01-02'),
color: '#6DB33F'
},
{
name: 'yudao-ui-admin-vue3',
icon: 'ep:element-plus',
message: 'github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus 管理后台',
time: new Date('2025-02-03'),
color: '#409EFF'
},
{
name: 'yudao-ui-mall-uniapp',
icon: 'icon-park-outline:mall-bag',
message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
personal: 'Vue3 + uniapp 商城手机端',
time: new Date('2025-03-04'),
color: '#ff4d4f'
},
{
name: 'yudao-cloud',
icon: 'material-symbols:cloud-outline',
message: 'github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date('2025-04-05'),
color: '#1890ff'
},
{
name: 'yudao-ui-admin-vben',
icon: 'devicon:antdesign',
message: 'github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben5(antd) 管理后台',
time: new Date('2025-05-06'),
color: '#e18525'
},
{
name: 'yudao-ui-admin-uniapp',
icon: 'ant-design:mobile',
message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp 管理手机端',
time: new Date('2025-06-01'),
color: '#2979ff'
}
]
projects = Object.assign(projects, data)
}
// 获取通知公告
let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '系统支持 JDK 8/17/21,Vue 2/3',
type: '技术兼容性',
keys: ['JDK', 'Vue'],
date: new Date()
},
{
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '架构灵活性',
keys: ['Boot', 'Cloud'],
date: new Date()
},
{
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '开源免授权',
keys: ['无需授权'],
date: new Date()
},
{
title: '国内使用最广泛的快速开发平台,远超 10w+ 企业使用',
type: '广泛企业认可',
keys: ['最广泛', '10w+'],
date: new Date()
}
]
notice = Object.assign(notice, data)
}
// 获取快捷入口
let shortcut = reactive<Shortcut[]>([])
const getShortcut = async () => {
const data = [
{
name: '首页',
icon: 'ion:home-outline',
url: '/',
color: '#1fdaca'
},
{
name: '商城中心',
icon: 'ep:shop',
url: '/mall/home',
color: '#ff6b6b'
},
{
name: 'AI 大模型',
icon: 'tabler:ai',
url: '/ai/chat',
color: '#7c3aed'
},
{
name: 'ERP 系统',
icon: 'simple-icons:erpnext',
url: '/erp/home',
color: '#3fb27f'
},
{
name: 'CRM 系统',
icon: 'simple-icons:civicrm',
url: '/crm/backlog',
color: '#4daf1bc9'
},
{
name: 'IoT 物联网',
icon: 'fa-solid:hdd',
url: '/iot/home',
color: '#1a73e8'
}
]
shortcut = Object.assign(shortcut, data)
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const getAllApi = async () => {
await Promise.all([
getCount(),
getProject(),
getNotice(),
getShortcut(),
getUserAccessSource(),
getWeeklyUserActivity()
])
loading.value = false
}
const handleProjectClick = (message: string) => {
window.open(`https://${message}`, '_blank')
}
const handleShortcutClick = (url: string) => {
router.push(url)
}
getAllApi()
</script>
<template>
<div>
<el-card shadow="never">
<el-skeleton :loading="loading" animated>
<el-row :gutter="16" justify="space-between">
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="flex items-center">
<el-avatar :src="avatar" :size="70" class="mr-16px">
<img src="@/assets/imgs/logo.png" alt="" />
</el-avatar>
<div>
<div class="text-20px">
{{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
</div>
<div class="mt-10px text-14px text-gray-500">
{{ t('workplace.toady') }},20℃ - 32℃!
</div>
</div>
</div>
</el-col>
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
<div class="h-70px flex items-center justify-end lt-sm:mt-10px">
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.project"
:duration="2600"
<div v-loading="loading">
<div class="data-board">
<!-- 标题与提示 -->
<div class="board-header">数据统计-大数据看板</div>
<!-- 筛选区域 -->
<div class="filter-bar">
<!-- 客户关键词搜索 -->
<el-input
v-model="searchKey"
@blur="getEcharts"
placeholder="请输入客户关键词"
clearable
class="filter-item"
>
<template #suffix>
<i class="el-icon-arrow-down" @click="openSearchDropdown"></i>
</template>
</el-input>
<!-- 统计维度筛选 -->
<el-select
v-model="statType"
placeholder="请选择"
class="filter-item"
@change="handleStatChange"
>
<el-option label="请选择" value="" />
<el-option label="年统计" value="year" />
<el-option label="月统计" value="month" />
<el-option label="日统计" value="day" />
</el-select>
<!-- 时间范围选择 -->
<el-date-picker
v-model="timeRange"
:type="pickerType"
range-separator="至"
:start-placeholder="startPlaceholderype"
:end-placeholder="endPlaceholder"
:format="format"
:value-format="valueFormat"
class="filter-item"
@change="getPickerValue"
/>
</div>
<el-divider direction="vertical" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.todo"
:duration="2600"
/>
</div>
<el-divider direction="vertical" border-style="dashed" />
<div class="px-8px text-right">
<div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="totalSate.access"
:duration="2600"
<card ref="cardRef" :homeInfoFirst="homeInfoFirst" />
<div class="page-container">
<div class="top top_1">
<div>
<BarChart
:title="homeInfoBfztjVal"
:xData="homeInfoBfztj?.weeks"
:yData="homeInfoBfztj?.visitCounts"
seriesName="拜访量"
color="#a9d672"
height="350px"
width="500px"
/>
</div>
<div>
<LineChart
:title="homeInfoBfrtjVal"
:xData="homeInfoBfrtj?.dates"
:yData="homeInfoBfrtj?.counts"
seriesName="访问量"
color="#5793f3"
height="350px"
/>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</div>
<el-row class="mt-8px" :gutter="8" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span>
<el-link
type="primary"
:underline="false"
href="https://github.com/yudaocode"
target="_blank"
>
{{ t('action.more') }}
</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col
v-for="(item, index) in projects"
:key="`card-${index}`"
:xl="8"
:lg="8"
:md="8"
:sm="24"
:xs="24"
>
<el-card
shadow="hover"
class="mr-5px mt-5px cursor-pointer"
@click="handleProjectClick(item.message)"
>
<div class="flex items-center">
<Icon
:icon="item.icon"
:size="25"
class="mr-8px"
:style="{ color: item.color }"
<div class="top top_2">
<div>
<PieChart
:data="homeInfoKhxzdjzbqk"
:total="200"
tooltipTip="客户性质等级占比情况"
width="500px"
/>
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.shortcutOperation') }}</span>
<div>
<BarChart_1
:data="homeInfoBfrjfbqk"
title="拜访人均分布情况"
width="600px"
height="400px"
/>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-8px" :style="{ color: item.color }" />
<el-link type="default" :underline="false" @click="handleShortcutClick(item.url)">
{{ item.name }}
</el-link>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
<div class="top top_3">
<div>
<PieChart
:data="homeInfoKhbffszbqk"
:total="200"
tooltipTip="客户拜访方式占比情况"
width="500px"
/>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
<div class="flex items-center">
<el-avatar :src="avatar" :size="35" class="mr-16px">
<img src="@/assets/imgs/logo.png" alt="" />
</el-avatar>
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ item.type }} : {{ item.title }}
</Highlight>
<PieChart
:data="homeInfoKhbmzbqk"
:total="200"
tooltipTip="客户部门占比情况"
width="500px"
/>
</div>
<div class="mt-16px text-12px text-gray-400">
{{ formatTime(item.date, 'yyyy-MM-dd') }}
<div>
<PieChart
:data="homeInfoKhbflxzbqk"
:total="200"
tooltipTip="客户拜访类型占比情况"
width="500px"
/>
</div>
</div>
<BarChart_1
:data="homeInfoBfcplxzbqk"
title="拜访产品类型占比情况"
width="1400px"
height="400px"
/>
</div>
<el-divider />
</div>
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { formatTime } from '@/utils'
import { ref, onMounted } from 'vue'
import card from './card.vue'
// import chartsCard from './chartsCard.vue'
import { homesApi } from '@/api/visit/home'
import BarChart from './components/BarChart.vue'
import BarChart_1 from './components/barChart_1.vue'
import LineChart from './components/LineChart.vue'
import PieChart from './components/PieChart.vue'
import { log } from 'console'
const searchKey = ref('')
const statType = ref('')
const timeRange = ref<[string, string] | []>([]) // 实际存储时间戳或标准格式
const pickerType = ref('daterange')
const startPlaceholderype = ref('开始日期')
const endPlaceholder = ref('结束日期')
const format = ref('YYYY年MM月DD日')
const valueFormat = ref('YYYY-MM-DD')
// const pieData = ref([
// { name: '电话', value: 765, color: '#5793f3' },
// { name: '上门', value: 560, color: '#a9d672' },
// { name: '社交网络', value: 400, color: '#fac858' },
// { name: '推广渠道', value: 200, color: '#f96c6a' },
// { name: '其他', value: 120, color: '#c2a6f2' }
// ])
// const chartData_1 = ref([
// { name: '业务员1', value: 78, color: '#fcd36e' },
// { name: '业务员2', value: 68, color: '#f28b8b' },
// { name: '业务员3', value: 50, color: '#a491f2' },
// { name: '业务员4', value: 42, color: '#fbbc58' },
// { name: '业务员5', value: 32, color: '#7ecb5c' },
// { name: '业务员6', value: 30, color: '#68c0cf' },
// { name: '业务员7', value: 28, color: '#66b1fc' }
// ])
const handleStatChange = (val: string) => {
console.log('切换统计维度:', val)
timeRange.value = []
if (val == 'year') {
pickerType.value = 'yearrange'
startPlaceholderype.value = '开始年份'
endPlaceholder.value = '结束年份'
format.value = 'YYYY年'
valueFormat.value = 'YYYY'
} else if (val == 'month') {
pickerType.value = 'monthrange'
startPlaceholderype.value = '开始月份'
endPlaceholder.value = '结束月份'
format.value = 'YYYY年MM月'
valueFormat.value = 'YYYY-MM'
} else {
pickerType.value = 'daterange'
startPlaceholderype.value = '开始日期'
endPlaceholder.value = '结束日期'
format.value = 'YYYY年MM月DD日'
valueFormat.value = 'YYYY-MM-DD'
}
// 调用接口,根据 statType 和 timeRange 获取数据
getEcharts()
}
const openSearchDropdown = () => {
// 打开下拉菜单(如筛选条件、历史记录等)
console.log('打开搜索下拉')
}
// 获取日期
const getPickerValue = (e: any) => {
console.log(e)
}
// 图表数据
// 加载状态
const loading = ref(false)
// 错误信息
const error = ref('')
import { useUserStore } from '@/store/modules/user'
// import { useWatermark } from '@/hooks/web/useWatermark'
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
import { pieOptions, barOptions } from './echarts-data'
import { useRouter } from 'vue-router'
// 各图表数据存储
const homeInfoFirst: Ref<any | null> = ref(null)
const homeInfoBfztj: Ref<any | null> = ref(null)
const homeInfoBfrtj: Ref<any | null> = ref(null)
const homeInfoKhxzdjzbqk: Ref<any | null> = ref([])
const homeInfoBfrjfbqk: Ref<any | null> = ref([])
const homeInfoKhbffszbqk: Ref<any | null> = ref([])
const homeInfoKhbmzbqk: Ref<any | null> = ref([])
const homeInfoKhbflxzbqk: Ref<any | null> = ref([])
const homeInfoBfcplxzbqk: Ref<any | null> = ref([])
const homeInfoBfztjVal = ref('拜访日统计')
const homeInfoBfrtjVal = ref('拜访日统计')
// 获取图表数据
const getEcharts = async () => {
error.value = ''
let params: any = {
companyName: searchKey.value,
searchTimeString: timeRange.value
}
try {
// 卡片数据
homesApi
.getHomeInfoFirst(params)
.then((res: any) => {
homeInfoFirst.value = res
console.log('卡片数据:', res)
})
.catch((err) => {
console.error('卡片数据获取失败:', err)
error.value = '卡片数据获取失败'
})
defineOptions({ name: 'Index' })
// 竖柱状图数据
homesApi
.getHomeInfoBfztj(params)
.then((res: any) => {
console.log('竖柱状图数据:', res)
homeInfoBfztj.value = res
})
.catch((err) => {
console.error('竖柱状图数据获取失败:', err)
error.value = '竖柱状图数据获取失败'
})
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
// const { setWatermark } = useWatermark()
const loading = ref(true)
const avatar = userStore.getUser.avatar
const username = userStore.getUser.nickname
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
// 获取统计数
let totalSate = reactive<WorkplaceTotal>({
project: 0,
access: 0,
todo: 0
})
// 折线图数据
homesApi
.getHomeInfoBfrtj(params)
.then((res: any) => {
console.log('折线图数据:', res)
homeInfoBfrtj.value = res
})
.catch((err) => {
console.error('折线图数据获取失败:', err)
error.value = '折线图数据获取失败'
})
const getCount = async () => {
const data = {
project: 40,
access: 2340,
todo: 10
}
totalSate = Object.assign(totalSate, data)
}
// 客户性质等级占比情况
homesApi
.getHomeInfoKhxzdjzbqk(params)
.then((res: any) => {
homeInfoKhxzdjzbqk.value = res.list
console.log('客户性质等级占比:', res.list, homeInfoKhxzdjzbqk)
})
.catch((err) => {
console.error('客户性质等级占比获取失败:', err)
error.value = '客户性质等级占比获取失败'
})
// 获取项目数
let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'ruoyi-vue-pro',
icon: 'simple-icons:springboot',
message: 'github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date('2025-01-02'),
color: '#6DB33F'
},
{
name: 'yudao-ui-admin-vue3',
icon: 'ep:element-plus',
message: 'github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus 管理后台',
time: new Date('2025-02-03'),
color: '#409EFF'
},
{
name: 'yudao-ui-mall-uniapp',
icon: 'icon-park-outline:mall-bag',
message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
personal: 'Vue3 + uniapp 商城手机端',
time: new Date('2025-03-04'),
color: '#ff4d4f'
},
{
name: 'yudao-cloud',
icon: 'material-symbols:cloud-outline',
message: 'github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date('2025-04-05'),
color: '#1890ff'
},
{
name: 'yudao-ui-admin-vben',
icon: 'devicon:antdesign',
message: 'github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben5(antd) 管理后台',
time: new Date('2025-05-06'),
color: '#e18525'
},
{
name: 'yudao-ui-admin-uniapp',
icon: 'ant-design:mobile',
message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp 管理手机端',
time: new Date('2025-06-01'),
color: '#2979ff'
}
]
projects = Object.assign(projects, data)
}
// 拜访人均分布情况
homesApi
.getHomeInfoBfrjfbqk(params)
.then((res: any) => {
console.log('拜访人均分布:', res)
homeInfoBfrjfbqk.value = res.list
})
.catch((err) => {
console.error('拜访人均分布获取失败:', err)
error.value = '拜访人均分布获取失败'
})
// 获取通知公告
let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '系统支持 JDK 8/17/21,Vue 2/3',
type: '技术兼容性',
keys: ['JDK', 'Vue'],
date: new Date()
},
{
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '架构灵活性',
keys: ['Boot', 'Cloud'],
date: new Date()
},
{
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '开源免授权',
keys: ['无需授权'],
date: new Date()
},
{
title: '国内使用最广泛的快速开发平台,远超 10w+ 企业使用',
type: '广泛企业认可',
keys: ['最广泛', '10w+'],
date: new Date()
}
]
notice = Object.assign(notice, data)
}
// 客户拜访方式占比情况
homesApi
.getHomeInfoKhbffszbqk(params)
.then((res: any) => {
console.log('客户拜访方式占比:', res)
homeInfoKhbffszbqk.value = res.list
})
.catch((err) => {
console.error('客户拜访方式占比获取失败:', err)
error.value = '客户拜访方式占比获取失败'
})
// 客户部门占比情况
homesApi
.getHomeInfoKhbmzbqk(params)
.then((res: any) => {
console.log('客户部门占比:', res)
homeInfoKhbmzbqk.value = res.list
})
.catch((err) => {
console.error('客户部门占比获取失败:', err)
error.value = '客户部门占比获取失败'
})
// 获取快捷入口
let shortcut = reactive<Shortcut[]>([])
// 客户拜访类型占比情况
homesApi
.getHomeInfoKhbflxzbqk(params)
.then((res: any) => {
console.log('客户拜访类型占比:', res)
homeInfoKhbflxzbqk.value = res.list
})
.catch((err) => {
console.error('客户拜访类型占比获取失败:', err)
error.value = '客户拜访类型占比获取失败'
})
// 拜访产品类型占比情况
homesApi
.getHomeInfoBfcplxzbqk(params)
.then((res: any) => {
console.log('拜访产品类型占比:', res)
homeInfoBfcplxzbqk.value = res.list
})
.catch((err) => {
console.error('拜访产品类型占比获取失败:', err)
error.value = '拜访产品类型占比获取失败'
})
const getShortcut = async () => {
const data = [
{
name: '首页',
icon: 'ion:home-outline',
url: '/',
color: '#1fdaca'
},
{
name: '商城中心',
icon: 'ep:shop',
url: '/mall/home',
color: '#ff6b6b'
},
{
name: 'AI 大模型',
icon: 'tabler:ai',
url: '/ai/chat',
color: '#7c3aed'
},
{
name: 'ERP 系统',
icon: 'simple-icons:erpnext',
url: '/erp/home',
color: '#3fb27f'
},
{
name: 'CRM 系统',
icon: 'simple-icons:civicrm',
url: '/crm/backlog',
color: '#4daf1bc9'
},
{
name: 'IoT 物联网',
icon: 'fa-solid:hdd',
url: '/iot/home',
color: '#1a73e8'
loading.value = true
} catch (err) {
console.error('数据获取出错:', err)
error.value = '数据获取出错,请重试'
} finally {
loading.value = false
}
]
shortcut = Object.assign(shortcut, data)
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = data.map((v) => {
return {
name: t(v.name),
value: v.value
onMounted(() => {
getEcharts()
})
watch(timeRange, (newVal: any) => {
getEcharts()
})
</script>
<style scoped lang="less">
/* 自定义 Element Plus 组件样式 */
.filter-item {
width: 200px;
}
.data-board {
display: flex;
.board-header {
margin-right: 200px;
}
})
.filter-bar {
display: flex;
align-items: center;
// justify-content: space-around;
width: 70%;
.el-date-editor {
margin-left: 30px;
flex: none;
}
}
}
.page-container {
padding: 20px;
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
h3 {
margin-top: 30px;
}
.top {
display: flex;
> div {
padding: 20px;
margin: 10px;
border: 1px dashed #999;
}
])
}
const getAllApi = async () => {
await Promise.all([
getCount(),
getProject(),
getNotice(),
getShortcut(),
getUserAccessSource(),
getWeeklyUserActivity()
])
loading.value = false
.top_1 {
> div {
flex: 1;
}
}
const handleProjectClick = (message: string) => {
window.open(`https://${message}`, '_blank')
.top_2 {
> div {
flex: 1;
}
}
const handleShortcutClick = (url: string) => {
router.push(url)
.top_3 {
justify-content: space-between;
flex-wrap: wrap;
> div {
width: 32%;
}
}
getAllApi()
</script>
</style>
\ No newline at end of file
<template>
<div class="year-range-picker">
<el-input
v-model="displayValue"
readonly
clearable
@click="toggleYearPanel"
@clear="clearRange"
placeholder="选择年份范围"
class="year-input"
>
<template #suffix>
<i class="el-icon-arrow-down" :class="{ 'is-reverse': panelVisible }"></i>
</template>
</el-input>
<transition name="fade">
<div class="year-panel" v-show="panelVisible">
<div class="panel-header">
<div class="year-type">
<el-radio-group v-model="selectedType">
<el-radio-button label="start">开始年份</el-radio-button>
<el-radio-button label="end">结束年份</el-radio-button>
</el-radio-group>
</div>
<div class="panel-actions">
<el-button size="mini" @click="closePanel">取消</el-button>
<el-button size="mini" type="primary" @click="confirmRange" :disabled="!isValidRange"
>确定</el-button
>
</div>
</div>
<div class="year-grid">
<div
v-for="year in yearList"
:key="year"
:class="{
'year-item': true,
'is-selected': isSelectedYear(year),
'is-disabled': isDisabledYear(year)
}"
@click="selectYear(year)"
>
{{ year }}年
</div>
</div>
<div class="quick-options">
<el-button size="small" @click="selectLastYear">近1年</el-button>
<el-button size="small" @click="selectLast3Years">近3年</el-button>
<el-button size="small" @click="selectLast5Years">近5年</el-button>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, defineProps, defineEmits, nextTick } from 'vue'
import { ElInput, ElRadioGroup, ElRadioButton, ElButton } from 'element-plus'
const props = defineProps<{
modelValue: [string, string] | []
showQuickOptions: boolean
yearRange: [number, number] // 可选年份范围 [开始年份, 结束年份]
}>()
const emits = defineEmits<{
(event: 'update:modelValue', value: [string, string]): void
(event: 'change', value: [string, string]): void
}>()
// 内部状态
const panelVisible = ref(false)
const selectedType = ref('start') // 当前选择的是开始还是结束年份
const startYear = ref<string | null>(props.modelValue[0] || null)
const endYear = ref<string | null>(props.modelValue[1] || null)
// 生成年份列表(默认可选范围:当前年份前后10年)
const yearRange = computed(() => {
const [min, max] = props.yearRange || []
const currentYear = new Date().getFullYear()
const defaultMin = currentYear - 10
const defaultMax = currentYear + 10
return [min || defaultMin, max || defaultMax]
})
const yearList = computed(() => {
const [min, max] = yearRange.value
return Array.from({ length: max - min + 1 }, (_, i) => min + i)
})
// 显示值
const displayValue = computed(() => {
if (startYear.value && endYear.value) {
return `${startYear.value}年 - ${endYear.value}年`
} else if (startYear.value) {
return `${startYear.value}年 - 未选择结束年份`
} else if (endYear.value) {
return `未选择开始年份 - ${endYear.value}年`
}
return ''
})
// 判断年份是否已选择
const isSelectedYear = (year: number) => {
if (selectedType.value === 'start') {
return year.toString() === startYear.value
} else {
return year.toString() === endYear.value
}
}
// 判断年份是否禁用
const isDisabledYear = (year: number) => {
if (selectedType.value === 'start' && endYear.value) {
return year > parseInt(endYear.value)
} else if (selectedType.value === 'end' && startYear.value) {
return year < parseInt(startYear.value)
}
return false
}
// 判断范围是否有效
const isValidRange = computed(() => {
return !!(startYear.value && endYear.value && startYear.value <= endYear.value)
})
// 监听外部值变化
watch(
() => props.modelValue,
(newVal) => {
startYear.value = newVal[0] || null
endYear.value = newVal[1] || null
}
)
// 监听内部值变化,同步到外部
watch([startYear, endYear], ([newStart, newEnd]) => {
if (newStart && newEnd && newStart <= newEnd) {
emits('update:modelValue', [newStart, newEnd])
}
})
// 切换年份面板显示
const toggleYearPanel = () => {
panelVisible.value = !panelVisible.value
}
// 关闭面板
const closePanel = () => {
panelVisible.value = false
}
// 确认选择
const confirmRange = () => {
if (isValidRange.value) {
emits('update:modelValue', [startYear.value!, endYear.value!])
emits('change', [startYear.value!, endYear.value!])
panelVisible.value = false
}
}
// 选择年份
const selectYear = (year: number) => {
if (selectedType.value === 'start') {
startYear.value = year.toString()
// 如果开始年份大于结束年份,清空结束年份
if (endYear.value && startYear.value > endYear.value) {
endYear.value = null
}
} else {
endYear.value = year.toString()
}
}
// 清空选择
const clearRange = () => {
startYear.value = null
endYear.value = null
emits('update:modelValue', [])
}
// 快捷选择:近1年
const selectLastYear = () => {
const currentYear = new Date().getFullYear().toString()
const lastYear = (new Date().getFullYear() - 1).toString()
startYear.value = lastYear
endYear.value = currentYear
confirmRange()
}
// 快捷选择:近3年
const selectLast3Years = () => {
const currentYear = new Date().getFullYear().toString()
const threeYearsAgo = (new Date().getFullYear() - 3).toString()
startYear.value = threeYearsAgo
endYear.value = currentYear
confirmRange()
}
// 快捷选择:近5年
const selectLast5Years = () => {
const currentYear = new Date().getFullYear().toString()
const fiveYearsAgo = (new Date().getFullYear() - 5).toString()
startYear.value = fiveYearsAgo
endYear.value = currentYear
confirmRange()
}
// 点击外部关闭面板
const clickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
const pickerEl = document.querySelector('.year-range-picker') as HTMLElement
if (pickerEl && !pickerEl.contains(target)) {
panelVisible.value = false
}
}
// 添加点击外部事件监听
onMounted(() => {
document.addEventListener('click', clickOutside)
})
// 移除事件监听
onUnmounted(() => {
document.removeEventListener('click', clickOutside)
})
</script>
<style scoped lang="less">
.year-range-picker {
position: relative;
.year-input {
cursor: pointer;
}
.year-panel {
position: absolute;
top: 40px;
left: 0;
width: 300px;
background: #000;
border: 1px solid #dcdcdc;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding: 10px;
.panel-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
.year-type {
.el-radio-button__inner {
padding: 6px 15px;
}
}
.panel-actions {
display: flex;
gap: 10px;
}
}
.year-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 15px;
.year-item {
padding: 8px;
text-align: center;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover:not(.is-disabled) {
background-color: #f5f7fa;
}
&.is-selected {
background-color: #409eff;
color: #fff;
}
&.is-disabled {
color: #c0c4cc;
cursor: not-allowed;
}
}
}
.quick-options {
display: flex;
gap: 10px;
margin-top: 10px;
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
}
</style>
\ No newline at end of file
<template>
<div class="stats-container">
<div class="stat-card">
<div class="stat-item single">
<span class="stat-number">{{ props.homeInfoFirst?.bfkhsl || 0 }}</span>
<span class="stat-label">拜访客户数量</span>
</div>
</div>
<div class="stat-card">
<div class="stat-item">
<span class="stat-number">{{ props.homeInfoFirst?.sykhsl || 0 }}</span>
<span class="stat-label">商业客户</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ props.homeInfoFirst?.ylkhsl || 0 }}</span>
<span class="stat-label">医疗客户数量</span>
</div>
</div>
<div class="stat-card">
<div class="stat-item single">
<span class="stat-number">{{ props.homeInfoFirst?.visitCount || 0 }}</span>
<span class="stat-label">客户拜访总次数</span>
</div>
</div>
<div class="stat-card">
<div class="stat-item">
<span class="stat-number">{{ props.homeInfoFirst?.sykfcs || 0 }}</span>
<span class="stat-label">商业拜访次数</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ props.homeInfoFirst?.ylkfcs || 0 }}</span>
<span class="stat-label">日常拜访次数</span>
</div>
</div>
<div class="stat-card">
<div class="stat-item single">
<span class="stat-number">{{ props.homeInfoFirst?.ywysl || 0 }}</span>
<span class="stat-label">业务员数量</span>
</div>
</div>
<!-- <div class="stat-card">
<div class="stat-item single">
<span class="stat-number">{{ statsData.card2.number }}</span>
<span class="stat-label">{{ statsData.card2.label }}</span>
</div>
</div> -->
</div>
</template>
<script setup lang="ts">
const props = defineProps({
homeInfoFirst: {
type: [Object, String],
// eslint-disable-next-line vue/require-valid-default-prop
default: {}
}
})
onMounted(() => {})
</script>
<style scoped lang="css">
.stats-container {
display: flex;
justify-content: space-around;
padding: 20px 100px;
gap: 20px; /* 卡片之间的间距 */
flex-wrap: wrap; /* 适配小屏幕,自动换行 */
}
.stat-card {
border: 1px solid #ccc;
border-radius: 4px;
padding: 16px;
min-width: 200px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.stat-item {
text-align: center;
margin: 0 10px;
}
.stat-item.single {
margin: 0;
}
.stat-number {
font-size: 24px;
font-weight: bold;
display: block;
color: #000;
}
.stat-label {
font-size: 14px;
color: #666;
}
</style>
\ No newline at end of file
<template>
<div class="bar-chart">
<h3 class="chart-title">{{ title }}</h3>
<chartsCard
:option="chartOption"
:width="width"
:height="height"
@chart-ready="handleChartReady"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import chartsCard from './chartsCard.vue'
const props = defineProps({
title: {
type: String,
default: '柱状图'
},
xData: {
type: Array,
required: true // X轴数据(通常是类别)
},
yData: {
type: Array,
required: true // Y轴数据(数值)
},
seriesName: {
type: String,
default: '数据' // 系列名称
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '400px'
},
color: {
type: String,
default: '#5793f3' // 柱子颜色
},
barWidth: {
type: [String, Number],
default: '40%' // 柱子宽度
}
})
const chartOption = computed(() => ({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: { color: '#333' },
formatter: (params: any) => {
const p = params[0]
return `${p.name}<br/>${p.seriesName}: ${p.value}`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.xData,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#999'
}
},
axisLabel: {
color: '#666',
interval: 0, // 强制显示所有标签
rotate: 45 // 标签旋转角度
}
},
yAxis: {
type: 'value',
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#eee'
}
}
},
series: [
{
name: props.seriesName,
type: 'bar',
data: props.yData,
barWidth: props.barWidth,
itemStyle: {
color: props.color,
borderRadius: [4, 4, 0, 0] // 柱子圆角
},
emphasis: {
itemStyle: {
color: `${props.color}DD` // 高亮颜色
}
}
}
]
}))
const handleChartReady = (chart: any) => {
// 可添加交互逻辑
}
</script>
<style scoped lang="less">
.bar-chart {
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
}
</style>
\ No newline at end of file
<template>
<div class="line-chart">
<h3 class="chart-title">{{ title }}</h3>
<chartsCard
:option="chartOption"
:width="width"
:height="height"
@chart-ready="handleChartReady"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import chartsCard from './chartsCard.vue'
const props = defineProps({
title: {
type: String,
default: '折线图'
},
xData: {
type: Array,
required: true // X轴数据(通常是时间或类别)
},
yData: {
type: Array,
required: true // Y轴数据(数值)
},
seriesName: {
type: String,
default: '数据' // 系列名称
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '400px'
},
color: {
type: String,
default: '#5793f3' // 线条颜色
},
isSmooth: {
type: Boolean,
default: true // 是否平滑曲线
}
})
const chartOption = computed(() => ({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: { color: '#333' },
formatter: (params: any) => {
const p = params[0]
return `${p.name}<br/>${p.seriesName}: ${p.value}`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: props.xData,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#999'
}
},
axisLabel: {
color: '#666'
}
},
yAxis: {
type: 'value',
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#eee'
}
}
},
series: [
{
name: props.seriesName,
type: 'line',
data: props.yData,
smooth: props.isSmooth,
lineStyle: {
width: 2,
color: props.color
},
itemStyle: {
color: props.color,
borderWidth: 2
},
symbol: 'circle', // 标记点形状
symbolSize: 6, // 标记点大小
showSymbol: false, // 默认不显示标记点,鼠标悬停时显示
emphasis: {
symbol: 'circle',
symbolSize: 8
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: `${props.color}80` }, // 半透明
{ offset: 1, color: `${props.color}10` } // 几乎透明
]
}
}
}
]
}))
const handleChartReady = (chart: any) => {
// 可添加交互逻辑
}
</script>
<style scoped lang="less">
.line-chart {
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
}
</style>
\ No newline at end of file
<template>
<div class="visit-way-pie-chart">
<h3 class="chart-title">客户拜访方式占比情况</h3>
<chartsCard
:option="chartOption"
:width="width"
:height="height"
@chart-ready="handleChartReady"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import chartsCard from './chartsCard.vue'
const props = defineProps({
data: {
type: Array,
required: true
// 数据格式示例:
// [
// { name: '电话', value: 765, color: '#5793f3' },
// { name: '上门', value: 560, color: '#a9d672' },
// { name: '社交网络', value: 400, color: '#fac858' },
// { name: '推广渠道', value: 200, color: '#f96c6a' },
// { name: '其他', value: 120, color: '#c2a6f2' },
// ]
},
total: {
type: Number,
required: true // 总拜访量
},
width: {
type: String,
default: '400px'
},
height: {
type: String,
default: '400px'
},
tooltipTip: {
type: String,
default: '拜访方式支持自定义类型添加' // 提示框内容
}
})
const chartOption = computed(() => {
// console.log(props.data,"props.data");
const legendData = props.data.map((item) => item.name)
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
// 自定义提示框浮层样式
extraCssText: 'background: #fff9cc; border: 1px solid #ffe57f; padding: 8px;',
// 提示框位置
position: (point) => {
const x = point[0] > 300 ? point[0] - 100 : point[0]
const y = point[1] < 100 ? point[1] + 20 : point[1]
return [x, y]
},
// 提示框内容追加说明
formatter: (params) => {
const baseInfo = `${params.seriesName} <br/>${params.marker} ${params.name} : ${params.value} (${params.percent}%)`
return `${baseInfo} <br/><span style="color: #999; font-size: 12px;">${props.tooltipTip}</span>`
}
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
align: 'left',
icon: 'circle', // 图例标记为圆形
itemWidth: 12,
itemHeight: 12,
itemGap: 15,
textStyle: {
color: '#333',
fontSize: 14
},
data: legendData
},
series: [
{
name: '客户拜访方式占比情况',
type: 'pie',
radius: ['40%', '70%'], // 环形图内外半径
center: ['35%', '50%'], // 图表中心位置,留出右侧图例空间
label: {
show: false // 隐藏饼图扇区默认标签
},
labelLine: {
show: false // 隐藏标签连线
},
data: props.data.map((item) => ({
name: item.name,
value: item.value,
itemStyle: {
color: item.color
}
})),
// 中心文字配置
emphasis: {
label: {
show: false
}
},
// 中心显示总拜访量
renderItem: (params, api) => {
if (params.name === '') {
return {
type: 'text',
position: api.coord([api.value(0), api.value(1)]),
content: `总拜访量\n${props.total}`,
style: {
textAlign: 'center',
fill: '#333',
fontSize: 16,
fontWeight: 'bold',
lineHeight: 1.4
}
}
}
return null
}
}
]
}
})
const handleChartReady = (chart: echarts.ECharts) => {
// 可在此处对图表进行更多自定义操作,如监听事件等
}
</script>
<style scoped lang="less">
.visit-way-pie-chart {
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: #333;
}
}
</style>
\ No newline at end of file
<template>
<div class="horizontal-bar-chart">
<h3 class="chart-title">{{ title }}</h3>
<chartsCard
:option="chartOption"
:width="width"
:height="height"
@chart-ready="handleChartReady"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import chartsCard from './chartsCard.vue'
const props = defineProps({
title: {
type: String,
default: '拜访人均分布情况'
},
data: {
type: Array,
required: true
// 数据格式示例:
// [
// { name: '业务员1', value: 78, color: '#fcd36e' },
// { name: '业务员2', value: 68, color: '#f28b8b' },
// { name: '业务员3', value: 50, color: '#a491f2' },
// { name: '业务员4', value: 42, color: '#fbbc58' },
// { name: '业务员5', value: 32, color: '#7ecb5c' },
// { name: '业务员6', value: 30, color: '#68c0cf' },
// { name: '业务员7', value: 28, color: '#66b1fc' },
// ]
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '400px'
}
})
const colors = ['#fcd36e', '#f28b8b', '#a491f2', '#fbbc58', '#7ecb5c', '#68c0cf', '#66b1fc']
const chartOption = computed(() => ({
tooltip: {
trigger: 'item',
formatter: '{b} : {c}' // 格式化提示内容,显示名称和数值
},
grid: {
left: '60px', // 左侧留出空间显示业务员名称
right: '40px',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'value',
// min: 0, // 最小值
// max: 100000, // 最大值
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#eee'
}
}
},
yAxis: {
type: 'category',
data: props.data.map((item) => item.name),
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
color: '#333',
fontSize: 14
}
},
series: [
{
name: '拜访人均分布',
type: 'bar',
barCategoryGap: '10%', // 柱子之间的间距
barWidth: 20, // 柱子宽度
data: props.data.map((item) => item.value),
itemStyle: {
color: (params: { dataIndex: number }) => {
const item = props.data[params.dataIndex]
return item.color || colors[params.dataIndex % colors.length]
},
borderRadius: [0, 6, 6, 0] // 仅右侧圆角,适配横向柱状图
},
label: {
show: true,
position: 'right', // 数值显示在柱子右侧
color: '#333',
fontSize: 12
}
}
]
}))
const handleChartReady = (chart: any) => {
// 可在此处对图表进行更多自定义操作,如监听事件等
}
</script>
<style scoped lang="less">
.horizontal-bar-chart {
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
color: '#333';
}
}
</style>
\ No newline at end of file
<template>
<div ref="chartRef" class="echarts-base" :style="{ width, height }"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
const props = defineProps({
option: {
type: Object,
required: true
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '400px'
}
})
const emits = defineEmits(['chartReady', 'chartClick'])
const chartRef = ref<HTMLElement | null>(null)
let myChart: echarts.ECharts | null = null
const initChart = () => {
nextTick(() => {
if (chartRef.value) {
myChart = echarts.init(chartRef.value)
myChart.setOption(props.option)
emits('chartReady', myChart)
myChart.on('click', (params) => {
emits('chartClick', params)
})
}
})
}
const resizeChart = () => {
myChart?.resize()
}
onMounted(() => {
initChart()
window.addEventListener('resize', resizeChart)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeChart)
myChart?.dispose()
myChart = null
})
watch(
() => props.option,
() => {
if (myChart) {
myChart.setOption(props.option)
}
},
{ deep: true }
)
</script>
<style scoped>
.echarts-base {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment