新增预警页面

This commit is contained in:
2025-12-09 00:20:41 +08:00
parent daeca7b5d7
commit 3aa11787dc
16 changed files with 1431 additions and 18 deletions

View File

@@ -0,0 +1,15 @@
package com.jeesite.modules.biz.dao;
import com.jeesite.common.dao.CrudDao;
import com.jeesite.common.mybatis.annotation.MyBatisDao;
import com.jeesite.modules.biz.entity.BizDataReport;
/**
* 数据指标DAO接口
* @author gaoxq
* @version 2025-12-08
*/
@MyBatisDao(dataSourceName="work")
public interface BizDataReportDao extends CrudDao<BizDataReport> {
}

View File

@@ -0,0 +1,86 @@
package com.jeesite.modules.biz.entity;
import java.io.Serializable;
import java.util.Date;
import com.jeesite.common.mybatis.annotation.JoinTable;
import com.jeesite.common.mybatis.annotation.JoinTable.Type;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import com.jeesite.common.entity.DataEntity;
import com.jeesite.common.mybatis.annotation.Column;
import com.jeesite.common.mybatis.annotation.Table;
import com.jeesite.common.mybatis.mapper.query.QueryType;
import com.jeesite.common.utils.excel.annotation.ExcelField;
import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
import com.jeesite.common.utils.excel.annotation.ExcelFields;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 数据指标Entity
*
* @author gaoxq
* @version 2025-12-08
*/
@EqualsAndHashCode(callSuper = true)
@Table(name = "biz_data_report", alias = "a", label = "指标信息信息", columns = {
@Column(name = "create_time", attrName = "createTime", label = "记录时间", isUpdate = false, isUpdateForce = true),
@Column(name = "id", attrName = "id", label = "主键ID", isPK = true),
@Column(name = "route_path", attrName = "routePath", label = "路由地址"),
@Column(name = "name", attrName = "name", label = "看板名称", queryType = QueryType.LIKE),
@Column(name = "image", attrName = "image", label = "看板图标", isQuery = false),
@Column(name = "sorting", attrName = "sorting", label = "排序", isQuery = false),
@Column(name = "remark", attrName = "remark", label = "看板描述"),
}, orderBy = "a.id DESC"
)
@Data
public class BizDataReport extends DataEntity<BizDataReport> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Date createTime; // 记录时间
private String routePath; // 路由地址
private String name; // 看板名称
private String image; // 看板图标
private Long sorting; // 排序
private String remark; // 看板描述
@ExcelFields({
@ExcelField(title = "记录时间", attrName = "createTime", align = Align.CENTER, sort = 10, dataFormat = "yyyy-MM-dd hh:mm"),
@ExcelField(title = "主键ID", attrName = "id", align = Align.CENTER, sort = 20),
@ExcelField(title = "看板名称", attrName = "name", align = Align.CENTER, sort = 40),
@ExcelField(title = "看板图标", attrName = "image", align = Align.CENTER, sort = 50),
@ExcelField(title = "排序", attrName = "sorting", align = Align.CENTER, sort = 60),
@ExcelField(title = "看板描述", attrName = "remark", align = Align.CENTER, sort = 70),
})
public BizDataReport() {
this(null);
}
public BizDataReport(String id) {
super(id);
}
public Date getCreateTime_gte() {
return sqlMap.getWhere().getValue("create_time", QueryType.GTE);
}
public void setCreateTime_gte(Date createTime) {
sqlMap.getWhere().and("create_time", QueryType.GTE, createTime);
}
public Date getCreateTime_lte() {
return sqlMap.getWhere().getValue("create_time", QueryType.LTE);
}
public void setCreateTime_lte(Date createTime) {
sqlMap.getWhere().and("create_time", QueryType.LTE, createTime);
}
}

View File

