新增预警页面

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

@@ -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();