feat: 重构首页布局并添加新功能模块

- 重构首页布局,移除旧图表组件,添加流程表格和迷你日历
- 新增常用应用组件,支持收藏和管理常用功能
- 新增流程表格组件,展示我的流程和待办任务
- 新增迷你日历组件,支持不同类型日期标记
- 优化统计卡片组件,拆分客户和供应商统计
- 调整部分表格列名显示更准确
This commit is contained in:
砂糖
2025-10-31 13:14:10 +08:00
parent cd040e57ea
commit c657dad4d3
7 changed files with 806 additions and 37 deletions

View File

@@ -0,0 +1,397 @@
<template>
<div class="all-applications-container">
<!-- 常用应用区域 -->
<div class="frequently-used-section">
<div class="frequently-used-header">
<h3 class="frequently-title">常用应用</h3>
<el-button
type="text"
size="small"
class="edit-btn"
@click="isEditingFavorites = !isEditingFavorites"
>
{{ isEditingFavorites ? '完成' : '编辑' }}
</el-button>
</div>
<div class="frequently-used-grid" v-if="frequentlyUsedApps.length > 0">
<div
v-for="app in frequentlyUsedApps"
:key="`fav-${app.parentPath}-${app.childPath}`"
class="frequently-app-item"
@click="handleAppClick(getParentMenu(app.parentPath), getChildMenu(app.parentPath, app.childPath))"
>
<div class="app-icon-wrapper">
<svg-icon :icon-class="getChildMenu(app.parentPath, app.childPath).meta.icon || 'documentation'" class="app-icon" />
</div>
<span class="app-name">{{ getChildMenu(app.parentPath, app.childPath).meta.title }}</span>
<!-- 删除按钮 - 仅在编辑模式显示 -->
<div
class="remove-btn"
v-if="isEditingFavorites"
@click.stop="removeFromFavorites(app.parentPath, app.childPath)"
>
<i class="el-icon-close"></i>
</div>
</div>
</div>
<div v-else>
<el-empty description="暂无常用应用" />
</div>
</div>
<!-- 原有全部应用区域 -->
<h3 class="title" v-if="isEditingFavorites">全部应用</h3>
<el-tabs v-model="activeTabName" class="app-tabs" v-if="isEditingFavorites">
<el-tab-pane
v-for="menu in filteredMenus"
:key="menu.path"
:label="menu.meta.title"
:name="menu.path"
>
<div class="app-grid">
<div
v-for="child in menu.children"
:key="child.path"
class="app-item"
>
<!-- @click="handleAppClick(menu, child)" -->
<div class="app-icon-wrapper">
<svg-icon :icon-class="child.meta.icon || 'documentation'" class="app-icon" />
<!-- 添加到常用按钮 -->
<div
class="add-to-favorite-btn"
@click.stop="addToFavorites(menu.path, child.path)"
v-if="!isInFavorites(menu.path, child.path)"
>
<i class="el-icon-star-off"></i>
</div>
<!-- 已在常用中的标识 -->
<div
class="in-favorite-indicator"
v-if="isInFavorites(menu.path, child.path)"
>
<i class="el-icon-star-on"></i>
</div>
</div>
<span class="app-name">{{ child.meta.title }}</span>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { getRouters } from '@/api/menu'
import path from 'path'
export default {
name: 'AllApplications',
data() {
return {
allMenus: [],
activeTabName: '',
frequentlyUsedApps: [], // 存储常用应用
isEditingFavorites: false // 是否处于编辑常用应用模式
}
},
computed: {
filteredMenus() {
const filterHidden = (menus) => {
return menus
.filter(menu => menu.hidden !== true)
.map(menu => {
if (menu.children) {
menu.children = filterHidden(menu.children)
}
return menu
})
}
const topLevelMenus = filterHidden(this.allMenus).filter(
menu => menu.children && menu.children.length > 0
)
return topLevelMenus
}
},
created() {
this.fetchMenus()
this.loadFrequentlyUsedApps()
},
methods: {
fetchMenus() {
getRouters().then(response => {
this.allMenus = response.data
if (this.filteredMenus.length > 0) {
this.activeTabName = this.filteredMenus[0].path
}
})
},
// 加载常用应用从localStorage
loadFrequentlyUsedApps() {
const saved = localStorage.getItem('frequentlyUsedApps')
if (saved) {
try {
this.frequentlyUsedApps = JSON.parse(saved)
} catch (e) {
console.error('Failed to parse frequently used apps', e)
this.frequentlyUsedApps = []
}
}
},
// 保存常用应用到localStorage
saveFrequentlyUsedApps() {
localStorage.setItem('frequentlyUsedApps', JSON.stringify(this.frequentlyUsedApps))
},
// 添加到常用应用
addToFavorites(parentPath, childPath) {
if (!this.isInFavorites(parentPath, childPath)) {
// 限制最多10个常用应用
if (this.frequentlyUsedApps.length >= 10) {
this.$message.warning('常用应用最多只能添加10个')
return
}
this.frequentlyUsedApps.unshift({ parentPath, childPath })
this.saveFrequentlyUsedApps()
this.$message.success('已添加到常用应用')
}
},
// 从常用应用中移除
removeFromFavorites(parentPath, childPath) {
this.frequentlyUsedApps = this.frequentlyUsedApps.filter(
app =>!(app.parentPath === parentPath && app.childPath === childPath)
)
this.saveFrequentlyUsedApps()
},
// 检查应用是否在常用列表中
isInFavorites(parentPath, childPath) {
return this.frequentlyUsedApps.some(
app => app.parentPath === parentPath && app.childPath === childPath
)
},
// 根据路径获取父菜单
getParentMenu(parentPath) {
return this.filteredMenus.find(menu => menu.path === parentPath)
},
// 根据路径获取子菜单
getChildMenu(parentPath, childPath) {
const parent = this.getParentMenu(parentPath)
if (parent && parent.children) {
return parent.children.find(child => child.path === childPath)
}
return {
meta: {}
}
},
handleAppClick(parentMenu, childMenu) {
if (!childMenu) return
const basePath = parentMenu.path
const fullPath = path.resolve(basePath, childMenu.path)
this.$router.push(fullPath)
}
}
}
</script>
<style lang="scss" scoped>
.all-applications-container {
padding: 16px; /* 调整间距,更贴近飞书紧凑感 */
border-radius: 12px; /* 飞书常用较大圆角 */
margin-top: 24px;
}
// 常用应用样式
.frequently-used-section {
margin-bottom: 32px; /* 增大间距 */
padding-bottom: 20px;
border-bottom: 1px solid #ddd; /* 飞书浅灰边框色 */
}
.frequently-used-header {
display: flex;
justify-content: space-between;
align-items: center;
// margin-bottom: 20px;
}
.frequently-title {
font-size: 16px; /* 稍小字体,飞书风格更简洁 */
font-weight: 500; /* 调整 FontWeight */
color: #111; /* 深一点的标题色 */
}
.edit-btn {
color: #687b98; /* 飞书常用的主题蓝 */
padding: 0;
font-size: 14px;
}
.frequently-used-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); /* 调整卡片宽度 */
gap: 20px; /* 增大间距 */
}
.frequently-app-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 16px; /* 调整内边距 */
border-radius: 12px; /* 大圆角 */
transition: all 0.3s ease;
position: relative;
// background-color: ; /* 飞书卡片常用浅灰底色 */
&:hover {
background-color: #fff; /* hover 时的浅灰 */
}
}
// 全部应用标题
.title {
font-size: 16px;
font-weight: 500;
margin-bottom: 20px;
color: #ddd;
}
// 应用网格布局
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); /* 调整卡片宽度 */
gap: 24px; /* 增大间距 */
padding-top: 16px;
}
.app-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 16px;
border-radius: 12px;
transition: background-color 0.3s ease;
position: relative;
// background-color: #222;
&:hover {
background-color: #fff;
}
}
// 应用图标样式
.app-icon-wrapper {
width: 48px; /* 稍小图标容器 */
height: 48px;
border-radius: 10px; /* 图标容器圆角 */
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
background-color: #fff; /* 飞书风格的浅蓝底色 */
position: relative;
}
.app-icon {
font-size: 24px; /* 调整图标大小 */
color: #687b98; /* 主题蓝 */
}
// 应用名称样式
.app-name {
font-size: 14px;
color: #687b98; /* 飞书常用的文本色 */
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
// 添加到常用按钮
.add-to-favorite-btn {
position: absolute;
bottom: 4px;
right: 4px;
width: 24px; /* 稍大按钮 */
height: 24px;
border-radius: 6px; /* 小圆角 */
background-color: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
color: #c0c4cc;
transition: all 0.2s;
&:hover {
color: #ffb400; /* 飞书常用的强调色 */
background-color: #fff;
}
}
// 已在常用中的标识
.in-favorite-indicator {
position: absolute;
bottom: 4px;
right: 4px;
width: 24px;
height: 24px;
border-radius: 6px;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #ffb400;
}
// 删除常用应用按钮
.remove-btn {
position: absolute;
top: -6px;
right: -6px;
width: 24px;
height: 24px;
border-radius: 6px;
background-color: #ff4d4f; /* 飞书删除按钮红 */
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
&:hover {
background-color: #ff3839;
transform: scale(1.05);
}
}
// 标签页样式
::v-deep .el-tabs__header {
margin-bottom: 0;
}
::v-deep .el-tabs__nav-wrap::after {
height: 1px;
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<div class="order-analysis-dashboard" v-loading="loading">
<!-- 业绩区 -->
<el-tabs v-model="activeTab">
<el-tab-pane label="我的流程" name="my">
<KLPTable v-loading="loading" :data="ownProcessList">
<el-table-column label="流程编号" align="center" prop="procInsId" :show-overflow-tooltip="true" />
<el-table-column label="流程名称" align="center" prop="procDefName" :show-overflow-tooltip="true" />
<el-table-column label="流程类别" align="center" prop="category" />
<el-table-column label="流程版本" align="center" width="80px">
<template slot-scope="scope">
<el-tag size="medium">v{{ scope.row.procDefVersion }}</el-tag>
</template>
</el-table-column>
<el-table-column label="当前节点" align="center" prop="taskName" />
<el-table-column label="提交时间" align="center" prop="createTime" width="180" />
<el-table-column label="流程状态" align="center" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.wf_process_status" :value="scope.row.processStatus" />
</template>
</el-table-column>
<el-table-column label="耗时" align="center" prop="duration" width="180" />
</KLPTable>
</el-tab-pane>
<el-tab-pane label="代办任务" name="todo">
<KLPTable v-loading="loading" :data="todoList">
<el-table-column label="任务编号" align="center" prop="taskId" :show-overflow-tooltip="true" />
<el-table-column label="流程名称" align="center" prop="procDefName" />
<el-table-column label="任务节点" align="center" prop="taskName" />
<el-table-column label="流程版本" align="center">
<template slot-scope="scope">
<el-tag size="medium">v{{ scope.row.procDefVersion }}</el-tag>
</template>
</el-table-column>
<el-table-column label="流程发起人" align="center">
<template slot-scope="scope">
<label>{{ scope.row.startUserName }}</label>
</template>
</el-table-column>
<el-table-column label="接收时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit-outline" @click="handleProcess(scope.row)"
v-hasPermi="['workflow:process:approval']">办理
</el-button>
</template>
</el-table-column>
</KLPTable>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import { listOwnProcess } from '@/api/workflow/process';
import { listTodoProcess } from '@/api/workflow/process';
export default {
name: 'OrderAnalysisDashboard',
data() {
return {
loading: false,
activeTab: 'my',
ownProcessList: [],
todoList: [],
queryParams: {
pageNum: 1,
pageSize: 20
},
dateRange: []
}
},
dicts: ['wf_process_status'],
created() {
this.loading = true;
Promise.all([this.getList1(), this.getList2()]).then(() => {
this.loading = false;
});
},
methods: {
/** 查询流程定义列表 */
async getList1() {
const response = await listOwnProcess(this.addDateRange(this.queryParams, this.dateRange));
this.ownProcessList = response.rows;
},
async getList2() {
const response = await listTodoProcess(this.addDateRange(this.queryParams, this.dateRange));
this.todoList = response.rows;
},
}
}
</script>
<style scoped>
.order-analysis-dashboard {
padding: 24px;
box-sizing: border-box;
}
.section-row {
margin-bottom: 30px;
}
.section-title {
margin-bottom: 20px;
padding: 0 10px;
}
.section-title h2 {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: #303133;
}
.section-title p {
margin: 0;
font-size: 14px;
color: #909399;
}
.top-row,
.chart-row {
margin-bottom: 20px;
}
.chart-row>.el-col {
display: flex;
flex-direction: column;
justify-content: stretch;
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<!-- 外层容器占用 30% 宽度可根据需要自行调样式 -->
<div class="mini-calendar" style="">
<!-- 日历头部显示YYYY年M月 -->
<div class="calendar-header">
<div>个人日历</div>
<div>{{ currentMonthYear }}</div>
</div>
<!-- 日历主体使用 table 简易排版每周 7 只显示当前月的日期 -->
<table class="calendar-body">
<thead>
<tr>
<th v-for="(day, index) in weekDays" :key="index">{{ day }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in calendarRows" :key="rowIndex">
<td
v-for="(cell, cellIndex) in row"
:key="cellIndex"
:class="getDayClass(cell.type)"
>
<!-- 只显示数字不含任何额外文本 -->
<span>{{ cell.dayNum }}</span>
</td>
</tr>
</tbody>
</table>
<!-- legend 区域展示不同 type 的色块与说明 -->
<div class="calendar-legend">
<div class="legend-item" v-for="legend in legends" :key="legend.type">
<span
class="color-box"
:style="{ backgroundColor: legend.color }"
></span>
<span class="legend-text">{{ legend.label }}</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: "MiniCalendar",
/**
* 父组件可以通过 :daysData 传入当前月的具体日期和 type。
* 示例:[{ date: '2025-03-01', type: 1 }, { date: '2025-03-02', type: 2 }, ...]
*/
props: {
daysData: {
type: Array,
default: () => []
}
},
data() {
return {
// 星期栏
weekDays: ["日", "一", "二", "三", "四", "五", "六"],
// legend 配置type对应的背景色 + 说明
legends: [
{ type: 1, label: "休息日", color: "#d3f6f3" },
{ type: 2, label: "节假日", color: "#ffe7ba" },
{ type: 3, label: "调休", color: "#ffdede" },
{ type: 4, label: "出差", color: "#fa18bc" },
{ type: 5, label: "请假", color: "#c4c4eb" }
]
};
},
computed: {
/**
* 当前日期对象(默认为今天,也可自行改成其它月份)
*/
currentDate() {
return new Date();
},
currentMonth() {
return this.currentDate.getMonth(); // 0-11
},
currentYear() {
return this.currentDate.getFullYear();
},
// 用于在日历头部显示“YYYY年M月”
currentMonthYear() {
return `${this.currentYear}${this.currentMonth + 1}`;
},
/**
* 生成一个二维数组,每一行代表一周,单元格中有 { dayNum, dateStr, type }。
* 只显示当月的日期,如果第一天不是周日,前面会插入空格。
*/
calendarRows() {
// 当前月1号
const firstDayOfMonth = new Date(this.currentYear, this.currentMonth, 1);
const firstDayIndex = firstDayOfMonth.getDay(); // 0=周日,1=周一, ...
// 本月总天数
const daysInMonth = new Date(this.currentYear, this.currentMonth + 1, 0).getDate();
// 准备一个空数组,往里放 dayNum 和 type
const cells = [];
// 1) 填充本月第一天前面的空白单元格
for (let i = 0; i < firstDayIndex; i++) {
cells.push({ dayNum: "", dateStr: "", type: null });
}
// 2) 填充本月每一天
for (let day = 1; day <= daysInMonth; day++) {
const dateObj = new Date(this.currentYear, this.currentMonth, day);
const dateStr = this.formatDate(dateObj); // YYYY-MM-DD
// 根据 daysData 查找 type
const dayType = this.findDayType(dateStr);
cells.push({
dayNum: day,
dateStr,
type: dayType
});
}
// 3) 将 cells 拆分成多行每行7列
const rows = [];
for (let i = 0; i < cells.length; i += 7) {
rows.push(cells.slice(i, i + 7));
}
return rows;
}
},
methods: {
/**
* 格式化 Date => "YYYY-MM-DD"
*/
formatDate(d) {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
},
/**
* 在 daysData 中找对应日期的 type找不到就默认为 0工作日
*/
findDayType(dateStr) {
const item = this.daysData.find((day) => day.date === dateStr);
return item ? item.type : 0; // 默认工作日(0)
},
/**
* 返回一个 class 用于设置对应背景色
*/
getDayClass(type) {
switch (type) {
case 0: return "type-workday"; // 工作日
case 1: return "type-rest"; // 休息日
case 2: return "type-holiday"; // 节假日
case 3: return "type-adjust"; // 调休
case 4: return "type-trip"; // 出差
case 5: return "type-leave"; // 请假
default: return "";
}
}
}
};
</script>
<style scoped>
.mini-calendar {
border: 1px solid #ccc;
padding: 10px;
background-color: white;
box-sizing: border-box;
/* 若需要更细节的布局控制,可以加上 float 或 display 属性,比如 float: left; */
}
/* 日历头部 */
.calendar-header {
text-align: center;
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
}
/* 日历主体样式 */
.calendar-body {
width: 100%;
border-collapse: collapse;
margin-bottom: 8px;
}
.calendar-body th,
.calendar-body td {
width: 14.28%; /* 7天 */
height: 40px;
text-align: center;
vertical-align: middle;
}
/* 不同 type 的背景色 */
.type-workday {
background-color: transparent;
}
.type-rest {
background-color: #d3f6f3;
}
.type-holiday {
background-color: #ffe7ba;
}
.type-adjust {
background-color: #ffdede;
}
.type-trip {
background-color: #c6ebf5;
}
.type-leave {
background-color: #c4c4eb;
}
/* legend 部分:展示各类型色块和说明 */
.calendar-legend {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.legend-item {
display: flex;
align-items: center;
}
.color-box {
display: inline-block;
width: 14px;
height: 14px;
margin-right: 4px;
}
.legend-text {
font-size: 12px;
}
</style>

View File

@@ -81,13 +81,21 @@ export default {
duration: 3200
},
{
type: 'customerSupplierCount',
title: '客户和供应商',
type: 'customerCount',
title: '客户',
icon: 'people',
iconClass: 'icon-customer',
value: 0,
duration: 3600
},
{
type: 'supplierCount',
title: '供应商',
icon: 'peoples',
iconClass: 'icon-supplier',
value: 0,
duration: 3600
},
{
type: 'equipmentCount',
title: '设备数量',
@@ -134,8 +142,10 @@ export default {
item.value = materialCoilRes.total
} else if (item.type === 'orderCount') {
item.value = orderRes.total
} else if (item.type === 'customerSupplierCount') {
item.value = customerRes.total + supplierRes.total
} else if (item.type === 'customerCount') {
item.value = customerRes.total
} else if (item.type === 'supplierCount') {
item.value = supplierRes.total
} else if (item.type === 'equipmentCount') {
item.value = equipmentRes.total
}
@@ -189,6 +199,10 @@ export default {
.icon-equipment {
background: #722ed1;
}
.icon-supplier {
background: #ffed65;
}
}
.icon-material {
@@ -211,6 +225,10 @@ export default {
color: #722ed1;
}
.icon-supplier {
color: #ffed65;
}
.card-panel-icon-wrapper {
float: left;
margin: 14px 0 0 14px;