大屏项目初始化

This commit is contained in:
2026-03-08 23:38:37 +08:00
parent 2c81b1478c
commit 03c61b4501
25 changed files with 1333 additions and 205 deletions

View File

@@ -0,0 +1,36 @@
import request from '@/utils/request'
/**
* 获取指标信息列表
*/
export function getAlertList(params) {
return request({
url: '/biz/homeDesktop/alertList',
method: 'get',
params: params
})
}
/**
* 获取指标信息列表
*/
export function getNoteList(params) {
return request({
url: '/biz/homeDesktop/noteList',
method: 'get',
params: params
})
}
/**
* 获取指标信息列表
*/
export function getQuickList(params) {
return request({
url: '/biz/homeDesktop/quickList',
method: 'get',
params: params
})
}

View File

@@ -16,57 +16,40 @@
v-loading="loading"
>
<el-table-column
prop="name"
label="姓名"
width="120"
prop="createTime"
label="记录日期"
width="180"
fixed="left"
/>
<el-table-column
prop="age"
label="年龄"
width="80"
/>
<el-table-column
prop="gender"
label="性别"
width="80"
/>
<el-table-column
prop="phone"
label="手机号"
width="150"
/>
<el-table-column
prop="email"
label="邮箱"
width="200"
/>
<el-table-column
prop="address"
label="地址"
width="250"
/>
<el-table-column
prop="company"
label="公司"
width="180"
/>
<el-table-column
prop="position"
label="职位"
width="150"
/>
<el-table-column
prop="salary"
label="薪资"
prop="alertTitle"
label="主题"
width="120"
show-overflow-tooltip="true"
/>
<el-table-column
prop="entryDate"
label="入职日期"
width="150"
fixed="right"
prop="alertContent"
label="内容"
show-overflow-tooltip="true"
/>
<el-table-column
label="级别"
width="80"
>
<template #default="scope">
{{ priorityDict[scope.row.alertLevel] || '未知' }}
</template>
</el-table-column>
<el-table-column
prop="alertType"
label="类型"
width="100"
/>
<el-table-column
prop="sourceSystem"
label="来源系统"
width="120"
/>
</el-table>
</div>
</div>
@@ -77,42 +60,32 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElTable, ElTableColumn, ElLoading } from 'element-plus'
// 生成模拟数据
const generateTableData = () => {
const data = []
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
const genders = ['男', '女']
const positions = ['前端开发', '后端开发', '产品经理', 'UI设计', '测试工程师', '运维工程师']
const companies = ['科技有限公司', '网络科技公司', '信息技术公司', '数据服务公司']
for (let i = 1; i <= 50; i++) {
data.push({
id: i,
name: names[Math.floor(Math.random() * names.length)] + i,
age: Math.floor(Math.random() * 30) + 20,
gender: genders[Math.floor(Math.random() * genders.length)],
phone: `13${Math.floor(Math.random() * 900000000) + 100000000}`,
email: `user${i}@example.com`,
address: `北京市朝阳区某某街道${Math.floor(Math.random() * 100)}`,
company: Math.floor(Math.random() * 10) + '号' + companies[Math.floor(Math.random() * companies.length)],
position: positions[Math.floor(Math.random() * positions.length)],
salary: `${Math.floor(Math.random() * 30) + 10}k`,
entryDate: `202${Math.floor(Math.random() * 5) + 1}-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`
})
}
return data
}
import { getAlertList } from '@/api/bizHome'
const tableData = ref([])
const loading = ref(true)
onMounted(() => {
// 模拟接口请求延迟
setTimeout(() => {
tableData.value = generateTableData()
loading.value = false
}, 500)
const priorityDict = {
1: '紧急',
2: '高',
3: '中',
4: '低'
}
async function getDataList() {
try {
const res = await getAlertList()
tableData.value = res || []
} catch (error) {
console.error('获取数据失败:', error)
tableData.value = []
}finally{
loading.value = false;
}
}
onMounted(async () => {
getDataList();
})
</script>
@@ -183,6 +156,23 @@ onMounted(() => {
overflow: auto;
}
:deep(.el-table td),
:deep(.el-table th.is-leaf) {
border-right: none !important;
}
:deep(.el-table__fixed-left) {
border-right: none !important;
box-shadow: none !important;
}
:deep(.el-table__fixed-right) {
border-left: none !important;
box-shadow: none !important;
}
:deep(.el-table__fixed td),
:deep(.el-table__fixed th) {
border-right: none !important;
}
:deep(.el-table__body-wrapper) {
overflow: auto;
}
@@ -207,14 +197,6 @@ onMounted(() => {
background: #c0c4cc;
}
:deep(.el-table__fixed-left) {
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table__fixed-right) {
box-shadow: -2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table th) {
font-weight: 600;
color: #303133;

View File

@@ -0,0 +1,430 @@
<template>
<div class="table-container">
<div class="main-card">
<div class="title-section">
<h1 class="title">年度便签汇总堆积图</h1>
<div class="tab-switcher">
<div
v-for="tab in tabDict"
:key="tab.key"
class="tab-item"
:class="{ 'tab-active': activeTabKey === tab.key }"
@click="switchTab(tab.key)"
>
{{ tab.label }}
</div>
</div>
</div>
<div class="divider"></div>
<div class="content-section">
<div ref="chartRef" class="chart-container"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getItemInfoList } from '@/api/bizApi'
const tabDict = [
{ key: 'NOTE_YEAR_Y001', label: '类型' }, // 默认选项
{ key: 'NOTE_YEAR_Y002', label: '月份' },
]
// 响应式数据
const activeTabKey = ref(tabDict[0].key) // 默认选中第一个
const indexCode = ref(activeTabKey.value)
const tableData = ref([])
const chartRef = ref(null)
let myChart = null
let resizeObserver = null
let resizeHandler = null
let seriesStatus = {
'待完成': true,
'进行中': true,
'已完成': true
}
const currentYear = new Date().getFullYear().toString();
// 获取数据方法
async function getDataList() {
try {
const reqParams = {
reqParam: currentYear,
itemCode: indexCode.value,
}
const res = await getItemInfoList(reqParams)
tableData.value = res || []
await nextTick()
renderChart()
} catch (error) {
console.error('获取数据失败:', error)
tableData.value = []
}
}
function switchTab(tabKey) {
if (activeTabKey.value === tabKey) return
activeTabKey.value = tabKey
indexCode.value = tabKey
getDataList()
}
// 计算总数
function calculateTotal() {
const xAxisData = tableData.value.map(item => item.xaxis || '')
const index01Data = tableData.value.map(item => Math.round(Number(item.index01) || 0))
const index02Data = tableData.value.map(item => Math.round(Number(item.index02) || 0))
const index03Data = tableData.value.map(item => Math.round(Number(item.index03) || 0))
return xAxisData.map((_, index) => {
let total = 0
if (seriesStatus['待完成']) total += index01Data[index]
if (seriesStatus['进行中']) total += index02Data[index]
if (seriesStatus['已完成']) total += index03Data[index]
return total
})
}
// 渲染图表
function renderChart() {
if (!chartRef.value || tableData.value.length === 0) return
if (myChart) {
myChart.dispose()
}
myChart = echarts.init(chartRef.value)
const xAxisData = tableData.value.map(item => item.xaxis || '')
const index01Data = tableData.value.map(item => Math.round(Number(item.index01) || 0))
const index02Data = tableData.value.map(item => Math.round(Number(item.index02) || 0))
const index03Data = tableData.value.map(item => Math.round(Number(item.index03) || 0))
const totalData = calculateTotal()
const option = {
legend: {
show: true,
orient: 'horizontal',
top: '8%',
left: 'center',
textStyle: { fontSize: 12, color: '#303133' },
itemWidth: 14,
itemHeight: 14,
itemGap: 20,
data: ['待完成', '进行中', '已完成'],
selected: seriesStatus,
selectable: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
triggerOn: 'mousemove'
},
padding: 12,
formatter: function(params) {
let total = 0
const validParams = params.filter(param => param.seriesName !== '总数')
validParams.forEach(param => total += param.value)
let html = `<div style="text-align: center; font-weight: bold; font-size: 12px; margin-bottom: 8px; color: #303133;">${params[0].axisValue}</div>`
html += `<table style="width: 100%; border-collapse: collapse; font-size: 12px;">`
html += `<tr style="background: #f5f7fa;">
<th style="padding: 6px; border: 1px solid #e6e6e6; text-align: center;">状态/数值</th>
${validParams.map(param => `<th style="padding: 6px; border: 1px solid #e6e6e6; text-align: center;">${param.seriesName}</th>`).join('')}
<th style="padding: 6px; border: 1px solid #e6e6e6; text-align: center;">总数</th>
</tr>`
html += `<tr>
<td style="padding: 6px; border: 1px solid #e6e6e6; text-align: center; color: #606266;">数量</td>
${validParams.map(param => `<td style="padding: 6px; border: 1px solid #e6e6e6; text-align: center; color: #303133;">${param.value}</td>`).join('')}
<td style="padding: 6px; border: 1px solid #e6e6e6; text-align: center; color: #303133; font-weight: bold;">${total}</td>
</tr>`
html += `</table>`
return html
},
position: function (point, params, dom, rect, size) {
return [point[0] + 10, point[1] - 10]
},
textStyle: { fontSize: 12 },
triggerOn: 'mousemove|click',
triggerOff: 'mouseout',
enterable: false,
leaveDelay: 0,
hideDelay: 0,
alwaysShowContent: false,
showContent: true
},
grid: {
left: '5%',
right: '5%',
bottom: '8%',
top: '18%',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#e5e7eb' } },
axisTick: { alignWithLabel: true, length: 6 },
axisLabel: { interval: 0, fontSize: 12, color: '#606266', rotate: 0, margin: 10 }
},
yAxis: {
type: 'value',
name: '数量',
nameTextStyle: { fontSize: 12, color: '#606266' },
axisLine: { lineStyle: { color: '#e5e7eb' } },
axisTick: { show: true },
axisLabel: {
formatter: (value) => `${Math.round(value)}`,
fontSize: 12,
color: '#606266'
},
minInterval: 1,
splitNumber: 5,
splitLine: { lineStyle: { color: '#f5f5f5' } },
max: function(value) {
return value.max * 1.2
}
},
series: [
{
name: '待完成',
type: 'bar',
stack: 'total',
data: index01Data,
barWidth: '12%',
itemStyle: { color: '#f56c6c' },
label: {
show: true,
position: 'inside',
formatter: '{c}',
fontSize: 10,
color: '#fff',
fontWeight: 'bold'
}
},
{
name: '进行中',
type: 'bar',
stack: 'total',
data: index02Data,
barWidth: '12%',
itemStyle: { color: '#409eff' },
label: {
show: true,
position: 'inside',
formatter: '{c}',
fontSize: 10,
color: '#fff',
fontWeight: 'bold'
}
},
{
name: '已完成',
type: 'bar',
stack: 'total',
data: index03Data,
barWidth: '12%',
itemStyle: { color: '#67c23a' },
label: {
show: true,
position: 'inside',
formatter: '{c}',
fontSize: 10,
color: '#fff',
fontWeight: 'bold'
}
},
{
name: '总数',
type: 'bar',
stack: 'total',
data: new Array(xAxisData.length).fill(0),
barWidth: '12%',
itemStyle: { color: 'transparent' },
label: {
show: true,
position: 'top',
formatter: function(params) {
return totalData[params.dataIndex]
},
fontSize: 11,
color: '#303133',
fontWeight: 'bold'
}
}
]
}
myChart.setOption(option)
myChart.on('legendselectchanged', function(params) {
seriesStatus[params.name] = params.selected[params.name]
const newTotalData = calculateTotal()
myChart.setOption({
series: [{
name: '总数',
label: {
formatter: function(p) {
return newTotalData[p.dataIndex]
}
}
}]
})
})
myChart.resize()
}
function watchContainerResize() {
if (!chartRef.value) return
resizeObserver = new ResizeObserver(() => {
if (myChart) myChart.resize()
})
resizeObserver.observe(chartRef.value)
resizeHandler = () => {
if (myChart) myChart.resize()
}
window.addEventListener('resize', resizeHandler)
}
function cleanUp() {
if (resizeObserver) resizeObserver.disconnect()
if (resizeHandler) window.removeEventListener('resize', resizeHandler)
if (myChart) {
myChart.off('legendselectchanged')
myChart.dispose()
myChart = null
}
resizeObserver = null
resizeHandler = null
}
watch(tableData, () => {
renderChart()
}, { deep: true })
onMounted(async () => {
await getDataList()
watchContainerResize()
})
onUnmounted(() => {
cleanUp()
})
</script>
<style scoped>
.table-container {
width: 100%;
height: 100%;
padding: 2px;
margin: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.main-card {
width: 100%;
height: 100%;
border: 1px solid #e5e7eb;
border-radius: 4px;
background-color: #fff;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.title-section {
width: 100%;
height: 36px;
padding: 0 12px;
box-sizing: border-box;
background-color: #f9fafb;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0;
border-radius: 4px 4px 0 0;
}
.title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
}
.tab-switcher {
display: flex;
gap: 8px;
overflow-x: auto;
scrollbar-width: none;
}
.tab-switcher::-webkit-scrollbar {
display: none;
}
.tab-item {
padding: 4px 12px;
font-size: 12px;
color: #606266;
cursor: pointer;
transition: all 0.2s;
position: relative;
white-space: nowrap;
}
.tab-item:hover {
color: #409eff;
}
.tab-item.tab-active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background-color: #409eff;
border-radius: 1px;
}
.divider {
width: 100%;
height: 1px;
background-color: #e5e7eb;
}
.content-section {
flex: 1;
width: 100%;
padding: 8px;
box-sizing: border-box;
overflow: hidden;
margin: 0;
display: flex;
align-items: center;
}
.chart-container {
width: 100%;
height: 100%;
min-height: 300px;
box-sizing: border-box;
}
</style>

View File

@@ -16,57 +16,46 @@
v-loading="loading"
>
<el-table-column
prop="name"
label="姓名"
width="120"
prop="createTime"
label="记录日期"
width="180"
fixed="left"
/>
<el-table-column
prop="age"
label="年龄"
width="80"
/>
<el-table-column
prop="gender"
label="性别"
width="80"
/>
<el-table-column
prop="phone"
label="手机号"
width="150"
/>
<el-table-column
prop="email"
label="邮箱"
width="200"
/>
<el-table-column
prop="address"
label="地址"
width="250"
/>
<el-table-column
prop="company"
label="公司"
width="180"
/>
<el-table-column
prop="position"
label="职位"
width="150"
/>
<el-table-column
prop="salary"
label="薪资"
prop="title"
label="主题"
width="120"
show-overflow-tooltip="true"
/>
<el-table-column
prop="entryDate"
label="入职日期"
width="150"
fixed="right"
prop="content"
label="内容"
show-overflow-tooltip="true"
/>
<el-table-column
label="级别"
width="80"
>
<template #default="scope">
{{ priorityDict[scope.row.priority] || '未知' }}
</template>
</el-table-column>
<el-table-column
label="类型"
width="100"
>
<template #default="scope">
{{ typeDict[scope.row.type] || '未知' }}
</template>
</el-table-column>
<el-table-column
label="状态"
width="100"
>
<template #default="scope">
{{ stausDict[scope.row.ustatus] || '未知' }}
</template>
</el-table-column>
</el-table>
</div>
</div>
@@ -76,43 +65,45 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ElTable, ElTableColumn, ElLoading } from 'element-plus'
// 生成模拟数据
const generateTableData = () => {
const data = []
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
const genders = ['男', '女']
const positions = ['前端开发', '后端开发', '产品经理', 'UI设计', '测试工程师', '运维工程师']
const companies = ['科技有限公司', '网络科技公司', '信息技术公司', '数据服务公司']
for (let i = 1; i <= 50; i++) {
data.push({
id: i,
name: names[Math.floor(Math.random() * names.length)] + i,
age: Math.floor(Math.random() * 30) + 20,
gender: genders[Math.floor(Math.random() * genders.length)],
phone: `13${Math.floor(Math.random() * 900000000) + 100000000}`,
email: `user${i}@example.com`,
address: `北京市朝阳区某某街道${Math.floor(Math.random() * 100)}`,
company: Math.floor(Math.random() * 10) + '号' + companies[Math.floor(Math.random() * companies.length)],
position: positions[Math.floor(Math.random() * positions.length)],
salary: `${Math.floor(Math.random() * 30) + 10}k`,
entryDate: `202${Math.floor(Math.random() * 5) + 1}-${Math.floor(Math.random() * 12) + 1}-${Math.floor(Math.random() * 28) + 1}`
})
}
return data
}
import { ElTable, ElTableColumn } from 'element-plus'
import { getNoteList } from '@/api/bizHome'
const tableData = ref([])
const loading = ref(true)
onMounted(() => {
// 模拟接口请求延迟
setTimeout(() => {
tableData.value = generateTableData()
const priorityDict = {
low: '低',
medium: '中',
high: '高'
}
const typeDict = {
work: '工作',
life: '生活',
study: '学习',
other: '其他'
}
const stausDict = {
todo: '待开始',
doing: '进行中',
done: '已完成'
}
async function getDataList() {
try {
const res = await getNoteList()
tableData.value = res || []
} catch (error) {
console.error('获取数据失败:', error)
tableData.value = []
} finally {
loading.value = false
}, 500)
}
}
onMounted(async () => {
getDataList()
})
</script>
@@ -183,7 +174,23 @@ onMounted(() => {
overflow: auto;
}
/* 表格滚动条样式优化 */
:deep(.el-table td),
:deep(.el-table th.is-leaf) {
border-right: none !important;
}
:deep(.el-table__fixed-left) {
border-right: none !important;
box-shadow: none !important;
}
:deep(.el-table__fixed-right) {
border-left: none !important;
box-shadow: none !important;
}
:deep(.el-table__fixed td),
:deep(.el-table__fixed th) {
border-right: none !important;
}
:deep(.el-table__body-wrapper) {
overflow: auto;
}
@@ -208,15 +215,6 @@ onMounted(() => {
background: #c0c4cc;
}
/* 固定表头样式优化 */
:deep(.el-table__fixed-left) {
box-shadow: 2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table__fixed-right) {
box-shadow: -2px 0 6px rgba(0, 0, 0, 0.05);
}
:deep(.el-table th) {
font-weight: 600;
color: #303133;

View File

@@ -15,9 +15,9 @@
@click="handleCardClick(item)"
>
<div class="icon-wrapper">
<img :src="item.icon" :alt="item.name" class="login-icon" />
<img :src="item.iconClass" :alt="item.systemName" class="login-icon" />
</div>
<div class="card-name">{{ item.name }}</div>
<div class="card-name">{{ item.systemName }}</div>
</div>
</div>
</div>
@@ -45,18 +45,22 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { ElButton, ElMessage } from 'element-plus'
import { getQuickList } from '@/api/bizHome'
const loginList = ref([
{ icon: 'https://picsum.photos/120/80?1', name: '账号密码' },
{ icon: 'https://picsum.photos/120/80?2', name: '手机验证码' },
{ icon: 'https://picsum.photos/120/80?3', name: '微信登录' },
{ icon: 'https://picsum.photos/120/80?4', name: '支付宝登录' },
{ icon: 'https://picsum.photos/120/80?5', name: 'QQ登录' },
{ icon: 'https://picsum.photos/120/80?6', name: '微博登录' },
{ icon: 'https://picsum.photos/120/80?7', name: '邮箱登录' },
{ icon: 'https://picsum.photos/120/80?8', name: '抖音登录' },
{ icon: 'https://picsum.photos/120/80?9', name: '快手登录' }
])
const loginList = ref()
const loading = ref(true)
async function getDataList() {
try {
const res = await getQuickList()
loginList.value = res || []
} catch (error) {
console.error('获取数据失败:', error)
loginList.value = []
}finally{
loading.value = false;
}
}
const cardWrapperRef = ref(null)
const cardListRef = ref(null)
@@ -94,10 +98,11 @@ const scrollCards = (direction) => {
}
const handleCardClick = (item) => {
ElMessage.success(`选择了${item.name}登录`)
window.open(item.homepageUrl,"_blank");
}
onMounted(() => {
getDataList()
const list = cardListRef.value
if (list) {
scrollLeft.value = list.scrollLeft

View File

@@ -24,12 +24,14 @@
</div>
<div class="right-section">
<div class="right-top-section">
<el-card class="right-card right-top-card">
<div class="right-placeholder">上区域内容</div>
<el-card shadow="hover" class="card-item right-top-card">
<div class="right-placeholder">
<ChartNote />
</div>
</el-card>
</div>
<div class="right-bottom-section">
<el-card class="right-card right-bottom-card">
<el-card shadow="hover" class="card-item right-bottom-card">
<div class="right-placeholder">下区域内容</div>
</el-card>
</div>
@@ -44,6 +46,7 @@ import UserTop from './components/UserTop.vue';
import NoteLeft from './components/Note.vue'
import AlertMain from './components/Alert.vue'
import QuickLogin from './components/Quick.vue'
import ChartNote from './components/ChartNote.vue'
</script>
<style scoped>
@@ -91,7 +94,7 @@ import QuickLogin from './components/Quick.vue'
}
.left-section {
width: 35%;
width: 45%;
height: 100%;
display: flex;
flex-direction: column;
@@ -101,6 +104,7 @@ import QuickLogin from './components/Quick.vue'
overflow: hidden;
}
/* 核心样式card-item 统一所有卡片样式 */
.card-item {
flex: 1;
display: flex;
@@ -136,30 +140,24 @@ import QuickLogin from './components/Quick.vue'
box-sizing: border-box;
}
.right-card {
/* 移除右侧卡片独立样式,统一继承 card-item */
.right-top-card, .right-bottom-card {
/* 仅保留占位,如需特殊样式可在此添加 */
}
/* 统一卡片内容占位符样式 */
.right-placeholder {
padding: 4px;
text-align: center;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 16px;
box-sizing: border-box;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
background-color: #fff;
}
.right-card:hover {
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.right-placeholder {
padding: 4px;
text-align: center;
}
/* 深度样式统一作用于所有 el-card */
:deep(.el-card) {
border-radius: 8px;
height: 100%;
@@ -179,6 +177,7 @@ import QuickLogin from './components/Quick.vue'
transform: translateY(-2px);
}
/* 响应式样式保持不变 */
@media (max-width: 768px) {
.main-section {
flex-direction: column;