@@ -0,0 +1,134 @@
package com.jeesite.modules.biz.service;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jeesite.common.entity.Page;
import com.jeesite.common.service.CrudService;
import com.jeesite.modules.biz.entity.BizDataReport;
import com.jeesite.modules.biz.dao.BizDataReportDao;
import com.jeesite.common.service.ServiceException;
import com.jeesite.common.config.Global;
import com.jeesite.common.validator.ValidatorUtils;
import com.jeesite.common.utils.excel.ExcelImport;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
/**
* 数据指标Service
* @author gaoxq
* @version 2025-12-08
*/
@Service
public class BizDataReportService extends CrudService<BizDataReportDao, BizDataReport> {
/**
* 获取单条数据
* @param bizDataReport 主键
*/
@Override
public BizDataReport get(BizDataReport bizDataReport) {
return super.get(bizDataReport);
}
/**
* 查询分页数据
* @param bizDataReport 查询条件
* @param bizDataReport page 分页对象
*/
@Override
public Page<BizDataReport> findPage(BizDataReport bizDataReport) {
return super.findPage(bizDataReport);
}
/**
* 查询列表数据
* @param bizDataReport 查询条件
*/
@Override
public List<BizDataReport> findList(BizDataReport bizDataReport) {
return super.findList(bizDataReport);
}
/**
* 保存数据(插入或更新)
* @param bizDataReport 数据对象
*/
@Override
@Transactional
public void save(BizDataReport bizDataReport) {
super.save(bizDataReport);
}
/**
* 导入数据
* @param file 导入的数据文件
*/
@Transactional
public String importData(MultipartFile file) {
if (file == null){
throw new ServiceException(text("请选择导入的数据文件!"));
}
int successNum = 0; int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
try(ExcelImport ei = new ExcelImport(file, 2, 0)){
List<BizDataReport> list = ei.getDataList(BizDataReport.class);
for (BizDataReport bizDataReport : list) {
try{
ValidatorUtils.validateWithException(bizDataReport);
this.save(bizDataReport);
successNum++;
successMsg.append("<br/>" + successNum + "、编号 " + bizDataReport.getId() + " 导入成功");
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、编号 " + bizDataReport.getId() + " 导入失败:";
if (e instanceof ConstraintViolationException){
ConstraintViolationException cve = (ConstraintViolationException)e;
for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
msg += Global.getText(violation.getMessage()) + " ("+violation.getPropertyPath()+")";
}
}else{
msg += e.getMessage();
}
failureMsg.append(msg);
logger.error(msg, e);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
failureMsg.append(e.getMessage());
return failureMsg.toString();
}
if (failureNum > 0) {
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
}else{
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
/**
* 更新状态
* @param bizDataReport 数据对象
*/
@Override
@Transactional
public void updateStatus(BizDataReport bizDataReport) {
super.updateStatus(bizDataReport);
}
/**
* 删除数据
* @param bizDataReport 数据对象
*/
@Override
@Transactional
public void delete(BizDataReport bizDataReport) {
super.delete(bizDataReport);
}
}

View File

@@ -0,0 +1,154 @@
package com.jeesite.modules.biz.web;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.jeesite.common.config.Global;
import com.jeesite.common.collect.ListUtils;
import com.jeesite.common.entity.Page;
import com.jeesite.common.lang.DateUtils;
import com.jeesite.common.utils.excel.ExcelExport;
import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
import org.springframework.web.multipart.MultipartFile;
import com.jeesite.common.web.BaseController;
import com.jeesite.modules.biz.entity.BizDataReport;
import com.jeesite.modules.biz.service.BizDataReportService;
/**
* 数据指标Controller
*
* @author gaoxq
* @version 2025-12-08
*/
@Controller
@RequestMapping(value = "${adminPath}/biz/dataReport")
public class BizDataReportController extends BaseController {
private final BizDataReportService bizDataReportService;
public BizDataReportController(BizDataReportService bizDataReportService) {
this.bizDataReportService = bizDataReportService;
}
/**
* 获取数据
*/
@ModelAttribute
public BizDataReport get(String id, boolean isNewRecord) {
return bizDataReportService.get(id, isNewRecord);
}
/**
* 查询列表
*/
@RequiresPermissions("biz:dataReport:view")
@RequestMapping(value = {"list", ""})
public String list(BizDataReport bizDataReport, Model model) {
model.addAttribute("bizDataReport", bizDataReport);
return "modules/biz/bizDataReportList";
}
/**
* 查询列表数据
*/
@RequiresPermissions("biz:dataReport:view")
@RequestMapping(value = "listData")
@ResponseBody
public Page<BizDataReport> listData(BizDataReport bizDataReport, HttpServletRequest request, HttpServletResponse response) {
bizDataReport.setPage(new Page<>(request, response));
Page<BizDataReport> page = bizDataReportService.findPage(bizDataReport);
return page;
}
/**
* 查看编辑表单
*/
@RequiresPermissions("biz:dataReport:view")
@RequestMapping(value = "form")
public String form(BizDataReport bizDataReport, Model model) {
model.addAttribute("bizDataReport", bizDataReport);
return "modules/biz/bizDataReportForm";
}
/**
* 保存数据
*/
@RequiresPermissions("biz:dataReport:edit")
@PostMapping(value = "save")
@ResponseBody
public String save(@Validated BizDataReport bizDataReport) {
bizDataReportService.save(bizDataReport);
return renderResult(Global.TRUE, text("保存指标信息成功!"));
}
/**
* 导出数据
*/
@RequiresPermissions("biz:dataReport:view")
@RequestMapping(value = "exportData")
public void exportData(BizDataReport bizDataReport, HttpServletResponse response) {
List<BizDataReport> list = bizDataReportService.findList(bizDataReport);
String fileName = "指标信息" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
try (ExcelExport ee = new ExcelExport("指标信息", BizDataReport.class)) {
ee.setDataList(list).write(response, fileName);
}
}
/**
* 下载模板
*/
@RequiresPermissions("biz:dataReport:view")
@RequestMapping(value = "importTemplate")
public void importTemplate(HttpServletResponse response) {
BizDataReport bizDataReport = new BizDataReport();
List<BizDataReport> list = ListUtils.newArrayList(bizDataReport);
String fileName = "指标信息模板.xlsx";
try (ExcelExport ee = new ExcelExport("指标信息", BizDataReport.class, Type.IMPORT)) {
ee.setDataList(list).write(response, fileName);
}
}
/**
* 导入数据
*/
@ResponseBody
@RequiresPermissions("biz:dataReport:edit")
@PostMapping(value = "importData")
public String importData(MultipartFile file) {
try {
String message = bizDataReportService.importData(file);
return renderResult(Global.TRUE, "posfull:" + message);
} catch (Exception ex) {
return renderResult(Global.FALSE, "posfull:" + ex.getMessage());
}
}
/**
* 删除数据
*/
@RequiresPermissions("biz:dataReport:edit")
@RequestMapping(value = "delete")
@ResponseBody
public String delete(BizDataReport bizDataReport) {
bizDataReportService.delete(bizDataReport);
return renderResult(Global.TRUE, text("删除指标信息成功!"));
}
@RequestMapping(value = "listAll")
@ResponseBody
public List<BizDataReport> listAll(BizDataReport bizDataReport) {
return bizDataReportService.findList(bizDataReport);
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jeesite.modules.biz.dao.BizDataReportDao">
<!-- 查询数据
<select id="findList" resultType="BizDataReport">
SELECT ${sqlMap.column.toSql()}
FROM ${sqlMap.table.toSql()}
<where>
${sqlMap.where.toSql()}
</where>
ORDER BY ${sqlMap.order.toSql()}
</select> -->
</mapper>

View File

@@ -41,6 +41,7 @@
"ant-design-vue": "4.2.6",
"axios": "1.12.2",
"dayjs": "1.11.18",
"echarts": "5.4.3",
"lodash-es": "4.17.21",
"vue": "3.5.22",
"vue-eslint-parser": "10.2.0",

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
*/
import { defHttp } from '@jeesite/core/utils/http/axios';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
import { UploadApiResult } from '@jeesite/core/api/sys/upload';
import { UploadFileParams } from '@jeesite/types/axios';
import { AxiosProgressEvent } from 'axios';
const { ctxPath, adminPath } = useGlobSetting();
export interface BizDataReport extends BasicModel<BizDataReport> {
createTime?: string; // 记录时间
routePath?: string; // 路由地址
name: string; // 看板名称
image: string; // 看板图标
sorting: number; // 排序
remark: string; // 看板描述
}
export const bizDataReportList = (params?: BizDataReport | any) =>
defHttp.get<BizDataReport>({ url: adminPath + '/biz/dataReport/list', params });
export const bizDataReportListAll = (params?: BizDataReport | any) =>
defHttp.get<BizDataReport[]>({ url: adminPath + '/biz/dataReport/listAll', params });
export const bizDataReportListData = (params?: BizDataReport | any) =>
defHttp.post<Page<BizDataReport>>({ url: adminPath + '/biz/dataReport/listData', params });
export const bizDataReportForm = (params?: BizDataReport | any) =>
defHttp.get<BizDataReport>({ url: adminPath + '/biz/dataReport/form', params });
export const bizDataReportSave = (params?: any, data?: BizDataReport | any) =>
defHttp.postJson<BizDataReport>({ url: adminPath + '/biz/dataReport/save', params, data });
export const bizDataReportImportData = (
params: UploadFileParams,
onUploadProgress: (progressEvent: AxiosProgressEvent) => void,
) =>
defHttp.uploadFile<UploadApiResult>(
{
url: ctxPath + adminPath + '/biz/dataReport/importData',
onUploadProgress,
},
params,
);
export const bizDataReportDelete = (params?: BizDataReport | any) =>
defHttp.get<BizDataReport>({ url: adminPath + '/biz/dataReport/delete', params });

View File

@@ -0,0 +1,130 @@
<!--
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
-->
<template>
<BasicDrawer
v-bind="$attrs"
:showFooter="true"
:okAuth="'biz:dataReport:edit'"
@register="registerDrawer"
@ok="handleSubmit"
width="70%"
>
<template #title>
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
<span> {{ getTitle.value }} </span>
</template>
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup name="ViewsBizDataReportForm">
import { ref, unref, computed } from 'vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { router } from '@jeesite/core/router';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicForm, FormSchema, useForm } from '@jeesite/core/components/Form';
import { BasicDrawer, useDrawerInner } from '@jeesite/core/components/Drawer';
import { BizDataReport, bizDataReportSave, bizDataReportForm } from '@jeesite/biz/api/biz/dataReport';
const emit = defineEmits(['success', 'register']);
const { t } = useI18n('biz.dataReport');
const { showMessage } = useMessage();
const { meta } = unref(router.currentRoute);
const record = ref<BizDataReport>({} as BizDataReport);
const getTitle = computed(() => ({
icon: meta.icon || 'i-ant-design:book-outlined',
value: record.value.isNewRecord ? t('新增指标信息') : t('编辑指标信息'),
}));
const inputFormSchemas: FormSchema<BizDataReport>[] = [
{
label: t('看板名称'),
field: 'name',
component: 'Input',
componentProps: {
maxlength: 52,
},
required: true,
},
{
label: t('看板路由'),
field: 'routePath',
component: 'Input',
componentProps: {
maxlength: 152,
},
required: true,
},
{
label: t('图片地址'),
field: 'image',
component: 'Input',
componentProps: {
maxlength: 225,
},
required: true,
},
{
label: t('看板排序'),
field: 'sorting',
component: 'InputNumber',
componentProps: {
maxlength: 10,
},
required: true,
},
{
label: t('描述说明'),
field: 'remark',
component: 'InputTextArea',
componentProps: {
maxlength: 125,
},
colProps: { md: 24, lg: 24 },
},
];
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm<BizDataReport>({
labelWidth: 120,
schemas: inputFormSchemas,
baseColProps: { md: 24, lg: 12 },
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
setDrawerProps({ loading: true });
await resetFields();
const res = await bizDataReportForm(data);
record.value = (res.bizDataReport || {}) as BizDataReport;
record.value.__t = new Date().getTime();
await setFieldsValue(record.value);
setDrawerProps({ loading: false });
});
async function handleSubmit() {
try {
const data = await validate();
setDrawerProps({ confirmLoading: true });
const params: any = {
isNewRecord: record.value.isNewRecord,
id: record.value.id || data.id,
};
// console.log('submit', params, data, record);
const res = await bizDataReportSave(params, data);
showMessage(res.message);
setTimeout(closeDrawer);
emit('success', data);
} catch (error: any) {
if (error && error.errorFields) {
showMessage(error.message || t('common.validateError'));
}
console.log('error', error);
} finally {
setDrawerProps({ confirmLoading: false });
}
}
</script>

View File

@@ -0,0 +1,103 @@
<!--
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
-->
<template>
<BasicModal
v-bind="$attrs"
:title="t('导入指标信息')"
:okText="t('导入')"
@register="registerModal"
@ok="handleSubmit"
:minHeight="120"
:width="400"
>
<Upload accept=".xls,.xlsx" :file-list="fileList" :before-upload="beforeUpload" @remove="handleRemove">
<a-button> <Icon icon="ant-design:upload-outlined" /> {{ t('选择文件') }} </a-button>
<span class="ml-4">{{ uploadInfo }}</span>
</Upload>
<div class="ml-4 mt-4">
{{ t('提示仅允许导入“xls”或“xlsx”格式文件') }}
</div>
<div class="mt-4">
<a-button @click="handleDownloadTemplate()" type="text">
<Icon icon="i-fa:file-excel-o" />
{{ t('下载模板') }}
</a-button>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Upload } from 'ant-design-vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { downloadByUrl } from '@jeesite/core/utils/file/download';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicModal, useModalInner } from '@jeesite/core/components/Modal';
import { bizDataReportImportData } from '@jeesite/biz/api/biz/dataReport';
import { FileType } from 'ant-design-vue/es/upload/interface';
import { AxiosProgressEvent } from 'axios';
const emit = defineEmits(['success', 'register']);
const { t } = useI18n('biz.dataReport');
const { showMessage, showMessageModal } = useMessage();
const fileList = ref<FileType[]>([]);
const uploadInfo = ref('');
const beforeUpload = (file: FileType) => {
fileList.value = [file];
return false;
};
const handleRemove = () => {
fileList.value = [];
};
const [registerModal, { setModalProps, closeModal }] = useModalInner(() => {
fileList.value = [];
uploadInfo.value = '';
});
async function handleDownloadTemplate() {
const { ctxAdminPath } = useGlobSetting();
downloadByUrl({ url: ctxAdminPath + '/biz/dataReport/importTemplate' });
}
function onUploadProgress(progressEvent: AxiosProgressEvent) {
const complete = ((progressEvent.loaded / (progressEvent.total || 1)) * 100) | 0;
if (complete != 100) {
uploadInfo.value = t('正在导入,请稍候') + ' ' + complete + '%...';
} else {
uploadInfo.value = '';
}
}
async function handleSubmit() {
try {
if (fileList.value.length == 0) {
showMessage(t('请选择要导入的数据文件'));
return;
}
setModalProps({ confirmLoading: true });
const params = {
file: fileList.value[0],
};
const { data } = await bizDataReportImportData(params, onUploadProgress);
showMessageModal({ content: data.message });
setTimeout(closeModal);
emit('success');
} catch (error: any) {
if (error && error.errorFields) {
showMessage(error.message || t('common.validateError'));
}
console.log('error', error);
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@@ -0,0 +1,124 @@
<template>
<!-- 调整卡片和图表容器尺寸让图表整体更小 -->
<a-card title="业务数据柱状图" style="width: 100%; height: 400px; margin: 20px 0;">
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
</a-card>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Card } from 'ant-design-vue';
import * as echarts from 'echarts';
const chartDom = ref<HTMLDivElement | null>(null);
let myChart: echarts.ECharts | null = null;
// 封装公共的标签配置(避免重复代码)
const barLabelConfig = {
show: true, // 开启数值显示
position: 'top', // 数值显示在柱子顶部
distance: 3, // 数值与柱子顶部的间距px
textStyle: {
fontSize: 11, // 数值字体大小
color: '#333', // 数值字体颜色(深色更清晰)
fontWeight: '500' // 字体加粗
},
formatter: '{c} 万'
};
const initChart = () => {
if (!chartDom.value) return;
myChart = echarts.init(chartDom.value);
const option = {
title: {
text: '月度销售额统计',
left: 'center',
textStyle: { fontSize: 18, color: '#333' }
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
textStyle: { fontSize: 12 }
},
legend: {
data: ['产品A', '产品B','产品C'],
top: 40,
textStyle: { fontSize: 12 }
},
// 调整图表内部边距,让绘图区域更紧凑
grid: {
left: '8%',
right: '8%',
bottom: '10%',
top: '20%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
axisLabel: { fontSize: 12 }
},
yAxis: {
type: 'value',
name: '销售额(万元)',
min: 0,
axisLabel: { fontSize: 12 }
},
series: [
{
name: '产品A',
type: 'bar',
data: [50, 70, 65, 80, 90, 100],
itemStyle: { color: '#1890ff' },
barWidth: 25, // 调整柱子宽度(数值越小越细)
label: barLabelConfig // 引用公共标签配置
},
{
name: '产品B',
type: 'bar',
data: [30, 45, 55, 70, 85, 95],
itemStyle: { color: '#52c41a' },
barWidth: 25,
label: barLabelConfig
},
{
name: '产品C',
type: 'bar',
data: [30, 45, 55, 70, 85, 95],
itemStyle: { color: '#f5a623' }, // 给产品C改个不同颜色避免和B混淆
barWidth: 25,
label: barLabelConfig
}
]
};
myChart.setOption(option);
};
// 窗口缩放时自适应图表
const resizeChart = () => myChart?.resize();
onMounted(() => {
initChart();
window.addEventListener('resize', resizeChart);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeChart);
myChart?.dispose(); // 销毁图表,避免内存泄漏
myChart = null;
});
</script>
<style scoped>
:deep(.ant-card) {
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
/* 可选:优化数值标签的样式(如果需要) */
:deep(.echarts-label) {
font-family: 'Microsoft YaHei', sans-serif;
}
</style>

View File

@@ -0,0 +1,220 @@
<!--
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author gaoxq
-->
<template>
<div>
<BasicTable @register="registerTable">
<template #tableTitle>
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
<span> {{ getTitle.value }} </span>
</template>
<template #toolbar>
<a-button type="default" :loading="loading" @click="handleExport()">
<Icon icon="i-ant-design:download-outlined" /> {{ t('导出') }}
</a-button>
<a-button type="primary" @click="handleForm({})" v-auth="'biz:dataReport:edit'">
<Icon icon="i-fluent:add-12-filled" /> {{ t('新增') }}
</a-button>
</template>
<template #slotBizKey="{ record }">
<a @click="handleForm({ id: record.id })" :title="record.name">
{{ record.name }}
</a>
</template>
<template #slotBizImg="{ record }">
<img :src="record.image" style="width: 30px; height: 24px; object-fit: contain;" />
</template>
</BasicTable>
<InputForm @register="registerDrawer" @success="handleSuccess" />
<FormImport @register="registerImportModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" setup name="ViewsBizDataReportList">
import { onMounted, ref, unref } from 'vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { downloadByUrl } from '@jeesite/core/utils/file/download';
import { router } from '@jeesite/core/router';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicTable, BasicColumn, useTable } from '@jeesite/core/components/Table';
import { BizDataReport, bizDataReportList } from '@jeesite/biz/api/biz/dataReport';
import { bizDataReportDelete, bizDataReportListData } from '@jeesite/biz/api/biz/dataReport';
import { useDrawer } from '@jeesite/core/components/Drawer';
import { useModal } from '@jeesite/core/components/Modal';
import { FormProps } from '@jeesite/core/components/Form';
import InputForm from './form.vue';
import FormImport from './formImport.vue';
const { t } = useI18n('biz.dataReport');
const { showMessage } = useMessage();
const { meta } = unref(router.currentRoute);
const record = ref<BizDataReport>({} as BizDataReport);
const getTitle = {
icon: meta.icon || 'i-ant-design:book-outlined',
value: meta.title || t('指标信息管理'),
};
const loading = ref(false);
const searchForm: FormProps<BizDataReport> = {
baseColProps: { md: 8, lg: 6 },
labelWidth: 90,
schemas: [
{
label: t('记录时间起'),
field: 'createTime_gte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('记录时间止'),
field: 'createTime_lte',
component: 'DatePicker',
componentProps: {
format: 'YYYY-MM-DD HH:mm',
showTime: { format: 'HH:mm' },
},
},
{
label: t('看板名称'),
field: 'name',
component: 'Input',
},
],
};
const tableColumns: BasicColumn<BizDataReport>[] = [
{
title: t('记录时间'),
dataIndex: 'createTime',
key: 'a.create_time',
sorter: true,
width: 180,
align: 'left',
fixed: 'left',
},
{
title: t('路由地址'),
dataIndex: 'routePath',
key: 'a.route_path',
sorter: true,
width: 225,
align: 'left',
},
{
title: t('看板名称'),
dataIndex: 'name',
key: 'a.name',
sorter: true,
width: 200,
align: 'left',
slot: 'slotBizKey',
},
{
title: t('看板图标'),
dataIndex: 'image',
key: 'a.image',
sorter: true,
width: 130,
align: 'left',
slot: 'slotBizImg',
},
{
title: t('排序'),
dataIndex: 'sorting',
key: 'a.sorting',
sorter: true,
width: 130,
align: 'center',
},
{
title: t('看板描述'),
dataIndex: 'remark',
key: 'a.remark',
sorter: true,
width: 225,
align: 'left',
},
];
const actionColumn: BasicColumn<BizDataReport> = {
width: 160,
align: 'center',
actions: (record: BizDataReport) => [
{
icon: 'i-clarity:note-edit-line',
title: t('编辑'),
onClick: handleForm.bind(this, { id: record.id }),
auth: 'biz:dataReport:edit',
},
{
icon: 'i-ant-design:delete-outlined',
color: 'error',
title: t('删除'),
popConfirm: {
title: t('是否确认删除指标信息?'),
confirm: handleDelete.bind(this, record),
},
auth: 'biz:dataReport:edit',
},
],
};
const [registerTable, { reload, getForm }] = useTable<BizDataReport>({
api: bizDataReportListData,
beforeFetch: (params) => {
return params;
},
columns: tableColumns,
actionColumn: actionColumn,
formConfig: searchForm,
showTableSetting: true,
useSearchForm: true,
canResize: true,
});
onMounted(async () => {
const res = await bizDataReportList();
record.value = (res.bizDataReport || {}) as BizDataReport;
await getForm().setFieldsValue(record.value);
});
const [registerDrawer, { openDrawer }] = useDrawer();
function handleForm(record: Recordable) {
openDrawer(true, record);
}
async function handleExport() {
loading.value = true;
const { ctxAdminPath } = useGlobSetting();
await downloadByUrl({
url: ctxAdminPath + '/biz/dataReport/exportData',
params: getForm().getFieldsValue(),
});
loading.value = false;
}
const [registerImportModal, { openModal: importModal }] = useModal();
function handleImport() {
importModal(true, {});
}
async function handleDelete(record: Recordable) {
const params = { id: record.id };
const res = await bizDataReportDelete(params);
showMessage(res.message);
await handleSuccess(record);
}
async function handleSuccess(record: Recordable) {
await reload({ record });
}
</script>

View File

@@ -2,7 +2,7 @@
<div class="pt-2 lg:flex">
<Avatar :src="userinfo.avatarUrl || headerImg" :size="72" class="!mx-auto !block" />
<div class="mt-2 flex flex-col justify-center md:ml-6 md:mt-0">
<h1 class="text-md md:text-lg">您好, {{ userinfo.userName }}, 开始您一天的工作吧</h1>
<h1 class="text-md md:text-lg">您好, <a @click="handleMyWorkClick">{{ userinfo.userName }}</a>, 开始您一天的工作吧</h1>
<span class="text-secondary"> 今日晴20 - 32 </span>
</div>
<div class="mt-4 flex flex-1 justify-end md:mt-0">
@@ -25,9 +25,15 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Avatar } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@jeesite/core/store/modules/user';
import headerImg from '@jeesite/assets/images/header.jpg';
const router = useRouter();
const userStore = useUserStore();
const userinfo = computed(() => userStore.getUserInfo);
const handleMyWorkClick = () => {
router.push('/desktop/workbench');
};
</script>

View File

@@ -0,0 +1,349 @@
<template>
<div class="card-container">
<button
class="control-btn left-btn"
@click="scrollCards('left')"
:disabled="!canScrollLeft"
>
<LeftOutlined />
</button>
<div class="card-scroll-wrapper" ref="scrollWrapper">
<div class="card-list" ref="cardList">
<Card
v-for="(item, index) in cardListData"
:key="index"
class="custom-card"
@click="handleCardClick(item.routePath)"
>
<div class="card-img-wrapper">
<img :src="item.image" :alt="item.name" class="card-img" />
</div>
<div class="card-name">{{ item.name }}</div>
</Card>
</div>
</div>
<!-- 右侧控制按钮 -->
<button
class="control-btn right-btn"
@click="scrollCards('right')"
:disabled="!canScrollRight"
@mousedown="forceUpdateScrollStatus"
>
<RightOutlined />
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { Card } from 'ant-design-vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
import { bizDataReportListAll, BizDataReport } from '@jeesite/biz/api/biz/dataReport';
// 路由实例
const router = useRouter();
const scrollWrapper = ref<HTMLDivElement | null>(null);
const cardList = ref<HTMLDivElement | null>(null);
const isScrolling = ref(false);
let scrollRafId: number | null = null;
let resizeTimer: NodeJS.Timeout | null = null;
const cardListData = ref<BizDataReport[]>([]);
// 配置项
const scrollStep = 190;
const scrollTolerance = 10;
const scrollAccuracy = 1;
const canScrollLeft = ref(false);
const canScrollRight = ref(false);
const fetchCardList = async () => {
try {
// 新增错误处理,避免接口异常导致页面卡死
const result = await bizDataReportListAll();
cardListData.value = result || []; // 防止返回 null 导致渲染异常
} catch (error) {
console.error('获取应用列表失败:', error);
cardListData.value = []; // 异常时置空列表,显示空状态
}
};
const updateScrollStatus = () => {
if (!scrollWrapper.value || !cardList.value) {
canScrollLeft.value = false;
canScrollRight.value = false;
return;
}
const { scrollLeft, clientWidth } = scrollWrapper.value;
const listScrollWidth = cardList.value.scrollWidth;
// 左滚判断
canScrollLeft.value = scrollLeft > scrollAccuracy;
// 右滚判断
const remainingScroll = listScrollWidth - (scrollLeft + clientWidth);
canScrollRight.value = remainingScroll > scrollTolerance;
};
const debounceUpdate = () => {
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
nextTick(updateScrollStatus);
}, 100);
};
const forceUpdateScrollStatus = () => {
nextTick(updateScrollStatus);
};
const scrollCards = (direction: 'left' | 'right') => {
const wrapper = scrollWrapper.value;
const list = cardList.value;
if (!wrapper || !list || isScrolling.value) return;
if (scrollRafId !== null) {
cancelAnimationFrame(scrollRafId);
scrollRafId = null;
}
isScrolling.value = true;
updateScrollStatus();
if (
(direction === 'left' && !canScrollLeft.value) ||
(direction === 'right' && !canScrollRight.value)
) {
isScrolling.value = false;
return;
}
let targetLeft = direction === 'left'
? wrapper.scrollLeft - scrollStep
: wrapper.scrollLeft + scrollStep;
const maxScrollLeft = list.scrollWidth - wrapper.clientWidth;
targetLeft = Math.max(0, Math.min(Math.round(targetLeft), maxScrollLeft));
const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
if (!supportsSmoothScroll) {
wrapper.scrollLeft = targetLeft;
isScrolling.value = false;
updateScrollStatus();
return;
}
wrapper.scrollTo({ left: targetLeft, behavior: 'smooth' });
const checkScrollEnd = () => {
if (!isScrolling.value) {
scrollRafId = null;
updateScrollStatus();
return;
}
const currentLeft = wrapper.scrollLeft;
const isAtTarget = Math.abs(currentLeft - targetLeft) <= scrollAccuracy;
const isAtEdge = (direction === 'left' && currentLeft <= scrollAccuracy) ||
(direction === 'right' && currentLeft >= maxScrollLeft - scrollAccuracy);
if (isAtTarget || isAtEdge) {
isScrolling.value = false;
scrollRafId = null;
updateScrollStatus();
return;
}
scrollRafId = requestAnimationFrame(checkScrollEnd);
};
const raf = window.requestAnimationFrame || ((cb) => {
const id = setTimeout(cb, 16);
return id as unknown as number;
});
scrollRafId = raf(checkScrollEnd);
};
const handleCardClick = (routePath: string) => {
console.log(routePath)
router.push(routePath);
};
// 初始化
onMounted(() => {
fetchCardList()
nextTick(() => {
const wrapper = scrollWrapper.value;
if (wrapper) {
wrapper.addEventListener('scroll', updateScrollStatus, { passive: true });
updateScrollStatus();
}
});
window.addEventListener('resize', debounceUpdate);
setTimeout(updateScrollStatus, 200);
});
// 清理资源
onUnmounted(() => {
if (resizeTimer) clearTimeout(resizeTimer);
if (scrollRafId !== null) {
cancelAnimationFrame(scrollRafId);
scrollRafId = null;
}
const wrapper = scrollWrapper.value;
if (wrapper) {
wrapper.removeEventListener('scroll', updateScrollStatus);
}
window.removeEventListener('resize', debounceUpdate);
});
watch(isScrolling, () => {
nextTick(updateScrollStatus);
});
</script>
<style scoped>
/* 样式部分保持不变 */
.card-container {
width: 100%;
height: 18vh;
min-height: 160px;
background-color: #e6f7ff;
padding: 10px 0;
position: relative;
overflow: hidden;
box-sizing: border-box;
display: flex;
align-items: center;
}
.control-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 24px !important;
min-width: 24px !important;
max-width: 24px !important;
height: 80%;
border-radius: 4px;
border: none;
background-color: #8cb4f5;
color: white;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: background-color 0.3s ease;
box-sizing: border-box;
padding: 0;
pointer-events: auto;
}
.left-btn {
left: 4px;
}
.right-btn {
right: 4px;
}
.control-btn:disabled {
cursor: not-allowed;
opacity: 0.4;
background-color: #8cb4f5;
}
.control-btn:hover:not(:disabled) {
background-color: #8cb4f5;
}
.card-scroll-wrapper {
width: calc(100% - 60px);
margin: 0 30px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
height: 100%;
flex: 1;
box-sizing: border-box;
}
.card-scroll-wrapper::-webkit-scrollbar {
display: none;
}
.card-list {
display: flex;
gap: 10px;
padding: 0 5px;
white-space: nowrap;
height: 100%;
align-items: center;
width: fit-content;
min-width: 100%;
}
.custom-card {
width: 180px;
height: calc(100% - 10px);
flex-shrink: 0;
border-radius: 6px;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 1px solid #f0f0f0;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.custom-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.card-img-wrapper {
width: 100%;
height: 65%;
overflow: hidden;
border-radius: 6px 6px 0 0;
}
.card-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.custom-card:hover .card-img {
transform: scale(1.03);
}
.card-name {
width: 100%;
height: 35%;
padding: 4px 6px;
font-size: 12px;
font-weight: 500;
text-align: center;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
</style>

View File

@@ -1,8 +1,19 @@
<template>
<PageWrapper title="数据看板">
<template #headerContent>
<MySchedule />
</template>
<div>
ssssss
</div>
</PageWrapper>
</template>
<script lang="ts" setup name="AboutPage">
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
import { PageWrapper } from '@jeesite/core/components/Page';
import MySchedule from './components/MySchedule.vue';
<script>
</script>
<style>
</style>

20
web-vue/pnpm-lock.yaml generated
View File

@@ -59,6 +59,9 @@ importers:
dayjs:
specifier: 1.11.18
version: 1.11.18
echarts:
specifier: 5.4.3
version: 5.4.3
lodash-es:
specifier: 4.17.21
version: 4.17.21
@@ -536,7 +539,7 @@ packages:
'@ant-design/icons-vue@7.0.1':
resolution: {integrity: sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==}
peerDependencies:
vue: ^3.5.17
vue: ^3.5.13
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
@@ -3269,6 +3272,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts@5.4.3:
resolution: {integrity: sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==}
echarts@6.0.0:
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
@@ -6081,6 +6087,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zrender@5.4.4:
resolution: {integrity: sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==}
zrender@6.0.0:
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
@@ -9143,6 +9152,11 @@ snapshots:
eastasianwidth@0.2.0: {}
echarts@5.4.3:
dependencies:
tslib: 2.3.0
zrender: 5.4.4
echarts@6.0.0:
dependencies:
tslib: 2.3.0
@@ -12118,6 +12132,10 @@ snapshots:
zod@3.25.76: {}
zrender@5.4.4:
dependencies:
tslib: 2.3.0
zrender@6.0.0:
dependencies:
tslib: 2.3.0

View File

@@ -9,7 +9,6 @@ import '@jeesite/core/design/index.less';
import App from './App.vue';
import { createApp } from 'vue';
import { isDevMode } from '@jeesite/core/utils/env';
import { registerGlobComp } from '@jeesite/core/components/registerGlobComp';
import { initAppConfigStore } from '@jeesite/core/logics/initAppConfig';
import { setupErrorHandle } from '@jeesite/core/logics/error-handle';
@@ -20,6 +19,9 @@ import { setupRouterGuard } from '@jeesite/core/router/guard';
import { setupStore } from '@jeesite/core/store';
import { setupDForm } from '@jeesite/dfm';
// 1. 引入 ECharts全量引入兼容Jeesite所有场景
import * as echarts from 'echarts';
async function bootstrap() {
const app = createApp(App);
@@ -53,18 +55,11 @@ async function bootstrap() {
// Dynamic Form
setupDForm();
app.config.globalProperties.$echarts = echarts;
if (window) window.echarts = echarts;
app.mount('#app');
}
// 仅开发模式显示
if (!isDevMode()) {
console.log(
'%c JeeSite %c快速开发平台 \n%c 用心去做我们的快速开发平台,用心去帮助我们的客户!让您用着省心的平台。\n 您的一个关注,就是对我们最大的支持: https://gitee.com/thinkgem/jeesite-vue (请点 star 收藏我们)\n 免费 QQ 技术交流群: 127515876、209330483、223507718、709534275、730390092、1373527、183903863(外包) \n 免费 微信 技术交流群: http://s.jeesite.com 如果加不上,可添加 微信 jeesitex 邀请您进群。%c\n ',
'font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:39px;color:#0f87e8;-webkit-text-fill-color:#0f87e8;-webkit-text-stroke:1px #0f87e8;',
'font-size:24px;color:#aaa;',
'font-size:14px;color:#888;',
'font-size:12px;',
);
}
bootstrap().then();
bootstrap().then();