Files
tkNewAdmin/src/views/Home/Index.vue
2025-12-10 16:31:22 +08:00

797 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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/avatar.gif" 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">
<!-- <div style="display: flex; width: 50%;"
v-if="wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020)">
<el-select v-model="allocationUser" :placeholder="t('newHosts.placeAllocationUser')" clearable
@change="getUserAccessSource(allocationUser)">
<el-option v-for="(user, index) in allocationUserList" :key="index" :label="user.label"
:value="user.value" />
</el-select>
</div> -->
<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
v-if="wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020)"
:xl="20" :lg="20" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-select style="width: 200px;" v-model="days" @change="updataDays">
<el-option :value="1" label="当日">当日</el-option>
<el-option :value="7" label="近7日">近7日</el-option>
</el-select>
<el-skeleton :loading="loading" animated>
<Echart style="width: 100%;" :options="barOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col v-if="tenantLevel != 1" :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<div>当日建联主播数量</div>
{{ HostsOperationNum }}
</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
v-if="wsCache.get('roleRouters').find(item => item.id === 5041)?.children.find(item => item.id === 5042)"
:xl="20" :lg="20" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-select style="width: 200px;" v-model="daysDsec" @change="updataDaysDsec">
<el-option :value="1" label="当日">当日</el-option>
<el-option :value="7" label="近7日">近7日</el-option>
</el-select>
<el-skeleton :loading="loading" animated>
<Echart style="width: 100%;" :options="barOptionsDataDsec" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col v-if="tenantLevel != 1" :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<div>当日建联大哥数量</div>
{{ HostsOperationNumDsec }}
</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">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<!-- <el-button @click="test">test</el-button> -->
<!-- <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/avatar.gif" 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, bigbrotherbarOptions } from './echarts-data'
import { useRouter } from 'vue-router'
import { getComplete, getEmployeeComplete } from '@/api/system/user'
import { useCache } from '@/hooks/web/useCache'
import { getSimpleUserListPage, getSimpleUserList } from '@/api/system/user'
import { EmployeeHostsApi } from '@/api/server/employeehosts'
import { ref, reactive, onMounted, onActivated } from 'vue'
import WxMusic from '../mp/components/wx-music'
import * as TenantApi from '@/api/system/tenant'
let HostsOperationNum = ref(0)//当日建联主播数量
let HostsOperationNumDsec = ref(0)//当日建联大哥数量
let days = ref(1)//当日
let daysDsec = ref(1)//当日
let tenantLevel = ref(null)//租户等级
let tenantType = ref(null)//租户类型
//初始
onMounted(async () => {
// console.log("菜单", wsCache.get('roleRouters'))
//判断菜单有没有钱包 是用户还是代理总后台
tenantType.value = (wsCache.get('roleRouters').find(item => item.id === 1)?.children.find(item => item.id === 1224)?.children.find(item => item.id === 1224))
//有钱包就是代理或者总后台 有级别
if (tenantType.value) {
await TenantApi.getSelfTenantLevel().then(res => {
tenantLevel.value = res.tenantLevel
console.log(res.tenantLevel)
})
}
if (tenantLevel.value == 1) {
loading.value = false
return
}
console.log(1321231)
await getAllApi()
await getAllocationList()
//判断菜单有没有爬大哥管理中的管理员权限
if (wsCache.get('roleRouters').find(item => item.id === 5041)?.children.find(item => item.id === 5042)) {
await fetchAllHostsCountDesc(1)
await fetchDailyHostsCountDesc()
}
//判断菜单有没有主播管理中的管理员权限
if (wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020)) {
await fetchAllHostsCount(1)
await fetchDailyHostsCount()
}
})
// 每次页面“再次显示”时都会触发(前提:该路由组件被 keep-alive 缓存)
onActivated(async () => {
if (tenantType.value) {
await TenantApi.getSelfTenantLevel().then(res => {
tenantLevel.value = res.tenantLevel
console.log(res.tenantLevel)
})
}
if (tenantLevel.value == 1) {
loading.value = false
return
}
//判断菜单有没有爬大哥管理中的管理员权限
if (wsCache.get('roleRouters').find(item => item.id === 5041)?.children.find(item => item.id === 5042)) {
await fetchAllHostsCountDesc(1)
await fetchDailyHostsCountDesc()
}
//判断菜单有没有主播管理中的管理员权限
if (wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020)) {
await fetchAllHostsCount(1)
await fetchDailyHostsCount()
}
})
// 天数切换
async function updataDays(val) {
await fetchAllHostsCount(val)
}
//大哥天数切换
async function updataDaysDsec(val: any) {
await fetchAllHostsCountDesc(val)
}
// import { useGlobalWebSocket } from '@/components/useGlobalWebSocket'
// let messageList = useGlobalWebSocket().messageList
// console.log(messageList.value)
const { wsCache } = useCache()
let allocationUser = ref('') //选中的分配用户
let allocationUserList = ref([
{
label: '分配用户1',
value: 1
}
]) //选中的分配用户
// function test() {
// console.log(messageList.value)
// }
defineOptions({ name: 'Index' })
// console.log(data)
// const { open } = useGlobalWebSocket()
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: 'YOLO助手',
icon: 'simple-icons:springboot',
message: 'www.yolozs.com/',
personal: '全球tiktok经纪人必备高效管理神器!',
time: new Date('2025-01-02'),
color: '#6DB33F'
},
// {
// name: '找大哥助手',
// icon: 'ep:element-plus',
// message: 'www.yolozs.com/',
// personal: '全球tiktok经纪人必备高效管理神器!',
// 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/21Vue 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 (id) => {
const res = id ? await getEmployeeComplete(id) : await getComplete()
const completeData = res ?? { unfinishedNum: 0, finishedNum: 0 }
console.log('completeData', completeData)
const data = [
{ value: completeData.unfinishedNum, name: 'analysis.noAlliance' },
{ value: completeData.finishedNum, name: 'analysis.alreadyJianlian' },
// { 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 barOptionsDataDsec = reactive<EChartsOption>(bigbrotherbarOptions) as EChartsOption
type EmpBarItem = {
userId: number
finishedNum: number | null
totalNum: number | null
unfinishedNum: number | null
}
// 传入希望展示的用户 id 列表 + 后端原始返回,补齐未返回的用户为 0
function mergeZeroUsers(ids: number[], resList: EmpBarItem[]): EmpBarItem[] {
const map = new Map<number, EmpBarItem>(resList.map(it => [it.userId, it]))
return ids.map(id => {
const it = map.get(id)
const finished = it?.finishedNum ?? 0
// 未建联:优先用后端给的 unfinishedNum若没有且提供了 totalNum则用 totalNum - finished否则 0
const total = it?.totalNum ?? 0
const unfinished = it?.unfinishedNum ?? (it?.totalNum != null && it?.finishedNum != null
? Math.max(Number(total) - Number(finished), 0)
: 0)
return {
userId: id,
finishedNum: Number(finished),
totalNum: Number(total),
unfinishedNum: Number(unfinished)
}
})
}
// 把后端返回的数据渲染到柱状图
function updateEmployeeBarChart(list: EmpBarItem[]) {
// 建立 id -> 昵称 的映射
const id2label = new Map(allocationUserList.value.map(u => [u.value, u.label]))
const labels = list.map(it => id2label.get(it.userId) || String(it.userId))
const finished = list.map(it => it.finishedNum ?? 0)
const unfinished = list.map(it => it.unfinishedNum ?? 0)
// legend
const legendNames = []
set(barOptionsData, 'legend.data', legendNames)
set(barOptionsData, 'xAxis.data', labels)
// 1) 轴标签强制全部显示 + 不自动隐藏
set(barOptionsData, 'xAxis.axisLabel.interval', 0)
set(barOptionsData, 'xAxis.axisLabel.hideOverlap', false)
// 2) 适当旋转,避免重叠(不想旋转可设为 0
set(barOptionsData, 'xAxis.axisLabel.rotate', 30)
// 3) 防止被裁剪:让 grid 预留标签空间
set(barOptionsData, 'grid.containLabel', true)
set(barOptionsData, 'grid.bottom', 60) // 视情况调大/调小
set(barOptionsData, 'xAxis.axisTick.alignWithLabel', true)
set(barOptionsData, 'series', [
{
name: t('analysis.alreadyJianlian'),
type: 'bar',
data: finished
},
// {
// name: t('analysis.noAlliance'),
// type: 'bar',
// data: unfinished
// }
])
}
// 周活跃量 大哥图表数据
function updateEmployeeBarChartDesc(lists: EmpBarItem[]) {
// 建立 id -> 昵称 的映射
const id2label = new Map(allocationUserList.value.map(u => [u.value, u.label]))
const labels = lists.map(it => id2label.get(it.userId) || String(it.userId))
const finished = lists.map(it => it.finishedNum ?? 0)
const unfinished = lists.map(it => it.unfinishedNum ?? 0)
// legend
const legendNames = []
set(barOptionsDataDsec, 'legend.data', legendNames)
set(barOptionsDataDsec, 'xAxis.data', labels)
// 1) 轴标签强制全部显示 + 不自动隐藏
set(barOptionsDataDsec, 'xAxis.axisLabel.interval', 0)
set(barOptionsDataDsec, 'xAxis.axisLabel.hideOverlap', false)
// 2) 适当旋转,避免重叠(不想旋转可设为 0
set(barOptionsDataDsec, 'xAxis.axisLabel.rotate', 30)
// 3) 防止被裁剪:让 grid 预留标签空间
set(barOptionsDataDsec, 'grid.containLabel', true)
set(barOptionsDataDsec, 'grid.bottom', 60) // 视情况调大/调小
set(barOptionsDataDsec, 'xAxis.axisTick.alignWithLabel', true)
set(barOptionsDataDsec, 'series', [
{
name: t('analysis.alreadyJianlian'),
type: 'bar',
data: finished
},
// {
// name: t('analysis.noAlliance'),
// type: 'bar',
// data: unfinished
// }
])
}
// 周活跃量 图表数据
const getWeeklyUserActivity = async () => {
const data = [
{ value: HostsOperationNum.value, name: 'analysis.monday' },
]
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(),
wsCache.get('roleRouters').find(item => item.id === 5019)?.children.find(item => item.id === 5020) ? getUserAccessSource(null) : null,
getWeeklyUserActivity()
])
loading.value = false
}
const handleProjectClick = (message: string) => {
window.open(`https://${message}`, '_blank')
}
const handleShortcutClick = (url: string) => {
router.push(url)
}
/** 查询员工 */
const getAllocationList = async () => {
loading.value = true
allocationUserList.value = []
try {
const data = await getSimpleUserList()
console.log('data', data)
data.forEach((item) => {
if (wsCache.get('user').user.id == item.id) {
} else {
allocationUserList.value.push({
label: item.nickname,
value: item.id
})
}
})
console.log(allocationUserList.value)
} finally {
loading.value = false
}
}
// 获取当前日期,格式为 YYYY-MM-DD
const getFormattedDate = () => {
const now = new Date()
return now.toISOString().split('T')[0] + ' 00:00:00'
}
// ✅ 抽成函数:每天数量
async function fetchDailyHostsCount() {
const res = await EmployeeHostsApi.getEmployeeHostsPage({
operationStatus: 1,
pageNo: 1,
pageSize: 10,
updateTime: getFormattedDate()
})
HostsOperationNum.value = res.total ?? 0
// 如果柱状图依赖这个值,顺便刷新一次图表数据
}
async function fetchDailyHostsCountDesc() {
const res = await EmployeeHostsApi.getEmployeeHostsPageDsec({
operationStatus: 1,
pageNo: 1,
pageSize: 10,
updateTime: getFormattedDate()
})
HostsOperationNumDsec.value = res.finishedNum ?? 0
// 如果柱状图依赖这个值,顺便刷新一次图表数据
}
// ✅ 爬主播建联率
async function fetchAllHostsCount(val) {
// 以“传入的用户 id”为准展示顺序也按这里来
const ids = allocationUserList.value.map(item => item.value)
const res = await EmployeeHostsApi.employeeCompleteBarChart(ids, val)
const rawList: EmpBarItem[] = Array.isArray(res) ? res : (res?.data ?? [])
// 补齐后端未返回的用户为 0
const list = mergeZeroUsers(ids, rawList)
// 渲染
updateEmployeeBarChart(list)
}
// ✅ 爬大哥建联率
async function fetchAllHostsCountDesc(val) {
// 以“传入的用户 id”为准展示顺序也按这里来
const idss = allocationUserList.value.map(item => item.value)
const ress = await EmployeeHostsApi.employeeCompleteBarChartDsec(idss, val)
console.log('fetchAllHostsCountDesc', ress);
const rawLists: EmpBarItem[] = Array.isArray(ress) ? ress : (ress?.data ?? [])
// 补齐后端未返回的用户为 0
const lists = mergeZeroUsers(idss, rawLists)
// 渲染
updateEmployeeBarChartDesc(lists)
}
</script>