This commit is contained in:
dhr
2025-09-28 18:03:50 +08:00
158 changed files with 51592 additions and 274 deletions

View File

@ -0,0 +1,309 @@
<template>
<div class="system-busPresettingBit-add">
<el-dialog v-model="isShowDialog" width="1250px" :close-on-click-modal="false" :destroy-on-close="true"
@close="closeDialog">
<template #header>
<div
v-drag="['.system-busPresettingBit-add .el-dialog', '.system-busPresettingBit-add .el-dialog__header']">
{{ title }}:添加摄像头预置位
</div>
</template>
<div class="info_list">
<div class="video_box">
<div class="video-container" id="video-container" style="width: 870px; height: 600px"></div>
</div>
<div>
<el-button type="primary" style="margin: 0 20px 10px" @click="addPre">
<el-icon>
<Plus />
</el-icon>
添加预置点
</el-button>
<el-table v-loading="loading" :data="tableData.data" border>
<el-table-column label="序号" align="center" type="index" width="55" />
<el-table-column label="名称" align="center" prop="presetName" width="120px"
show-overflow-tooltip>
<template #default="scope">
<el-input v-model="scope.row.presetName" placeholder="请输入内容"
@change="handleEdit(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="135px">
<template #default="scope">
<el-button type="primary" link @click="handleDebug(scope.row)">
<el-icon>
<View />
</el-icon>调用
</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">
<el-icon>
<DeleteFilled />
</el-icon>删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination style="padding: 5px 16px" v-show="tableData.total > 0" :total="tableData.total"
v-model:page="tableData.param.pageNum" v-model:limit="tableData.param.pageSize"
@pagination="busPresettingBitList" :layout="layout" />
</div>
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, onBeforeUnmount, getCurrentInstance, nextTick } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
import { listDevicePreset, addDevicePreset, updateDevicePreset, delDevicePreset, callDevicePreset } from '@/api/devicePreset';
import { getToken } from '@/api/securitySurveillance/index.js';
import EZUIKit from 'ezuikit-js';
import { ca } from 'element-plus/es/locale/index.mjs';
const emit = defineEmits(['update']);
const { proxy } = getCurrentInstance() as any;
const formRef = ref<HTMLElement | null>(null);
const menuRef = ref();
const loading = ref(false);
const isShowDialog = ref(false);
const layout = ref('total, prev, pager, next');
const title = ref('');
const updateRow = ref<any>(null);
const src = ref(null);
const flvPlayer = ref<any>(null);
const formData = ref({
deviceSerial: undefined,
channelNo: '1',
presetName: undefined
});
const tableData = ref({
data: [],
total: 0,
loading: false,
param: {
pageNum: 1,
pageSize: 15,
deviceSerial: ''
}
});
// 打开弹窗
function openDialog(row: any) {
resetForm();
updateRow.value = row;
title.value = row.deviceName;
formData.value.deviceSerial = row.deviceSerial;
tableData.value.param.deviceSerial = row.deviceSerial;
isShowDialog.value = true;
busPresettingBitList();
nextTick(() => {
videoPlay(row);
});
}
// 添加预置点
function addPre() {
ElMessageBox.prompt('请输入预置点名称', '添加预置点', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputErrorMessage: '请输入预置点名称'
})
.then(({ value }) => {
// 加载动画
const loading = ElLoading.service({
lock: true,
text: '添加中',
background: 'rgba(0, 0, 0, 0.7)',
})
formData.value.presetName = value;
addDevicePreset(formData.value)
.then(() => {
ElMessage.success('添加成功');
busPresettingBitList();
})
.finally(() => {
// loading.value = false;
loading.close();
});
})
.catch(() => { });
}
// 视频播放
function videoPlay(obj: any) {
console.log('objobjobj', obj);
getToken().then((res: any) => {
if (res.msg == "ok" && obj.deviceSerial) {
flvPlayer.value = new EZUIKit.EZUIKitPlayer({
audio: '0',
id: 'video-container',
accessToken: res.data,
url: `ezopen://open.ys7.com/${obj.deviceSerial}/1.hd.live`,
template: 'pcLive',
width: 870,
height: 600,
plugin: ['talk'],
handleError: function (err: any) {
console.log(err);
if (err?.data?.ret === 20020) {
// 20020 是并发连接限制的错误码
ElMessage.error('当前观看人数已达上限,请稍后再试');
}
}
});
}
});
}
// 关闭弹窗
function closeDialog() {
if (flvPlayer.value) {
flvPlayer.value.destroy().then((data: any) => {
console.log('promise 获取 数据', data);
});
flvPlayer.value = null;
}
isShowDialog.value = false;
}
// 获取列表
function busPresettingBitList() {
loading.value = true;
listDevicePreset(tableData.value.param).then((res: any) => {
tableData.value.data = res.rows ?? [];
tableData.value.total = res.total;
loading.value = false;
});
}
// 取消
function onCancel() {
closeDialog();
}
// 删除
function handleDelete(row: any) {
let msg = '你确定要删除所选数据?';
let id: number[] = [];
if (row) {
msg = '此操作将永久删除数据,是否继续?';
id = [row.id];
}
if (id.length === 0) {
ElMessage.error('请选择要删除的数据。');
return;
}
ElMessageBox.confirm(msg, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
const obj = {
deviceSerial: row.deviceSerial,
ids: id
};
delDevicePreset({
id: row.id,
deviceSerial: row.deviceSerial,
channelNo: "1",
presetIndex: row.presetIndex
}).then((res: any) => {
if (res.code === 200) {
ElMessage.success('删除成功');
busPresettingBitList();
}
});
})
.catch(() => { });
}
// 调用
function handleDebug(row: any) {
callDevicePreset([{
deviceSerial: row.deviceSerial,
presetIndex: row.presetIndex,
channelNo: "1",
id: row.id
}]).then((res: any) => {
if (res.code === 200) {
ElMessage.success('调用成功');
}
});
}
// 修改
function handleEdit(row: any) {
const param = {
id: row.id,
deviceSerial: row.deviceSerial,
presetName: row.presetName
};
updateDevicePreset(param)
.then(() => {
ElMessage.success('修改成功');
busPresettingBitList();
})
.finally(() => {
loading.value = false;
});
}
// 重置表单
function resetForm() {
formData.value = {
deviceSerial: undefined,
channelNo: '1',
presetName: undefined
};
}
onBeforeUnmount(() => {
if (flvPlayer.value) {
flvPlayer.value.destroy().then((data: any) => {
console.log('promise 获取 数据', data);
});
flvPlayer.value = null;
}
});
// ✅ 关键:暴露方法给父组件调用
defineExpose({
openDialog,
closeDialog
});
</script>
<style scoped lang="scss">
.system-busPresettingBit-add {
.info_list {
width: 100%;
display: flex;
height: 100%;
.video_box {
width: 75%;
height: 600px;
margin-right: 10px;
.iframe {
border: none;
outline: none;
}
.video_air {
width: 100%;
height: 100%;
object-fit: fill;
}
}
}
}
</style>

374
src/views/camera/index.vue Normal file
View File

@ -0,0 +1,374 @@
<template>
<div class="system-ys7Devices-container">
<el-card shadow="hover">
<div class="system-ys7Devices-search mb8">
<el-form :model="tableData.param" ref="queryRef" :inline="true" label-width="100px">
<el-row>
<el-col :span="8" class="colBlock">
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="tableData.param.deviceName" placeholder="请输入设备名称" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" class="colBlock">
<el-form-item label="设备类型" prop="deviceType">
<el-input v-model="tableData.param.deviceType" placeholder="请输入设备类型" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" :class="!showAll ? 'colBlock' : 'colNone'">
<el-form-item>
<el-button type="primary" @click="ys7DevicesList"><el-icon>
<Search />
</el-icon>搜索</el-button>
<el-button @click="resetQuery(queryRef)"><el-icon>
<Refresh />
</el-icon>重置</el-button>
<el-button type="primary" link @click="toggleSearch">
{{ word }}
<el-icon v-show="showAll">
<ArrowUp />
</el-icon>
<el-icon v-show="!showAll">
<ArrowDown />
</el-icon>
</el-button>
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="设备序列号" prop="deviceSerial">
<el-input v-model="tableData.param.deviceSerial" placeholder="请输入设备串号" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="状态" prop="status">
<el-select v-model="tableData.param.status" placeholder="请选择设备状态" clearable>
<el-option label="在线" :value="1" />
<el-option label="离线" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="设备版本" prop="deviceVersion">
<el-input v-model="tableData.param.deviceVersion" placeholder="请输入设备版本" clearable
@keyup.enter.native="ys7DevicesList" />
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item label="所属项目" prop="projectId">
<el-select v-model="tableData.param.projectId" placeholder="请选择所属项目" clearable
filterable>
<el-option v-for="item in projectList" class="device_row" :key="item.id"
:label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8" :class="showAll ? 'colBlock' : 'colNone'">
<el-form-item>
<el-button type="primary" @click="ys7DevicesList"><el-icon>
<Search />
</el-icon>搜索</el-button>
<el-button @click="resetQuery(queryRef)"><el-icon>
<Refresh />
</el-icon>重置</el-button>
<el-button type="primary" link @click="toggleSearch">
{{ word }}
<el-icon v-show="showAll">
<ArrowUp />
</el-icon>
<el-icon v-show="!showAll">
<ArrowDown />
</el-icon>
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-row :gutter="10" class="mb8">
<!-- <el-col :span="1.5">
<el-button type="primary" @click="handleAdd" v-auth="'api/v1/system/ys7Devices/add'"
><el-icon><Plus /></el-icon>新增</el-button
>
</el-col> -->
<!-- <el-col :span="1.5">
<el-button type="success" :disabled="single" @click="handleUpdate(null)"
v-auth="'api/v1/system/ys7Devices/edit'"><el-icon>
<Edit />
</el-icon>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" :disabled="multiple" @click="handleDelete(null)"
v-auth="'api/v1/system/ys7Devices/delete'"><el-icon>
<Delete />
</el-icon>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" :disabled="multiple" @click="onLinkProject(null)"
v-auth="'api/v1/system/ys7Devices/add'"><el-icon>
<Link />
</el-icon>设备分配</el-button>
</el-col>-->
</el-row>
</div>
<el-table v-loading="loading" :data="tableData.data" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="设备序列号" align="center" prop="deviceSerial" min-width="100px" />
<el-table-column label="设备名称" align="center" prop="deviceName" min-width="100px" />
<el-table-column label="设备类型" align="center" prop="deviceType" min-width="100px" />
<el-table-column label="状态" align="center" prop="status" min-width="100px">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status === 1">在线</el-tag>
<el-tag type="danger" v-if="scope.row.status === 0">离线</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="视频加密" align="center" prop="videoEncrypted" min-width="100px">
<template #default="scope">
<el-switch v-model="scope.row.videoEncrypted" class="ml-2" :active-value="1" :inactive-value="0"
:loading="scope.row.enctyptLoading" @change="encryptChange(scope.row)" inline-prompt
active-text="开启" inactive-text="关闭"
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" />
</template>
</el-table-column> -->
<!-- <el-table-column label="" align="center" prop="defence" min-width="100px" /> -->
<el-table-column label="设备版本" align="center" prop="deviceVersion" min-width="100px" />
<!-- <el-table-column label="所属项目" align="center" prop="projectId" min-width="100px">
<template #default="scope">
{{ scope.row.projectName ? scope.row.projectName : '未分配' }}
</template>
</el-table-column> -->
<!-- <el-table-column label="备注" align="center" prop="remark" min-width="100px" /> -->
<!-- <el-table-column label="创建时间" align="center" prop="deviceCreateTime" min-width="100px">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.deviceCreateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column> -->
<el-table-column label="操作" align="center" class-name="small-padding" min-width="160px" fixed="right">
<template #default="scope">
<!-- <el-button type="primary" link @click="handleUpdate(scope.row)"
v-auth="'api/v1/system/ys7Devices/edit'"><el-icon>
<EditPen />
</el-icon>修改</el-button>
<el-button type="primary" link @click="handleDelete(scope.row)"
v-auth="'api/v1/system/ys7Devices/delete'"><el-icon>
<DeleteFilled />
</el-icon>删除</el-button>
<el-button type="primary" link @click="onLinkProject(scope.row)"
v-auth="'api/v1/system/ys7Devices/delete'"><el-icon>
<Link />
</el-icon>设备分配</el-button> -->
<el-button type="primary" link @click="addPreset(scope.row)"><el-icon>
<Plus />
</el-icon>添加预置位</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="tableData.total > 0" :total="tableData.total" v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize" @pagination="ys7DevicesList" />
</el-card>
<presetAdd ref="presetAddRef"></presetAdd>
</div>
</template>
<script setup lang="ts">
import { ElMessageBox, ElMessage, FormInstance } from 'element-plus';
import { useUserStoreHook } from '@/store/modules/user';
import { getMonitoringList } from '@/api/securitySurveillance/index.js';
import presetAdd from './components/presetAdd.vue';
// proxy 获取
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// ref 定义
const loading = ref(false);
const queryRef = ref<FormInstance>();
const addRef = ref();
const editRef = ref();
const detailRef = ref();
const bindProRef = ref();
const presetAddRef = ref();
// 展开/收起搜索项
const showAll = ref(false);
const word = computed(() => (showAll.value ? '收起搜索' : '展开搜索'));
// 多选控制
const single = ref(true);
const multiple = ref(true);
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const projects = computed(() => userStore.projects);
// 状态管理
const state = reactive<any>({
ids: [],
serials: [],
tableData: {
data: [],
total: 0,
loading: false,
param: {
pageNum: 1,
pageSize: 10,
id: undefined,
createdAt: undefined,
deviceSerial: undefined,
deviceName: undefined,
deviceType: undefined,
status: undefined,
defence: undefined,
deviceVersion: undefined,
projectId: currentProject.value?.id,
dateRange: [],
isFront: false
}
},
projectList: projects.value
});
// 初始化
const initTableData = () => {
ys7DevicesList();
// sysProjectList();
};
// 搜索重置
const resetQuery = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
ys7DevicesList();
};
// 获取设备列表
const ys7DevicesList = () => {
loading.value = true;
getMonitoringList({
pageStart: state.tableData.param.pageNum,
pageSize: state.tableData.param.pageSize,
isflow: false
}).then((res: any) => {
let list = res.data.object ?? [];
state.tableData.data = list.map((item) => {
item.enctyptLoading = false;
return item;
});
state.tableData.total = Number(res.data.sum);
console.log(state.tableData);
loading.value = false;
});
};
// 展开/收起搜索项
const toggleSearch = () => {
showAll.value = !showAll.value;
};
// 多选事件
const handleSelectionChange = (selection: any[]) => {
state.ids = selection.map((item) => item.id);
state.serials = selection.map((item) => item.deviceSerial);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
// 新增
// const handleAdd = () => {
// addRef.value.openDialog();
// };
// // 编辑
// const handleUpdate = (row?: Ys7DeviceVO) => {
// if (!row) {
// row = state.tableData.data.find((item) => item.id === state.ids[0])!;
// }
// editRef.value.openDialog(toRaw(row));
// };
// 删除
// const handleDelete = (row?: any) => {
// let msg = row ? `此操作将永久删除数据,是否继续?` : '你确定要删除所选数据?';
// let id = row ? [row.id] : state.ids;
// if (id.length === 0) {
// ElMessage.error('请选择要删除的数据。');
// return;
// }
// ElMessageBox.confirm(msg, '提示', {
// confirmButtonText: '确认',
// cancelButtonText: '取消',
// type: 'warning'
// })
// .then(() => {
// delYs7Device(id).then(() => {
// ElMessage.success('删除成功');
// ys7DevicesList();
// });
// })
// .catch(() => { });
// };
// 绑定项目
// const onLinkProject = (row?: Ys7DeviceVO) => {
// let serials = row ? [row.deviceSerial] : state.ids;
// if (serials.length === 0) {
// ElMessage.error('请选择要绑定项目的设备');
// return;
// }
// let info = { serials, row };
// bindProRef.value.openDialog(toRaw(info));
// };
// 添加预置位
const addPreset = (row: any) => {
presetAddRef.value.openDialog(row);
};
// 开关加密
// const encryptChange = (row: any) => {
// row.enctyptLoading = true;
// // const action = row.videoEncrypted === 0 ? 1 : 0;
// console.log(row.videoEncrypted);
// toggleEncrypt({ videoEncrypted: row.videoEncrypted, id: row.id })
// .then(() => {
// proxy?.$modal.msgSuccess(row.videoEncrypted === 0 ? '关闭成功' : '开启成功');
// })
// .finally(() => {
// row.enctyptLoading = false;
// ys7DevicesList();
// });
// };
//监听项目id刷新数据
// const listeningProject = watch(
// () => currentProject.value?.id,
// (nid, oid) => {
// tableData.value.param.projectId = nid;
// initTableData();
// }
// );
// 页面加载
onMounted(() => {
initTableData();
});
// onUnmounted(() => {
// listeningProject();
// });
// 暴露变量
const { tableData, projectList } = toRefs(state);
</script>
<style lang="scss" scoped>
.colBlock {
display: block;
}
.colNone {
display: none;
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<div class="chart-container">
<!--组件温度 图表内容区域 -->
<div ref="chartRef" class="chart-content"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts';
// 定义props类型
interface TrendSeriesItem {
name: string;
data: number[];
color: string;
}
interface TrendData {
dates: string[];
series: TrendSeriesItem[];
}
// 定义props
const props = defineProps<{
trendData: TrendData;
}>();
// 图表DOM引用
const chartRef = ref(null);
// 图表实例
let chartInstance = null;
// 初始化图表
const initChart = () => {
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const option = {
xAxis: {
type: "category",
data: props.trendData.dates,
axisTick: {
show: false // 去除刻度线
}
},
yAxis: {
type: "value",
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
},
legend: {
show: true,
icon: 'square',
left: '2%',
itemWidth: 10,
itemHeight: 10,
itemAlign: 'middle', // 设置图例项垂直居中
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
series: props.trendData.series.map((item, index) => ({
name: item.name,
data: item.data,
type: "bar",
barWidth: '10%' ,
itemStyle: {
color: item.color,
},
})),
};
chartInstance.setOption(option);
};
// 响应窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 监听props变化
watch(() => props.trendData, () => {
if (chartInstance) {
initChart();
}
}, { deep: true });
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
};
});
</script>
<style scoped>
.chart-container {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
height: 400px;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
}
.chart-header h2 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-content {
width: 100%;
height: calc(100% - 54px);
padding: 10px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.chart-container {
height: 350px;
}
}
@media (max-width: 480px) {
.chart-container {
height: 300px;
}
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.chart-actions {
width: 100%;
display: flex;
justify-content: space-between;
}
.chart-actions button {
margin: 0;
flex: 1;
margin-right: 5px;
}
.chart-actions button:last-child {
margin-right: 0;
}
}
.model {
padding: 20px;
background-color: rgba(242, 248, 252, 1);
}
</style>

View File

@ -0,0 +1,219 @@
<template>
<div class="chart-container">
<!--组件温度 图表内容区域 -->
<div ref="chartRef" class="chart-content"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts';
// 定义props类型
interface PieItem {
value: number;
name: string;
displayName: string;
color: string;
}
interface PieData {
normal: PieItem;
interrupt: PieItem;
abnormal: PieItem;
serious: PieItem;
}
// 定义props
const props = defineProps<{
pieData: PieData;
}>();
// 图表DOM引用
const chartRef = ref(null);
// 图表实例
let chartInstance = null;
// 初始化图表
const initChart = () => {
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const option = {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `${params.data.displayName}: ${params.value}`;
}
},
grid: {
left: '0%',
right: '20%',
bottom: '0%',
top: '0%',
containLabel: true
},
legend: {
top: 'middle',
orient: 'vertical',
right: '5%', // 调整图例位置,使其更靠近左侧
itemWidth: 15,
itemHeight: 15,
formatter: (name: string) => {
const item = Object.values(props.pieData).find(item => item.name === name);
return item?.displayName || name;
}
},
series: [
{
type: 'pie',
radius: '80%',
label: {
show: false
},
color: [
props.pieData.normal.color,
props.pieData.interrupt.color,
props.pieData.abnormal.color,
props.pieData.serious.color
],
data: [
{
value: props.pieData.normal.value,
name: props.pieData.normal.name,
displayName: props.pieData.normal.displayName
},
{
value: props.pieData.interrupt.value,
name: props.pieData.interrupt.name,
displayName: props.pieData.interrupt.displayName
},
{
value: props.pieData.abnormal.value,
name: props.pieData.abnormal.name,
displayName: props.pieData.abnormal.displayName
},
{
value: props.pieData.serious.value,
name: props.pieData.serious.name,
displayName: props.pieData.serious.displayName
}
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
chartInstance.setOption(option);
};
// 响应窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 监听props变化
watch(() => props.pieData, () => {
if (chartInstance) {
initChart();
}
}, { deep: true });
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
};
});
</script>
<style scoped>
.chart-container {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
height: 150px;
width: 100%;
padding: 5px;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
}
.chart-header h2 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-content {
width: 100%;
height: 100%;
padding: 5px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.chart-container {
height: 350px;
}
}
@media (max-width: 480px) {
.chart-container {
height: 300px;
}
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.chart-actions {
width: 100%;
display: flex;
justify-content: space-between;
}
.chart-actions button {
margin: 0;
flex: 1;
margin-right: 5px;
}
.chart-actions button:last-child {
margin-right: 0;
}
}
.model {
padding: 20px;
background-color: rgba(242, 248, 252, 1);
}
</style>

View File

@ -0,0 +1,352 @@
<template>
<el-table :data="localAlarmLevels" :border="false" style="width: 100%">
<el-table-column prop="levelName" label="级别名称" align="center">
<template #default="scope">
<span :class="['level-name', `level-${scope.row.level}`]">{{ scope.row.levelName }}</span>
</template>
</el-table-column>
<el-table-column prop="description" label="标识含义" align="center"></el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="scope">
<el-tag :type="getPriorityType(scope.row.priority)">{{ scope.row.priority }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="responseTime" label="响应时间" align="center">
<template #default="scope">
<span style="color: #186DF5;">{{ scope.row.responseTime }}</span>
</template>
</el-table-column>
<el-table-column prop="processingMethod" label="处理方式" align="center">
<template #default="scope">
<div class="process-methods">
<el-tag size="small" v-for="method in scope.row.processingMethod" :key="method" :type="getMethodType(method)">{{ method }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="enabled" label="是否启用" width="100" align="center">
<template #default="scope">
<el-switch v-model="scope.row.enabled" active-color="#13ce66" inactive-color="#ff4949" @change="handleEnabledChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="scope">
<el-button link type="primary" @click="handleConfig(scope.row)">配置</el-button>
<el-button link type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 配置对话框 -->
<el-dialog v-model="configDialogVisible" title="告警配置" width="600px">
<div v-if="currentConfigData">
<h3 class="config-title">{{ currentConfigData.levelName }} - 详细配置</h3>
<el-form ref="configFormRef" :model="currentConfigData" label-width="120px">
<el-form-item label="告警声音">
<el-select v-model="currentConfigData.alarmSound" placeholder="请选择告警声音">
<el-option label="默认声音" value="default" />
<el-option label="紧急声音" value="urgent" />
<el-option label="普通声音" value="normal" />
</el-select>
</el-form-item>
<el-form-item label="通知方式">
<el-checkbox-group v-model="currentConfigData.notificationMethods">
<el-checkbox label="短信" />
<el-checkbox label="邮件" />
<el-checkbox label="站内信" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="告警持续时间">
<el-input-number v-model="currentConfigData.duration" :min="1" :max="60" label="分钟" />
</el-form-item>
<el-form-item label="自动处理">
<el-switch v-model="currentConfigData.autoProcess" />
</el-form-item>
<el-form-item label="处理说明" v-if="currentConfigData.autoProcess">
<el-input v-model="currentConfigData.processDescription" type="textarea" placeholder="请输入自动处理说明" />
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="configDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfigSave">保存配置</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
// 定义告警等级类型
interface AlarmLevel {
id: number;
levelName: string;
description: string;
priority: string;
responseTime: string;
processingMethod: string[];
enabled: boolean;
level: number; // 用于样式区分
}
// 定义配置数据类型
interface ConfigData extends AlarmLevel {
alarmSound: string;
notificationMethods: string[];
duration: number;
autoProcess: boolean;
processDescription: string;
}
// 定义props
const props = defineProps<{
alarmLevels: AlarmLevel[];
}>();
// 本地数据副本
const localAlarmLevels = ref<AlarmLevel[]>([]);
// 初始化本地数据
watch(() => props.alarmLevels, (newVal) => {
// 深拷贝以避免直接修改props
localAlarmLevels.value = JSON.parse(JSON.stringify(newVal));
}, { immediate: true, deep: true });
// 对话框相关状态
const configDialogVisible = ref(false);
const configFormRef = ref<any>();
const currentConfigData = ref<ConfigData | null>(null);
// 获取优先级对应的标签类型
const getPriorityType = (priority: string) => {
const priorityMap: Record<string, string> = {
'一级': 'danger',
'二级': 'warning',
'三级': 'success',
'四级': 'primary'
};
return priorityMap[priority] || 'default';
};
// 获取处理方式对应的标签类型
const getMethodType = (method: string) => {
const methodMap: Record<string, string> = {
'系统锁定': 'danger',
'声光报警': 'warning',
'短信通知': 'primary',
'邮件通知': 'info',
'系统记录': 'success'
};
return methodMap[method] || 'info';
};
// 处理启用状态变更
const handleEnabledChange = (row: AlarmLevel) => {
ElMessage.success(`${row.levelName} ${row.enabled ? '已启用' : '已禁用'}`);
// 这里可以添加保存到后端的逻辑
};
// 打开配置对话框
const handleConfig = (row: AlarmLevel) => {
// 构建配置数据
currentConfigData.value = {
...row,
alarmSound: 'default',
notificationMethods: ['短信'],
duration: 30,
autoProcess: false,
processDescription: ''
};
configDialogVisible.value = true;
};
// 保存配置
const handleConfigSave = () => {
if (currentConfigData.value) {
// 找到对应的告警等级并更新
const index = localAlarmLevels.value.findIndex(item => item.id === currentConfigData.value!.id);
if (index !== -1) {
localAlarmLevels.value[index] = {
...localAlarmLevels.value[index],
enabled: currentConfigData.value!.enabled
};
}
ElMessage.success('配置保存成功');
configDialogVisible.value = false;
}
};
// 删除告警等级
const handleDelete = (id: number) => {
ElMessageBox.confirm('确定要删除该告警等级吗?', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = localAlarmLevels.value.findIndex(item => item.id === id);
if (index !== -1) {
localAlarmLevels.value.splice(index, 1);
ElMessage.success('删除成功');
}
}).catch(() => {
// 用户取消删除
});
};
</script>
<style scoped lang="scss">
.level-set-container {
padding: 20px;
width: 100%;
height: 100%;
box-sizing: border-box;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.level-name {
font-weight: 500;
padding: 2px 6px 2px 18px;
border-radius: 3px;
transition: all 0.3s ease;
position: relative;
}
.level-name::before {
content: '';
position: absolute;
left: 4px;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
}
.level-1::before {
background-color: #ff4949;
}
.level-2::before {
background-color: #f7ba1e;
}
.level-3::before {
background-color: #13ce66;
}
.level-4::before {
background-color: #1890ff;
}
.level-name:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.level-1 {
color: #ff4949;
}
.level-2 {
color: #f7ba1e;
}
.level-3 {
color: #13ce66;
}
.level-4 {
color: #1890ff;
}
.process-methods {
display: flex;
gap: 6px;
flex-wrap: wrap;
padding: 4px 0;
}
/* 优化表格样式 */
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.el-table th) {
background-color: #fafafa;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #ebeef5;
}
:deep(.el-table tr:hover > td) {
background-color: #f0f9ff !important;
}
:deep(.el-table__row:nth-child(even)) {
background-color: #fafafa;
}
/* 优化按钮和操作列 */
:deep(.el-button--text) {
transition: all 0.3s ease;
padding: 4px 12px;
border-radius: 4px;
}
:deep(.el-button--text:hover) {
background-color: rgba(0, 0, 0, 0.05);
}
/* 优化对话框样式 */
.config-title {
margin-bottom: 20px;
color: #303133;
font-size: 16px;
font-weight: 500;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 优化表单样式 */
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-form-item__label) {
color: #606266;
font-weight: 500;
}
:deep(.el-select),
:deep(.el-input),
:deep(.el-input-number) {
width: 100%;
}
/* 响应式调整 */
@media (max-width: 768px) {
.level-set-container {
padding: 15px;
}
:deep(.el-table) {
font-size: 12px;
}
}
</style>

View File

@ -0,0 +1,339 @@
<template>
<div class="total-view-dashboard">
<!-- 今日报警总数 -->
<div class="total-view-card blue-border">
<div class="total-content">
<div class="content-row">
<div class="left-section">
<div class="total-header">
<span class="total-title">今日报警总数</span>
</div>
<div class="total-number">{{ totalData.totalAlarm }}</div>
</div>
<div class="icon-section">
<el-icon class="total-icon blue">
<img src="@/assets/demo/health.png" alt="">
</el-icon>
</div>
</div>
<div class="total-comparison">
<el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升">
</el-icon>
<span class="comparison-text green">+{{ totalData.totalIncrease }}</span>
<span class="period-text">较上月同期</span>
</div>
</div>
</div>
<!-- 未处理报警 -->
<div class="total-view-card purple-border">
<div class="total-content">
<div class="content-row">
<div class="left-section">
<div class="total-header">
<span class="total-title">未处理报警</span>
</div>
<div class="total-number">{{ totalData.unprocessedAlarm }}</div>
</div>
<div class="icon-section">
<el-icon class="total-icon purple">
<img src="@/assets/demo/sms-tracking.png" alt="">
</el-icon>
</div>
</div>
<div class="total-comparison">
<el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升">
</el-icon>
<span class="comparison-text green">+{{ totalData.unprocessedIncrease }}</span>
<span class="period-text">较上月同期</span>
</div>
</div>
</div>
<!-- 已处理报警 -->
<div class="total-view-card green-border">
<div class="total-content">
<div class="content-row">
<div class="left-section">
<div class="total-header">
<span class="total-title">已处理报警</span>
</div>
<div class="total-number">{{ totalData.processedAlarm }}</div>
</div>
<div class="icon-section">
<el-icon class="total-icon green">
<img src="@/assets/demo/archive.png" alt="">
</el-icon>
</div>
</div>
<div class="total-comparison">
<el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升">
</el-icon>
<span class="comparison-text green">+{{ totalData.processedIncrease }}</span>
<span class="period-text">较上月同期</span>
</div>
</div>
</div>
<!-- 严重报警 -->
<div class="total-view-card orange-border">
<div class="total-content">
<div class="content-row">
<div class="left-section">
<div class="total-header">
<span class="total-title">严重报警</span>
</div>
<div class="total-number">{{ totalData.seriousAlarm }}</div>
</div>
<div class="icon-section">
<el-icon class="total-icon orange">
<img src="@/assets/demo/mouse-square.png" alt="">
</el-icon>
</div>
</div>
<div class="total-comparison">
<el-icon class="trend-icon green">
<img src="/src/assets/demo/up.png" alt="上升">
</el-icon>
<span class="comparison-text green">+{{ totalData.seriousIncrease }}</span>
<span class="period-text">较上月同期</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
// 定义props类型
interface TotalData {
totalAlarm: number;
unprocessedAlarm: number;
processedAlarm: number;
seriousAlarm: number;
totalIncrease: number;
unprocessedIncrease: number;
processedIncrease: number;
seriousIncrease: number;
}
// 定义props
const props = defineProps<{
totalData: TotalData;
}>();
</script>
<style scoped lang="scss">
.total-view-dashboard {
display: flex;
gap: 16px;
width: 100%;
flex-wrap: wrap;
}
.total-view-card {
display: flex;
align-items: center;
padding: 20px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
position: relative;
flex: 1;
min-width: 200px;
height: 150px;
transition: all 0.3s ease;
overflow: hidden;
}
.total-view-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 左侧边框样式 - 使用伪元素创建与指定内容高度一致的边框 */
.total-view-card::before {
content: '';
position: absolute;
left: 0;
top: 42px;
width: 4px;
height: 45px;
border-radius: 0 2px 2px 0;
transition: height 0.3s ease;
}
.total-view-card:hover::before {
height: 80px;
}
.blue-border::before {
background-color: #0080FC;
}
.blue-border {
background-color: #EAF5FF;
}
/* 添加卡片背景渐变效果 */
.total-view-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.3) 100%);
pointer-events: none;
}
.purple-border::before {
background-color: #722ed1;
}
.purple-border {
background-color: #F3EDFF;
}
.green-border::before {
background-color: #009B72;
}
.green-border {
background-color: #E8FFF9;
}
.orange-border::before {
background-color: #fa8c16;
}
.orange-border {
background-color: #FFF6EC;
}
.total-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.content-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.left-section {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.icon-section {
display: flex;
align-items: center;
justify-content: center;
margin-left: 12px;
}
.total-header {
display: flex;
align-items: center;
}
.total-title {
font-size: 14px;
color: #606266;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.total-icon {
width: 40px;
height: 40px;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.total-icon.blue {
background-color: #DBEEFF;
color: #1890ff;
}
.total-icon.purple {
background-color: #E9DEFF;
color: #722ed1;
}
.total-icon.green {
background-color: #CEFFF2;
color: #52c41a;
}
.total-icon.orange {
background-color: #FFEBD3;
color: #fa8c16;
}
.total-number {
font-size: 18px;
font-weight: 600;
color: #303133;
line-height: 1;
}
.total-comparison {
display: flex;
align-items: center;
gap: 8px;
height: 16px;
}
.trend-icon {
width: 16px;
height: 16px;
}
.trend-icon.green {
color: #52c41a;
}
.comparison-text {
font-size: 12px;
}
.comparison-text.green {
color: #52c41a;
}
.period-text {
font-size: 12px;
color: #909399;
}
@media screen and (max-width: 1200px) {
.total-view-dashboard {
flex-wrap: wrap;
}
.total-view-card {
flex: 0 0 calc(50% - 8px);
}
}
@media screen and (max-width: 768px) {
.total-view-card {
flex: 0 0 100%;
}
}
</style>

View File

@ -0,0 +1,210 @@
<template>
<div class="model">
<!-- 标题栏 -->
<el-row>
<el-col :span="12">
<TitleComponent title="报警管理" subtitle="配置新能源厂站的报警级别、类型及相关规则" />
</el-col>
</el-row>
<!-- 第一行报警管理和报警级别分布 -->
<el-row :gutter="20" class="content-row">
<el-col :span="16">
<el-card shadow="hover" class="custom-card">
<TitleComponent title="报警管理" :font-level="2" />
<totalView :totalData="totalData" />
</el-card>
</el-col>
<el-col :span="8">
<!-- 报警级别分布 -->
<el-card shadow="hover" class="custom-card">
<TitleComponent title="报警级别分布" :font-level="2" />
<levelPie :pieData="pieData" />
</el-card>
</el-col>
</el-row>
<!-- 第二行报警趋势分析 -->
<el-row :gutter="20" class="content-row">
<el-col :span="24">
<el-card shadow="hover" class="custom-card">
<TitleComponent title="报警趋势分析" :font-level="2" />
<fenxiBar :trendData="trendData" />
</el-card>
</el-col>
</el-row>
<!-- 第三行报警级别设置 -->
<el-row :gutter="20" class="content-row">
<el-col :span="24">
<el-card shadow="hover" class="custom-card">
<TitleComponent title="报警级别设置" :font-level="2" />
<levelSet :alarmLevels="alarmLevelsData" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue';
import TitleComponent from '@/components/TitleComponent/index.vue';
import levelPie from '@/views/integratedManage/alarmManage/components/levelPie.vue'
import fenxiBar from '@/views/integratedManage/alarmManage/components/fenxiBar.vue'
import totalView from '@/views/integratedManage/alarmManage/components/totalView.vue';
import levelSet from '@/views/integratedManage/alarmManage/components/levelSet.vue';
// 模拟报警总数数据
const totalData = ref({
totalAlarm: 28,
unprocessedAlarm: 8,
processedAlarm: 20,
seriousAlarm: 3,
totalIncrease: 8,
unprocessedIncrease: 3,
processedIncrease: 5,
seriousIncrease: 1
});
// 模拟报警级别分布数据
const pieData = ref({
normal: {
value: 1048,
name: '提示信息',
displayName: '提示信息',
color: 'rgb(0, 179, 255)'
},
interrupt: {
value: 735,
name: '一般告警',
displayName: '一般告警',
color: 'rgb(45, 214, 131)'
},
abnormal: {
value: 580,
name: '重要告警',
displayName: '重要告警',
color: 'rgb(255, 208, 35)'
},
serious: {
value: 484,
name: '严重告警',
displayName: '严重告警',
color: 'rgb(227, 39, 39)'
}
});
// 模拟报警趋势数据
const trendData = ref({
dates: ['09-04', '09-05', '09-06', '09-07', '09-08', '09-09', '09-10'],
series: [
{
name: '维护提醒',
data: [120, 200, 150, 80, 70, 110, 130],
color: 'rgb(0, 179, 255)'
},
{
name: '数据异常',
data: [80, 170, 100, 50, 90, 140, 170],
color: 'rgb(22, 93, 255)'
},
{
name: '信号减弱',
data: [60, 140, 100, 120, 110, 100, 130],
color: 'rgb(255, 153, 0)'
},
{
name: '温度过高',
data: [60, 140, 100, 120, 110, 100, 130],
color: 'rgb(250, 220, 25)'
},
{
name: '通讯中断',
data: [60, 140, 100, 120, 110, 100, 130],
color: 'rgb(251, 62, 122)'
}
]
});
// 模拟告警级别设置数据
const alarmLevelsData = ref([
{
id: 1,
levelName: '严重告警',
description: '系统或应用出现严重故障',
priority: '一级',
responseTime: '15分钟以内',
processingMethod: ['系统锁定', '声光报警', '短信通知'],
enabled: true,
level: 1
},
{
id: 2,
levelName: '重要告警',
description: '系统或应用出现严重故障',
priority: '二级',
responseTime: '30分钟以内',
processingMethod: ['声光报警', '短信通知', '系统记录'],
enabled: true,
level: 2
},
{
id: 3,
levelName: '一般告警',
description: '非关键性故障或潜在风险',
priority: '三级',
responseTime: '120分钟以内',
processingMethod: ['短信通知', '系统记录'],
enabled: true,
level: 3
},
{
id: 4,
levelName: '提示信息',
description: '系统或应用非关键性变化或即将达到阈值的状态',
priority: '四级',
responseTime: '24小时以内',
processingMethod: ['短信通知'],
enabled: false,
level: 4
}
]);
</script>
<style scoped>
.model {
padding: 20px 15px;
background-color: rgba(242, 248, 252, 1);
}
.content-row {
margin-bottom: 20px;
}
.custom-card {
border-radius: 8px;
transition: all 0.3s ease;
border: none;
}
.custom-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 响应式布局调整 */
@media (max-width: 1200px) {
.content-row {
margin-bottom: 15px;
}
}
@media (max-width: 768px) {
.model {
padding: 15px 10px;
}
.content-row {
margin-bottom: 10px;
}
}
</style>

View File

@ -0,0 +1,327 @@
<template>
<div class="chart-container">
<!-- 图表标题和时间范围选择器 -->
<div class="chart-header">
<h2>出勤趋势分析</h2>
<div class="chart-actions">
<button @click="timeRange = 'week'" :class="{ active: timeRange === 'week' }">每周</button>
<button @click="timeRange = 'month'" :class="{ active: timeRange === 'month' }">每月</button>
</div>
</div>
<!-- 图表内容区域 -->
<div ref="chartRef" class="chart-content"></div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import * as echarts from 'echarts';
// 接收从父组件传入的数据
const props = defineProps({
attendData: {
type: Object,
default: () => ({
week: {
xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
actualCount: [40, 20, 30, 15, 22, 63, 58],
expectedCount: [100, 556, 413, 115, 510, 115, 317]
},
month: {
xAxis: ['第1周', '第2周', '第3周', '第4周'],
actualData: [280, 360, 320, 400],
theoreticalData: [300, 400, 350, 450]
}
})
}
});
// 图表DOM引用
const chartRef = ref(null);
// 图表实例
let chartInstance = null;
// 时间范围状态
const timeRange = ref('week');
// 根据时间范围计算当前显示的数据
const chartData = computed(() => {
const dataForRange = props.attendData[timeRange.value] || props.attendData.week;
// 处理字段名称差异
if (timeRange.value === 'week') {
return {
xAxis: dataForRange.xAxis || [],
actualCount: dataForRange.actualCount || [],
expectedCount: dataForRange.expectedCount || []
};
} else {
return {
xAxis: dataForRange.xAxis || [],
actualCount: dataForRange.actualData || [],
expectedCount: dataForRange.theoreticalData || []
};
}
});
// 定义颜色常量
const ACTUAL_COUNT_COLOR = '#029CD4'; // 蓝色 - 实际人数
const EXPECTED_COUNT_COLOR = '#0052D9'; // 蓝色 - 应出勤人数
// 初始化图表
const initChart = () => {
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
// 使用计算后的数据
const { xAxis, actualCount, expectedCount } = chartData.value;
const option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,1)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 14
},
formatter: function(params) {
const actualCount = params[0].value;
const expectedCount = params[1].value;
return `
<div style="padding: 5px;">
<div style="color: ${params[0].color};">实际人数: ${actualCount}</div>
<div style="color: ${params[1].color};">应出勤人数: ${expectedCount}</div>
</div>
`;
}
},
legend: {
top: 30,
left: 'center',
itemWidth: 10,
itemHeight: 10,
itemGap: 25,
data: ['实际人数', '应出勤人数'],
textStyle: {
color: '#666',
fontSize: 12
}
},
grid: {
top: '30%',
right: '10%',
bottom: '10%',
left: '6%',
containLabel: true
},
xAxis: {
data: xAxis,
type: 'category',
boundaryGap: true,
axisLabel: {
textStyle: {
color: '#666',
fontSize: 12
}
},
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#ddd'
}
}
},
yAxis: [
{
type: 'value',
name: '人数',
nameTextStyle: {
color: '#666',
fontSize: 12
},
interval: 100,
axisLabel: {
textStyle: {
color: '#666',
fontSize: 12
}
},
axisTick: {
show: false
},
axisLine: {
show: false
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
}
],
series: [
{
name: '实际人数',
type: 'bar',
barWidth: '40%',
itemStyle: {
color: ACTUAL_COUNT_COLOR
},
data: actualCount
},
{
name: '应出勤人数',
type: 'line',
showSymbol: false,
symbol: 'circle',
symbolSize: 6,
emphasis: {
showSymbol: true,
symbolSize: 10
},
lineStyle: {
width: 2,
color: EXPECTED_COUNT_COLOR
},
itemStyle: {
color: EXPECTED_COUNT_COLOR,
borderColor: '#fff',
borderWidth: 2
},
data: expectedCount
}
]
};
chartInstance.setOption(option);
};
// 响应窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 监听时间范围变化,更新图表
watch(timeRange, () => {
initChart();
});
// 监听数据变化,更新图表
watch(() => props.attendData, () => {
initChart();
}, { deep: true });
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
};
});
</script>
<style scoped>
.chart-container {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
height: 435px;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
}
.chart-header h2 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-actions button {
background: none;
border: 1px solid #e0e0e0;
padding: 5px 12px;
border-radius: 4px;
margin-left: 8px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.chart-actions button.active {
background-color: #1890ff;
color: white;
border-color: #1890ff;
}
.chart-content {
width: 100%;
height: calc(100% - 54px);
padding: 10px;
}
@media (max-width: 768px) {
.chart-container {
height: 435px;
}
}
@media (max-width: 480px) {
.chart-container {
height: 400px;
}
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.chart-actions {
width: 100%;
display: flex;
justify-content: space-between;
}
.chart-actions button {
margin: 0;
flex: 1;
margin-right: 5px;
}
.chart-actions button:last-child {
margin-right: 0;
}
}
.model {
padding: 20px;
background-color: rgba(242, 248, 252, 1);
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<div class="box">
<div class="total">
<div class="infoBox">
<div class="date text-color">2025-08-26</div>
<div class="temperature text-color">28</div>
<div class="role text-color">中午好管理员</div>
<div class="cycle text-color">加入项目已经89天</div>
</div>
<img src="@/assets/demo/icTicket.png" alt="" class="imgbox">
</div>
</div>
</template>
<style scoped lang="scss">
.total {
width: 100%;
position: relative;
overflow: hidden;
.imgbox {
position: absolute;
top: 60px;
left: 210px;
}
.infoBox {
height: 217px;
border-radius: 12px;
padding: 30px;
background: rgba(24, 109, 245, 1);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
.text-color {
color: rgba(255, 255, 255, 1);
}
.date {
font-size: 16px;
}
.temperature {
font-weight: 600;
font-size: 28px;
}
.role {
font-size: 24px;
}
.cycle {
font-size: 16px;
}
}
}
</style>

View File

@ -0,0 +1,290 @@
<template>
<!-- 人员排班弹窗 -->
<el-dialog :model-value="manageAttendDialogVisible" @update:model-value="handleDialogVisibleChange" title="管理考勤"
width="500">
<!-- 添加表单引用和校验规则 -->
<el-form ref="attendFormRef" :model="attendForm" :rules="formRules" label-width="100px">
<el-form-item label="选择日期" prop="schedulingDate">
<el-date-picker v-model="attendForm.schedulingDate" type="date" placeholder="选择日期" style="width: 100%;"
:disabled-date="(time) => time.getTime() < Date.now() - 8.64e7" :date-format="'yyyy-MM-dd'"
value-format="YYYY-MM-DD" />
</el-form-item>
<!-- 排班类型为空提示 -->
<div v-if="shiftTypes.length === 0" class="empty-tip">
<el-alert title="暂无排班类型请先添加排班类型" type="warning" show-icon :closable="false" />
</div>
<!-- 排班人员为空提示 -->
<div v-if="props.personnelList.length === 0" class="empty-tip">
<el-alert title="暂无排班人员请先添加排班人员" type="warning" show-icon :closable="false" />
</div>
<!-- 动态排班表单 -->
<!-- 动态排班表单 -->
<div v-for="(item, index) in attendForm.userTypeBos" :key="index" class="dynamic-shift-item">
<el-form-item :label="index === 0 ? '排班设置' : ''" :required="index === 0">
<div class="shift-form-row">
<!-- 排班类型选择 -->
<el-select v-model="item.schedulingType" placeholder="请选择排班类型" style="width: 40%; margin-right: 10px;"
filterable :validate-event="false">
<!-- 使用完整的shiftTypes列表以确保已选项目也能正确显示label -->
<el-option v-for="option in shiftTypes" :key="option.value" :label="option.label" :value="option.value"
:disabled="attendForm.userTypeBos.some((bosItem) => bosItem.schedulingType === option.value && bosItem !== item)"></el-option>
</el-select>
<!-- 人员选择 -->
<el-select v-model="item.opsUserId" placeholder="请选择人员" style="width: 50%; margin-right: 10px;" multiple
filterable :validate-event="false">
<el-option v-for="person in props.personnelList" :key="person.value" :label="person.label"
:value="person.value"></el-option>
</el-select>
<!-- 删除按钮 (仅在不是第一个项目时显示) -->
<el-button v-if="index > 0" type="danger" icon="CircleCloseFilled" circle
@click="removeShiftItem(index)"></el-button>
</div>
</el-form-item>
</div>
<!-- 添加排班类型按钮 -->
<el-form-item>
<el-button type="primary" icon="CirclePlusFilled" @click="addShiftItem"
:disabled="attendForm.userTypeBos.length >= shiftTypes.length">
添加排班类型
</el-button>
<div v-if="attendForm.userTypeBos.length > 0" class="form-tip">
提示:已添加 {{ attendForm.userTypeBos.length }}/{{ shiftTypes.length }} 种排班类型
</div>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleConfirm"
:disabled="shiftTypes.length === 0 || props.personnelList.length === 0">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
// 定义组件的props
const props = defineProps({
manageAttendDialogVisible: {
type: Boolean,
default: false
},
// 排班人员列表数据
personnelList: {
type: Array,
default: () => []
},
// 排班类型列表
typeList: {
type: Array,
default: () => []
}
});
// 定义组件的emits
const emit = defineEmits<{
'update:manageAttendDialogVisible': [value: boolean];
'confirm': [formData: any];
}>();
// 排班类型列表从传入的typeList派生
const shiftTypes = ref((props.typeList || []).map(item => ({
label: (item as { schedulingName: string }).schedulingName,
value: (item as { id: any }).id
})));
// 导入表单相关类型和消息组件
import type { FormInstance, FormRules } from 'element-plus';
import { ElMessage } from 'element-plus';
// 表单引用
const attendFormRef = ref<FormInstance>();
// 考勤表单数据
const attendForm = ref({
schedulingDate: '',
userTypeBos: [
{ schedulingType: '', opsUserId: [] }
]
});
// 表单校验规则
const formRules = ref<FormRules>({
schedulingDate: [
{ required: true, message: '请选择日期', trigger: 'change' }
]
});
// 监听typeList变化更新shiftTypes
watch(() => props.typeList, (newTypeList) => {
shiftTypes.value = (newTypeList || []).map(item => ({
label: (item as { schedulingName: string }).schedulingName,
value: (item as { id: any }).id
}));
// 当没有排班类型时,清空排班项;有排班类型但没有排班项时,添加一个空排班项
if (shiftTypes.value.length === 0) {
attendForm.value.userTypeBos = [];
} else if (attendForm.value.userTypeBos.length === 0) {
attendForm.value.userTypeBos = [{ schedulingType: '', opsUserId: [] }];
}
}, { deep: true });
// 添加新的排班类型项
const addShiftItem = () => {
// 检查是否还有可用的排班类型
if (attendForm.value.userTypeBos.length < shiftTypes.value.length) {
attendForm.value.userTypeBos.push({ schedulingType: '', opsUserId: [] });
// 现在不再需要为新添加的项添加监听器
}
};
// 删除排班类型项
const removeShiftItem = (index: number) => {
if (attendForm.value.userTypeBos.length > 1) {
attendForm.value.userTypeBos.splice(index, 1);
// 不再需要更新可用选项
}
};
// 处理取消
const handleCancel = () => {
emit('update:manageAttendDialogVisible', false);
resetForm();
};
// 处理弹窗可见性变化
const handleDialogVisibleChange = (newVisible: boolean) => {
emit('update:manageAttendDialogVisible', newVisible);
if (!newVisible) {
resetForm();
}
};
// 处理确认
const handleConfirm = async () => {
if (!attendFormRef.value) return;
try {
// 验证日期
await attendFormRef.value.validateField('schedulingDate');
// 验证每个排班项
let isValid = true;
const validationPromises: Promise<void>[] = [];
attendForm.value.userTypeBos.forEach((item, index) => {
// 验证排班类型
if (!item.schedulingType) {
isValid = false;
ElMessage.error(`${index + 1}行排班类型不能为空`);
return;
}
// 验证人员选择
if (!item.opsUserId || item.opsUserId.length === 0) {
isValid = false;
ElMessage.error(`${index + 1}行请至少选择一名人员`);
return;
}
});
if (isValid) {
// 提交表单数据给父组件
emit('confirm', attendForm.value);
emit('update:manageAttendDialogVisible', false);
resetForm();
}
} catch (error) {
// 日期验证失败
console.error('表单验证失败', error);
}
};
// 重置表单
const resetForm = () => {
attendForm.value = {
schedulingDate: '',
// 只有当有排班类型时才初始化一个空的排班项
userTypeBos: shiftTypes.value.length > 0 ? [{ schedulingType: '', opsUserId: [] }] : []
};
};
// 监听弹窗显示状态变化,在显示时重置表单
watch(() => props.manageAttendDialogVisible, (newVisible) => {
if (newVisible) {
resetForm();
}
});
</script>
<style scoped>
/* 动态排班表单样式 */
.dynamic-shift-item {
margin-bottom: 15px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.shift-form-row {
display: flex;
align-items: center;
width: 100%;
}
.form-tip {
color: #909399;
font-size: 12px;
margin-top: 8px;
margin-left: 10px;
}
/* 空数据提示样式 */
.empty-tip {
margin-bottom: 15px;
}
.empty-tip .el-alert {
margin-bottom: 10px;
}
/* 确认按钮禁用状态提示 */
.dialog-footer .el-button.is-disabled {
cursor: not-allowed;
}
/* 响应式调整 */
@media screen and (max-width: 768px) {
.shift-form-row {
flex-direction: column;
align-items: stretch;
}
.shift-form-row .el-select {
width: 100% !important;
margin-right: 0 !important;
margin-bottom: 8px;
}
.shift-form-row .el-button {
align-self: flex-start;
margin-top: 8px;
}
}
</style>

View File

@ -0,0 +1,391 @@
<template>
<div class="schedule-table-container">
<el-table
:data="scheduleData"
style="width: 100%"
max-height="600"
stripe
border
v-loading="loading"
>
<!-- 固定列 -->
<el-table-column fixed prop="name" label="姓名" width="120" align="center" />
<el-table-column fixed="left" prop="postName" label="岗位" width="120" align="center" />
<el-table-column fixed="left" prop="weeklyHours" label="周总计/小时" width="120" align="center" />
<!-- 日期列 - 纵向显示号数和星期几 -->
<el-table-column
v-for="(dateInfo, index) in currentMonthDates"
:key="index"
:prop="`day${index + 1}`"
width="80"
align="center"
>
<template #header>
<div class="vertical-header">
<div class="date-number">{{ dateInfo.date }}</div>
<div class="week-day">{{ dateInfo.weekDay }}</div>
</div>
</template>
<template #default="scope">
<div
class="schedule-cell"
:class="getShiftClass(scope.row[`day${index + 1}`])"
@click="handleCellClick(scope.row, {...dateInfo, index}, scope)"
>
{{ formatShiftText(scope.row[`day${index + 1}`]) }}
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<!-- <div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div> -->
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watch } from 'vue';
import { getCurrentMonthDates, getMonthDates } from '@/utils/getDate';
import { ElMessage } from 'element-plus';
// 定义排班类型接口
interface UserTypePair {
schedulingDate: string;
schedulingType: string;
schedulingTypeName: string;
// 可能还有其他字段
[key: string]: any;
}
// 定义员工排班信息接口
interface ScheduleItem {
opsUserId: number;
opsUserName: string;
durationCount: number;
postName: string;
userTypePairs: UserTypePair[];
// 可能还有其他字段
[key: string]: any;
}
// 定义日期信息接口
interface DateInfo {
date: number;
weekDay: string;
fullDate: string;
year: number;
month: number;
}
// 定义表格行数据接口
interface TableRowData {
opsUserId: number;
name: string;
postName: string;
weeklyHours: number;
[key: string]: any; // 动态添加day1, day2等字段
}
// 定义props接收排班数据
const props = defineProps<{
scheduleList: ScheduleItem[];
loading?: boolean;
// 可选:指定要显示的月份
targetMonth?: { year: number; month: number };
}>();
const emit = defineEmits<{
'edit-schedule': [rowData: TableRowData, columnData: DateInfo & { index: number }, cellEvent: any];
'page-change': [currentPage: number, pageSize: number];
}>();
// 排班类型与样式的映射关系
const shiftConfig = {
'早班': { color: '#67c23a', className: 'morning-shift' },
'中班': { color: '#e6a23c', className: 'afternoon-shift' },
'晚班': { color: '#409eff', className: 'evening-shift' },
'休息': { color: '#909399', className: 'rest-day' },
};
// 获取当前月的日期信息
const currentMonthDates = ref<(DateInfo & { year: number; month: number })[]>([]);
// 分页相关状态
const currentPage = ref(1);
const pageSize = ref(10);
const total = computed(() => props.scheduleList ? props.scheduleList.length : 0);
// 格式化排班文本,支持多排班情况
const formatShiftText = (shiftData: any): string => {
if (!shiftData) return '休息';
// 如果是字符串,直接返回
if (typeof shiftData === 'string') {
return shiftData;
}
// 如果是数组,返回第一个排班类型
if (Array.isArray(shiftData)) {
return shiftData.length > 0 ? shiftData[0].schedulingTypeName || '休息' : '休息';
}
// 如果是对象,返回排班类型名称
if (typeof shiftData === 'object' && shiftData.schedulingTypeName) {
return shiftData.schedulingTypeName;
}
return '休息';
};
// 获取排班对应的样式类名
const getShiftClass = (shiftData: any): string => {
const shiftText = formatShiftText(shiftData);
return shiftConfig[shiftText as keyof typeof shiftConfig]?.className || 'unknown-shift';
};
// 生成排班数据
const scheduleData = computed((): TableRowData[] => {
const startIndex = (currentPage.value - 1) * pageSize.value;
const endIndex = startIndex + pageSize.value;
// 确保 props.scheduleList 存在
const scheduleList = props.scheduleList || [];
// 如果没有数据且loading为false返回空数组显示空状态
if (scheduleList.length === 0 && !props.loading) {
return [];
}
// 处理排班数据
return scheduleList.map((item: ScheduleItem) => {
const rowData: TableRowData = {
opsUserId: item.opsUserId,
name: item.opsUserName || `未知员工${item.opsUserId}`,
postName: item.postName || '未知岗位',
weeklyHours: item.durationCount || 0
};
// 为当月每一天生成排班数据
currentMonthDates.value.forEach((dateInfo, dayIndex) => {
// 格式化日期为 YYYY-MM-DD
const dateKey = `${dateInfo.year}-${String(dateInfo.month).padStart(2, '0')}-${String(dateInfo.date).padStart(2, '0')}`;
// 从userTypePairs中查找对应日期的所有排班信息
let daySchedule = null;
if (item.userTypePairs && Array.isArray(item.userTypePairs)) {
// 查找对应日期的所有排班信息
const dateSchedules = item.userTypePairs.filter(pair => pair.schedulingDate === dateKey);
// 如果有多个排班,也返回,方便后续扩展显示多个排班
daySchedule = dateSchedules.length > 0 ? dateSchedules : null;
}
// 如果找到排班信息,存储原始数据;如果没有,设置为'休息'
rowData[`day${dayIndex + 1}`] = daySchedule || '休息';
});
return rowData;
}).slice(startIndex, endIndex);
});
// 更新日期列表
const updateDates = () => {
if (props.targetMonth) {
// 使用指定的月份
const dates = getMonthDates(props.targetMonth.year, props.targetMonth.month - 1); // getMonthDates的month参数是0-11
currentMonthDates.value = dates.map(date => ({
...date,
year: props.targetMonth!.year,
month: props.targetMonth!.month
}));
} else {
// 使用当前月份
const today = new Date();
const dates = getCurrentMonthDates();
currentMonthDates.value = dates.map(date => ({
...date,
year: today.getFullYear(),
month: today.getMonth() + 1
}));
}
};
// 分页大小变化处理
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; // 重置为第一页
emit('page-change', currentPage.value, pageSize.value);
};
// 当前页码变化处理
const handleCurrentChange = (current: number) => {
currentPage.value = current;
emit('page-change', currentPage.value, pageSize.value);
};
// 处理单元格点击事件
const handleCellClick = (rowData: TableRowData, columnData: DateInfo & { index: number }, cellEvent: any) => {
// 获取当前单元格的排班数据
const cellData = rowData[`day${columnData.index + 1}`];
const shiftText = formatShiftText(cellData);
// 如果是休息状态,显示提示信息,不触发编辑事件
if (shiftText === '休息') {
ElMessage.warning('请前往管理考勤增加排班');
return;
}
// 非休息状态,触发编辑事件
emit('edit-schedule', rowData, columnData, cellEvent);
};
// 组件挂载时初始化
onMounted(() => {
updateDates();
});
// 监听目标月份变化,更新日期列表
watch(() => props.targetMonth, () => {
updateDates();
}, { deep: true });
// 监听排班数据变化,重置页码
watch(() => props.scheduleList, () => {
currentPage.value = 1;
}, { deep: true });
</script>
<style scoped>
.schedule-table-container {
overflow-x: auto;
padding: 16px;
background: #fff;
border-radius: 4px;
}
/* 优化滚动条样式 */
.schedule-table-container::-webkit-scrollbar {
height: 8px;
}
.schedule-table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.schedule-table-container::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 4px;
}
.schedule-table-container::-webkit-scrollbar-thumb:hover {
background: #909399;
}
/* 优化表格样式 */
:deep(.el-table) {
font-size: 14px;
}
:deep(.el-table__header-wrapper th) {
background-color: #fafafa;
font-weight: 500;
padding: 0 !important;
height: auto !important;
min-height: 60px;
}
:deep(.el-table__body-wrapper) {
overflow-x: visible;
}
/* 纵向表头样式 */
.vertical-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 8px 0;
}
.date-number {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.week-day {
font-size: 12px;
color: #666;
}
/* 排班单元格样式 */
.schedule-cell {
padding: 8px 0;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
border-radius: 4px;
}
.schedule-cell:hover {
background-color: #f5f7fa;
transform: scale(1.05);
}
/* 排班类型样式 */
.morning-shift {
color: #67c23a; /* 早班 - 绿色 */
font-weight: 500;
}
.afternoon-shift {
color: #e6a23c; /* 中班 - 橙色 */
font-weight: 500;
}
.evening-shift {
color: #409eff; /* 晚班 - 蓝色 */
font-weight: 500;
}
.rest-day {
color: #909399; /* 休息 - 灰色 */
}
.unknown-shift {
color: #333; /* 未知类型 - 默认黑色 */
}
/* 分页容器样式 */
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
align-items: center;
}
/* 分页组件样式优化 */
:deep(.el-pagination) {
font-size: 14px;
}
/* 加载状态样式优化 */
:deep(.el-loading-mask) {
background-color: rgba(255, 255, 255, 0.8);
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<div class="box">
<div class="chart-header">
<TitleComponent title="审批" :font-level="2" />
<span>更多</span>
</div>
<div class="approval-content">
<div
v-for="(item, index) in approvalData"
:key="index"
class="approval-item"
>
<div class="approval-left">
<div class="approval-icon">
<img :src="item.iconPath" :alt="item.type">
</div>
<div class="approval-info">
<div class="info">
<div class="type">{{ item.type }}</div>
<div class="day">{{ item.days }}</div>
</div>
<div class="info1">
<div class="time">
<img src="@/assets/demo/time.png" alt="时间">
<span>{{ item.timeRange }}</span>
</div>
<div class="people">
<img src="@/assets/demo/people.png" alt="人员">
<span>{{ item.people }}</span>
</div>
</div>
</div>
</div>
<div class="approval-tag">
<el-tag :type="item.statusType">{{ item.status }}</el-tag>
</div>
</div>
</div>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
// 接收从父组件传入的数据
const props = defineProps({
approvalData: {
type: Array,
default: () => []
}
});
</script>
<style scoped lang="scss">
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
.chart-header span {
color: #186DF5;
font-size: 14px;
cursor: pointer;
}
.approval-content {
background-color: white;
.approval-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #F2F8FC;
border-radius: 8px;
margin-bottom: 12px;
border: 1px solid #F2F3F5;
transition: all 0.3s ease;
}
.approval-item:hover {
border-color: #E4E6EB;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.approval-left {
display: flex;
align-items: center;
flex: 1;
}
.approval-icon {
width: 48px;
height: 48px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
position: relative;
overflow: hidden;
background: #186DF5;
}
.approval-icon img {
width: 26px;
height: 26px;
}
.approval-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.info {
display: flex;
justify-content: space-between;
align-items: center;
width: 90px;
}
.info .type {
font-size: 14px;
font-weight: 500;
color: #333;
}
.info .day {
font-size: 14px;
color: #666;
}
.info1 {
display: flex;
align-items: center;
gap: 16px;
}
.info1 .time,
.info1 .people {
display: flex;
align-items: center;
font-size: 12px;
color: rgba(113, 128, 150, 1);
}
.info1 img {
width: 12px;
height: 12px;
margin-right: 4px;
}
.approval-tag {
margin-left: 16px;
}
.approval-tag .el-tag {
padding: 4px 12px;
font-size: 12px;
border-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,336 @@
<template>
<div class="box">
<div class="chart-header">
<TitleComponent title="日历" :font-level="2" />
</div>
<div class="calendar-container">
<div class="calendar-header">
<el-button size="small" type="text" @click="prevMonth">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<span class="current-month">{{ currentYear }} {{ currentMonthName }}</span>
<el-button size="small" type="text" @click="nextMonth">
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<div class="calendar-weekdays">
<span v-for="day in weekdays" :key="day" class="weekday">{{ day }}</span>
</div>
<div class="calendar-days">
<!-- 上月剩余天数 -->
<div v-for="(day, index) in prevMonthDays" :key="'prev-' + index" class="day prev-month-day">
{{ day }}
</div>
<!-- 当月天数 -->
<div
v-for="day in currentMonthDays"
:key="day"
class="day current-month-day"
:class="{
'current-day': isCurrentDay(day),
'selected-day': isSelectedDay(day)
}"
@click="selectDay(day)"
>
{{ day }}
<!-- 今天有红点标记 -->
<span v-if="isToday(day)" class="today-marker"></span>
<!-- 考勤状态标记 -->
<span v-if="getAttendanceStatus(day)" class="attendance-marker" :class="getAttendanceStatus(day)"></span>
</div>
<!-- 下月开始天数 -->
<div v-for="(day, index) in nextMonthDays" :key="'next-' + index" class="day next-month-day">
{{ day }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
import { ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
// 接收从父组件传入的数据
const props = defineProps({
calendarData: {
type: Object,
default: () => ({
// 初始化当前日期
today: new Date(),
currentDate: new Date(2025, 8, 27), // 2025年9月27日截图中显示的日期
selectedDate: new Date(2025, 8, 27),
// 模拟考勤数据
attendanceData: {
2025: {
9: {
1: 'normal',
4: 'late',
8: 'absent',
10: 'leave',
15: 'normal',
20: 'normal',
25: 'late',
27: 'normal'
}
}
}
})
}
});
// 初始化当前日期
const today = ref(props.calendarData.today);
const currentDate = ref(props.calendarData.currentDate);
const selectedDate = ref(props.calendarData.selectedDate);
// 模拟考勤数据
const attendanceData = ref(props.calendarData.attendanceData);
// 星期几的显示
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// 计算属性
const currentYear = computed(() => currentDate.value.getFullYear());
const currentMonth = computed(() => currentDate.value.getMonth());
const currentMonthName = computed(() => monthNames[currentMonth.value]);
// 获取当月的天数
const currentMonthDays = computed(() => {
return new Date(currentYear.value, currentMonth.value + 1, 0).getDate();
});
// 获取当月第一天是星期几0-60是星期日
const firstDayOfMonth = computed(() => {
return new Date(currentYear.value, currentMonth.value, 1).getDay();
});
// 获取上月剩余天数
const prevMonthDays = computed(() => {
const days = [];
const prevMonth = new Date(currentYear.value, currentMonth.value, 0).getDate(); // 上月最后一天
for (let i = firstDayOfMonth.value - 1; i >= 0; i--) {
days.push(prevMonth - i);
}
return days;
});
// 获取下月开始天数
const nextMonthDays = computed(() => {
const days = [];
const totalDays = prevMonthDays.value.length + currentMonthDays.value;
const nextDays = 35 - totalDays; // 显示5周共35天
for (let i = 1; i <= nextDays; i++) {
days.push(i);
}
return days;
});
// 方法
const prevMonth = () => {
currentDate.value = new Date(currentYear.value, currentMonth.value - 1, 1);
};
const nextMonth = () => {
currentDate.value = new Date(currentYear.value, currentMonth.value + 1, 1);
};
const selectDay = (day) => {
selectedDate.value = new Date(currentYear.value, currentMonth.value, day);
// 显示选择的日期和考勤状态
let message = `Selected: ${currentMonthName.value} ${day}, ${currentYear.value}`;
const status = getAttendanceStatus(day);
if (status) {
const statusMap = {
normal: '正常',
late: '迟到',
absent: '缺勤',
leave: '请假'
};
message += ` - 考勤状态: ${statusMap[status] || '未知'}`;
}
ElMessage.success(message);
};
// 获取考勤状态
const getAttendanceStatus = (day) => {
if (attendanceData.value[currentYear.value] &&
attendanceData.value[currentYear.value][currentMonth.value + 1] &&
attendanceData.value[currentYear.value][currentMonth.value + 1][day]) {
return attendanceData.value[currentYear.value][currentMonth.value + 1][day];
}
return null;
};
const isToday = (day) => {
return day === today.value.getDate() &&
currentMonth.value === today.value.getMonth() &&
currentYear.value === today.value.getFullYear();
};
const isCurrentDay = (day) => {
return day === today.value.getDate() &&
currentMonth.value === today.value.getMonth() &&
currentYear.value === today.value.getFullYear();
};
const isSelectedDay = (day) => {
return day === selectedDate.value.getDate() &&
currentMonth.value === selectedDate.value.getMonth() &&
currentYear.value === selectedDate.value.getFullYear();
};
</script>
<style scoped lang="scss">
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.chart-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
}
.chart-header span {
color: #186DF5;
font-size: 14px;
cursor: pointer;
}
.calendar-container {
background: white;
border-radius: 8px;
padding: 16px;
// border: 1px solid #F2F3F5;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 8px 0;
}
.current-month {
font-size: 16px;
font-weight: 500;
color: #333;
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin-bottom: 8px;
}
.weekday {
text-align: center;
font-size: 14px;
color: #666;
font-weight: 500;
padding: 8px 0;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
height: 350px;
}
.day {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 40px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.prev-month-day,
.next-month-day {
color: #C0C4CC;
}
.current-month-day {
color: #333;
}
.current-month-day:hover {
background-color: #ECF5FF;
color: #186DF5;
}
.current-day {
position: relative;
}
.today-marker {
position: absolute;
bottom: 4px;
width: 4px;
height: 4px;
border-radius: 50%;
background-color: #FF6B3B;
}
.selected-day {
background-color: #186DF5;
color: white !important;
font-weight: 500;
}
.selected-day:hover {
background-color: #4096ff;
color: white !important;
}
// 考勤状态标记样式
.attendance-marker {
position: absolute;
bottom: 2px;
width: 6px;
height: 6px;
border-radius: 50%;
}
.attendance-marker.normal {
background-color: #52c41a;
}
.attendance-marker.late {
background-color: #faad14;
}
.attendance-marker.absent {
background-color: #f5222d;
}
.attendance-marker.leave {
background-color: #1890ff;
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="box">
<TitleComponent title="今日出勤" :font-level="2" />
<div class="todayAttend">
<div class="todayAttendItem">
<img :src="props.todayAttendData.attendance.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">出勤</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.attendance.count }}</span>
</div>
</div>
<div class="todayAttendItem">
<img :src="props.todayAttendData.late.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">迟到</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.late.count }}</span>
</div>
</div>
<div class="todayAttendItem">
<img :src="props.todayAttendData.earlyLeave.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">早退</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.earlyLeave.count }}</span>
</div>
</div>
<div class="todayAttendItem">
<img :src="props.todayAttendData.absent.icon" alt="" width="30px" height="30px">
<div class="todayAttendItemInfo">
<span class="todayAttendItemTitle">缺勤</span>
<span class="todayAttendItemNum">{{ props.todayAttendData.absent.count }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
// 接收从父组件传入的数据
const props = defineProps({
todayAttendData: {
type: Object,
default: () => ({})
}
});
</script>
<style scoped lang="scss">
.todayAttend {
display: flex;
justify-content: space-between;
align-items: center;
}
.todayAttendItem {
width: 110px;
height: 100px;
background: #E5F0FF;
padding: 5px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: column;
.todayAttendItemInfo{
display: flex;
justify-content: space-around;
align-items: center;
.todayAttendItemTitle {
color: rgba(113, 128, 150, 1);
font-size: 12px;
}
.todayAttendItemNum {
font-size: 16px;
color: rgba(0, 30, 59, 1);
}
}
}
</style>

View File

@ -0,0 +1,290 @@
<template>
<div class="total-view-container">
<div class="total-view-content">
<!-- 使用循环生成统计卡片 -->
<div v-for="(item, index) in statsItems" :key="index" class="stats-card">
<div class="stats-card-header">
<span class="stats-title">{{ item.title }}</span>
<span class="stats-change" :class="{ positive: item.data.isPositive, negative: !item.data.isPositive }">
{{ item.data.isPositive ? '↑' : '↓' }} {{ item.data.change }} {{ item.compareText }}
</span>
</div>
<div class="stats-card-body">
<div class="stats-value">{{ item.data.value }}</div>
<div class="stats-chart">
<div :ref="el => chartRefs[index] = el" class="chart-container"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
// 接收从父组件传入的数据
const props = defineProps({
totalData: {
type: Object,
default: () => ({
attendance: {
value: 248,
change: '+8.2%',
isPositive: true,
chartData: [150, 230, 224, 218, 135, 300, 220],
color: '#FF7D00',
title: '总出勤人数',
compareText: '较昨日同期',
chartType: 'bar'
},
rest: {
value: 8,
change: '+8.2%',
isPositive: true,
chartData: [10, 12, 15, 8, 7, 9, 10],
color: '#00C48C',
title: '调休',
compareText: '较上月同期',
chartType: 'line'
},
leave: {
value: 24,
change: '-10%',
isPositive: false,
chartData: [30, 25, 28, 22, 20, 26, 24],
color: '#FF5252',
title: '本月请假',
compareText: '较昨日同期',
chartType: 'line'
},
rate: {
value: '96.8%',
change: '+10%',
isPositive: true,
chartData: [90, 92, 94, 95, 97, 98, 96.8],
color: '#029CD4',
title: '平均出勤率',
compareText: '较昨日同期',
chartType: 'line'
}
})
}
});
// 图表引用数组
const chartRefs = ref([]);
// 转换totalData为数组格式方便循环渲染
const statsItems = computed(() => {
return Object.keys(props.totalData).map(key => ({
title: props.totalData[key].title,
data: {
value: props.totalData[key].value,
change: props.totalData[key].change,
isPositive: props.totalData[key].isPositive,
chartData: props.totalData[key].chartData,
color: props.totalData[key].color
},
compareText: props.totalData[key].compareText,
chartType: props.totalData[key].chartType
}));
});
// 初始化图表
const initCharts = () => {
const chartInstances = [];
// 循环初始化所有图表
statsItems.value.forEach((item, index) => {
if (!chartRefs.value[index]) return;
const chartInstance = echarts.init(chartRefs.value[index]);
// 根据图表类型设置不同的配置
if (item.chartType === 'bar') {
// 柱状图配置
chartInstance.setOption({
tooltip: { show: false },
grid: { top: 0, bottom: 0, left: -70, right: 0, containLabel: true },
xAxis: {
type: 'category',
data: Array(item.data.chartData.length).fill(''),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false }
},
yAxis: {
type: 'value',
show: false
},
series: [{
data: item.data.chartData,
type: 'bar',
barWidth: 10,
itemStyle: {
color: item.data.color,
borderRadius: [10, 10, 0, 0] // 柱状图圆角
},
emphasis: {
focus: 'series'
}
}]
});
} else if (item.chartType === 'line') {
// 折线图配置
chartInstance.setOption({
tooltip: { show: false },
grid: { top: 10, bottom: 0, left: -30, right: 0, containLabel: true },
xAxis: {
type: 'category',
data: Array(item.data.chartData.length).fill(''),
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false }
},
yAxis: {
type: 'value',
show: false
},
series: [{
data: item.data.chartData,
type: 'line',
smooth: true,
showSymbol: false,
lineStyle: {
width: 4, // 折线图线条加粗
color: item.data.color
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: `${item.data.color}33`
}, {
offset: 1,
color: `${item.data.color}02`
}])
}
}]
});
}
chartInstances.push(chartInstance);
});
// 响应窗口大小变化
window.addEventListener('resize', () => {
chartInstances.forEach(instance => {
instance.resize();
});
});
};
// 监听props变化重新初始化图表
watch(() => props.totalData, () => {
// 清空之前的图表引用
chartRefs.value = [];
// 等待DOM更新后重新初始化图表
nextTick(() => {
initCharts();
});
}, { deep: true });
// 组件挂载后初始化图表
onMounted(() => {
initCharts();
});
</script>
<style scoped lang="scss">
.total-view-container {
background-color: #fff;
border-radius: 8px;
padding: 20px;
height: 217px;
}
.total-view-content {
display: flex;
justify-content: space-between;
gap: 0;
}
.stats-card {
flex: 1;
padding: 16px;
background-color: #fff;
border-radius: 0;
border: none;
border-right: 1px dashed #E4E7ED;
}
.stats-card:last-child {
border-right: none;
}
.stats-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.stats-title {
font-size: 14px;
color: #606266;
}
.stats-change {
font-size: 12px;
}
.stats-change.positive {
color: #52C41A;
}
.stats-change.negative {
color: #F5222D;
}
.stats-card-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.stats-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
.stats-chart {
width: 100%;
height: 60px;
}
.chart-container {
width: 100%;
height: 100%;
}
// 响应式布局
@media screen and (max-width: 1200px) {
.total-view-content {
flex-wrap: wrap;
}
.stats-card {
width: calc(50% - 10px);
}
}
@media screen and (max-width: 768px) {
.stats-card {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,666 @@
<template>
<!-- 考勤管理 -->
<div class="model">
<!-- 标题栏 -->
<el-row :gutter="24">
<el-col :span="12">
<TitleComponent title="考勤管理" subtitle="项目出勤情况、人员排班及请假调休管理" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<!-- 子col1下拉 -->
<el-col :span="4">
<el-select placeholder="选择电站">
<el-option label="所有电站" value="all"></el-option>
</el-select>
</el-col>
<!-- 子col2下拉框容器 -->
<el-col :span="4">
<el-select placeholder="日期范围">
<el-option label="所有月份" value="all"></el-option>
</el-select>
</el-col>
<el-col :span="4">
<el-button type="primary">
导出数据
<el-icon class="el-icon--right">
<UploadFilled />
</el-icon>
</el-button>
</el-col>
</el-col>
</el-row>
<!-- 第一行totalView infoBox -->
<el-row :gutter="20">
<el-col :span="17">
<totalView :totalData="totalData"></totalView>
</el-col>
<el-col :span="7">
<infoBox></infoBox>
</el-col>
</el-row>
<!-- 第二行人员排班和出勤趋势分析 -->
<el-row :gutter="20">
<el-col :span="17">
<div class="analysis-content">
<attendTrend :attendData="attendData"></attendTrend>
<el-card>
<div style="display: flex; justify-content: space-between; align-items: center;">
<TitleComponent title="人员排班" :fontLevel="2" />
<el-button type="primary" @click="manageAttendDialogVisible = true">
管理考勤
</el-button>
</div>
<renyuanpaiban @edit-schedule="handleEditSchedule" :schedule-list="scheduleList">
</renyuanpaiban>
</el-card>
</div>
</el-col>
<!-- 右侧日历卡片 -->
<el-col :span="7">
<div class="calendar-content">
<el-card>
<calendar :calendarData="calendarData"></calendar>
<todayAttend :todayAttendData="todayAttendData"></todayAttend>
<approval :approvalData="approvalData"></approval>
</el-card>
</div>
</el-col>
</el-row>
<!-- 人员排班弹窗组件 -->
<renyuanguanliDialog v-model:manageAttendDialogVisible="manageAttendDialogVisible"
@confirm="handleAttendConfirm" :personnel-list="paibanRenYuanList" :type-list="scheduleTypes" />
<!-- 编辑排班弹窗 -->
<el-dialog v-model="editScheduleDialogVisible" title="修改排班" width="400">
<el-form :model="editScheduleForm" label-width="100px">
<el-form-item label="员工姓名">
<el-input v-model="editScheduleForm.name" disabled />
</el-form-item>
<el-form-item label="排班日期">
<el-input v-model="editScheduleForm.date" disabled />
</el-form-item>
<el-form-item label="当前排班">
<el-input v-model="editScheduleForm.currentShift" disabled />
</el-form-item>
<el-form-item label="修改为">
<el-select v-model="editScheduleForm.newShift" placeholder="请选择排班类型" style="width: 100%;">
<el-option v-for="option in editscheduleTypes" :key="option.id" :label="option.schedulingName"
:value="option.id"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editScheduleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmEditSchedule">
确认修改
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import infoBox from '@/views/integratedManage/attendManage/components/infoBox.vue'
import attendTrend from '@/views/integratedManage/attendManage/components/attendTrend.vue'
import todayAttend from '@/views/integratedManage/attendManage/components/rightBox/todayAttend.vue'
import approval from '@/views/integratedManage/attendManage/components/rightBox/approval.vue'
import calendar from '@/views/integratedManage/attendManage/components/rightBox/calendar.vue'
import totalView from '@/views/integratedManage/attendManage/components/totalView.vue'
import renyuanpaiban from '@/views/integratedManage/attendManage/components/renyuanpaiban.vue'
import renyuanguanliDialog from '@/views/integratedManage/attendManage/components/renyuanguanliDialog.vue'
import { getPaibanRenYuanList, getPaibanRiLiList, savePaiban, updatePaiban, deletePaiban } from '@/api/renyuan/paiban';
import { SchedulingVO } from '@/api/renyuan/paiban/types';
import { listSchedulingDate } from '@/api/renyuan/schedulingDate';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { getCurrentMonthDates } from '@/utils/getDate';
const currentMonthDates = getCurrentMonthDates();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 初始化用户store
const userStore = useUserStore();
// 排班人员列表
const paibanRenYuanList = ref([]);
// 排班人员数据
const scheduleList = ref<SchedulingVO[]>([]);
// 排班类型
const scheduleTypes = ref([]);
// 修改弹出框的类型下拉
const editscheduleTypes = ref([]);
// 编辑排班弹窗
const editScheduleDialogVisible = ref(false);
// 人员排班弹窗
const manageAttendDialogVisible = ref(false);
// 获取排班人员列表
const fetchPaibanRenYuanList = async (deptId?: string) => {
try {
// 如果没有提供deptId默认使用当前登录用户的部门ID
const targetDeptId = deptId || userStore.deptId;
if (!targetDeptId) {
console.warn('未提供部门ID无法获取排班人员列表');
return;
}
const response = await getPaibanRenYuanList(targetDeptId);
// console.log('获取排班人员:', response);
paibanRenYuanList.value = response.data?.map((user: any) => ({
label: user.nickName,
value: user.userId,
deptId: user.deptId,
})) || [];
} catch (error) {
console.error('获取排班人员列表失败:', error);
}
};
// 获取排班数据
const getscheduleData = async (query?: SchedulingVO) => {
try {
if (userStore.selectedProject && userStore.selectedProject.id) {
const res = await getPaibanRiLiList(query);
if (res.code === 200) {
scheduleList.value = res.data || [];
} else {
proxy?.$modal.msgError(res.msg || '获取排班数据失败');
}
}
} catch (error) {
proxy?.$modal.msgError('获取排班数据失败');
}
}
// 获取排班类型
const getTypeList = async () => {
try {
const res = await listSchedulingDate({ projectId: userStore.selectedProject?.id, pageNum: 1, pageSize: 10 });
if (res.code === 200) {
scheduleTypes.value = res.rows || [];
// 在scheduleTypes基础上新增休息字段
editscheduleTypes.value = [
...(res.rows || []),
{ id: 'rest', schedulingName: '休息' }
];
} else {
proxy?.$modal.msgError(res.msg || '获取排班类型失败');
// 如果获取失败,至少保留休息选项
editscheduleTypes.value = [{ id: 'rest', schedulingName: '休息' }];
}
} catch (error) {
console.error('获取排班类型出错:', error);
proxy?.$modal.msgError('获取排班类型失败');
// 异常情况下也保留休息选项
editscheduleTypes.value = [{ id: 'rest', schedulingTypeName: '休息' }];
}
}
// 安排人员排班
const arrangePaiban = async (formData: any) => {
try {
// 添加projectId到表单数据中
const dataWithProjectId = {
...formData,
projectId: userStore.selectedProject?.id
};
const res = await savePaiban(dataWithProjectId);
if (res.code === 200) {
proxy?.$modal.msgSuccess('排班成功');
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 关闭弹窗
manageAttendDialogVisible.value = false;
} else {
proxy?.$modal.msgError(res.msg || '排班失败');
}
} catch (error) {
proxy?.$modal.msgError('排班失败');
}
}
// 修改排班
const updateSchedule = async (formData: any) => {
try {
const res = await updatePaiban(formData);
if (res.code === 200) {
proxy?.$modal.msgSuccess('修改排班成功');
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 关闭弹窗
editScheduleDialogVisible.value = false;
} else {
proxy?.$modal.msgError(res.msg || '修改排班失败');
}
} catch (error) {
proxy?.$modal.msgError('修改排班失败');
}
}
// 删除排班
const deleteSchedule = async (ID: any) => {
try {
const res = await deletePaiban(ID);
if (res.code === 200) {
proxy?.$modal.msgSuccess('删除排班成功');
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 关闭弹窗
editScheduleDialogVisible.value = false;
} else {
proxy?.$modal.msgError(res.msg || '删除排班失败');
}
} catch (error) {
proxy?.$modal.msgError('删除排班失败');
}
}
// 处理考勤管理确认
const handleAttendConfirm = (formData: any) => {
// console.log('考勤表单数据:', formData);
// 这里可以添加表单提交逻辑
arrangePaiban(formData);
};
// 编辑排班表单数据
const editScheduleForm = ref({
opsUserId: '', // 新增opsUserId字段
name: '',
date: '',
currentShift: '',
newShift: '',
id: '' // 新增scheduledId字段用于存储排班的id
});
// 格式化排班文本,用于显示当前排班
const formatShiftDisplay = (shiftData: any): string => {
if (!shiftData) return '休息';
// 如果是字符串,直接返回
if (typeof shiftData === 'string') {
return shiftData;
}
// 如果是数组,返回第一个排班类型的名称
if (Array.isArray(shiftData) && shiftData.length > 0) {
if (typeof shiftData[0] === 'object' && shiftData[0].schedulingTypeName) {
return shiftData[0].schedulingTypeName;
}
return shiftData[0].toString();
}
// 如果是对象,返回排班类型名称
if (typeof shiftData === 'object' && shiftData.schedulingTypeName) {
return shiftData.schedulingTypeName;
}
// 如果是对象但没有schedulingTypeName属性尝试返回其字符串表示
if (typeof shiftData === 'object') {
return JSON.stringify(shiftData);
}
return '休息';
};
// 处理编辑排班
const handleEditSchedule = (rowData: any, columnData: any) => {
// 设置表单数据
editScheduleForm.value = {
opsUserId: rowData.opsUserId || '', // 从opsUserId字段获取用户ID
name: rowData.name,
date: `${columnData.fullDate}`,
currentShift: '',
newShift: '',
id: ''
};
// 查找当前排班
Object.keys(rowData).forEach(key => {
if (key.startsWith('day')) {
const dayIndex = parseInt(key.replace('day', '')) - 1;
if (dayIndex === columnData.index) {
const formattedShift = formatShiftDisplay(rowData[key]);
editScheduleForm.value.currentShift = formattedShift;
editScheduleForm.value.newShift = formattedShift;
// 如果不是休息状态则获取id并赋值到表单中
if (formattedShift !== '休息' && rowData[key]) {
// 处理rowData[key]为数组的情况
if (Array.isArray(rowData[key]) && rowData[key].length > 0 && rowData[key][0].id) {
editScheduleForm.value.id = rowData[key][0].id;
}
// 同时处理rowData[key]为对象的情况作为兼容
else if (typeof rowData[key] === 'object' && rowData[key].id) {
editScheduleForm.value.id = rowData[key].id;
}
}
}
}
});
// 显示弹窗
editScheduleDialogVisible.value = true;
};
// 确认修改排班
const handleConfirmEditSchedule = () => {
// 按照要求的格式准备数据
const submitData = {
projectId: userStore.selectedProject?.id,
opsUserId: editScheduleForm.value.opsUserId,
schedulingType: editScheduleForm.value.newShift,
schedulingDate: editScheduleForm.value.date,
id: editScheduleForm.value.id
};
console.log('提交的排班数据格式:', submitData);
if (submitData.schedulingType == 'rest') {
deleteSchedule(submitData.id);
} else {
updateSchedule(submitData);
}
};
// 出勤数据 - 用于attendTrend组件
const attendData = ref(
{
week: {
xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
actualCount: [40, 20, 30, 15, 22, 63, 58, 43, 39, 36],
expectedCount: [100, 556, 413, 115, 510, 115, 317, 118, 14, 7]
},
month: {
xAxis: ['第1周', '第2周', '第3周', '第4周'],
actualData: [280, 360, 320, 400],
theoreticalData: [300, 400, 350, 450]
},
}
)
// Mock数据 - 更新为循环生成所需的数据结构
const totalData = ref({
attendance: {
value: 248,
change: '+8.2%',
isPositive: true,
chartData: [150, 230, 224, 218, 135, 300, 220],
color: '#FF7D00',
title: '总出勤人数',
compareText: '较昨日同期',
chartType: 'bar'
},
rest: {
value: 8,
change: '+8.2%',
isPositive: true,
chartData: [10, 12, 15, 8, 7, 9, 10],
color: '#00C48C',
title: '调休',
compareText: '较上月同期',
chartType: 'line'
},
leave: {
value: 24,
change: '-10%',
isPositive: false,
chartData: [30, 25, 28, 22, 20, 26, 24],
color: '#FF5252',
title: '本月请假',
compareText: '较昨日同期',
chartType: 'line'
},
rate: {
value: '96.8%',
change: '+10%',
isPositive: true,
chartData: [90, 92, 94, 95, 97, 98, 96.8],
color: '#029CD4',
title: '平均出勤率',
compareText: '较昨日同期',
chartType: 'line'
}
});
// 审批数据 - 用于approval组件
const approvalData = ref([
{
type: '事假',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '病假',
days: 2,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '调休',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '事假',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '待审批',
statusType: 'primary',
iconPath: '/src/assets/demo/approval.png'
},
{
type: '事假',
days: 1,
timeRange: '09.14-09.15',
people: '水泥班组-王五',
status: '已通过',
statusType: 'success',
iconPath: '/src/assets/demo/approval.png'
}
]);
// 今日出勤数据 - 用于todayAttend组件
const todayAttendData = ref({
attendance: {
count: 150,
icon: '/src/assets/demo/qin.png'
},
late: {
count: 5,
icon: '/src/assets/demo/chi.png'
},
earlyLeave: {
count: 2,
icon: '/src/assets/demo/tui.png'
},
absent: {
count: 8,
icon: '/src/assets/demo/que.png'
}
});
// 日历数据 - 用于calendar组件
const calendarData = ref({
// 初始化当前日期
today: new Date(),
currentDate: new Date(2025, 8, 27), // 2025年9月27日截图中显示的日期
selectedDate: new Date(2025, 8, 27),
// 模拟考勤数据
attendanceData: {
2025: {
9: {
1: 'normal',
4: 'late',
8: 'absent',
10: 'leave',
15: 'normal',
20: 'normal',
25: 'late',
27: 'normal'
}
}
}
});
// 封装刷新排班数据的方法
const refreshScheduleData = (projectId: string) => {
// 获取排班数据
getscheduleData({
projectId: projectId,
schedulingStartDate: currentMonthDates[0].fullDate,
schedulingEndDate: currentMonthDates[currentMonthDates.length - 1].fullDate,
});
// 获取排班类型
getTypeList();
};
// 监听projectId变化
const projectIdWatcher = watch(
() => userStore.selectedProject?.id,
(newProjectId, oldProjectId) => {
if (newProjectId && newProjectId !== oldProjectId) {
refreshScheduleData(newProjectId);
}
},
{ immediate: false, deep: true }
);
onMounted(() => {
// 刷新排班数据
refreshScheduleData(userStore.selectedProject.id);
// 获取可以排班的人员列表
fetchPaibanRenYuanList(String(userStore.deptId));
});
// 组件卸载时移除监听器
onUnmounted(() => {
if (projectIdWatcher) {
projectIdWatcher();
}
});
</script>
<style scoped lang="scss">
.model {
padding: 24px 20px;
background-color: rgba(242, 248, 252, 1);
}
/* 标题栏与内容区域间距 */
.el-row+.el-row {
margin-top: 24px;
}
/* 分析内容区域 */
.analysis-content {
display: flex;
flex-direction: column;
gap: 45px;
// border: 1px solid red;
}
/* 日历内容区域 */
.calendar-content {
display: flex;
flex-direction: column;
}
/* 右侧日历卡片内组件间距 */
.calendar-content .el-card {
display: flex;
flex-direction: column;
height: 100%;
}
.calendar-content .el-card>* {
margin-bottom: 16px;
}
.calendar-content .el-card>*:last-child {
margin-bottom: 0;
flex: 1;
}
/* 卡片样式统一 */
.el-card {
border-radius: 8px !important;
border: none !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
/* 下拉选择器和按钮样式调整 */
.el-select {
width: 100%;
margin-right: 12px;
}
.el-button {
margin-left: 8px;
}
/* 响应式布局优化 */
@media screen and (max-width: 1200px) {
.model {
padding: 16px;
}
.el-row+.el-row {
margin-top: 16px;
}
.analysis-content {
gap: 16px;
}
/* 日历卡片内组件间距 */
.calendar-content .el-card>* {
margin-bottom: 12px;
}
}
/* 更细粒度的响应式调整 */
@media screen and (max-width: 768px) {
.model {
padding: 12px;
}
.el-select {
margin-right: 8px;
}
.el-button {
margin-left: 4px;
padding: 8px 12px;
}
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<div class="p-2">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="排班名称" prop="schedulingName">
<el-input v-model="queryParams.schedulingName" placeholder="请输入排班名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['personnel:schedulingDate:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['personnel:schedulingDate:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['personnel:schedulingDate:remove']">删除</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="schedulingDateList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="排班名称" align="center" prop="schedulingName" />
<el-table-column label="开始时间" align="center" prop="startTime" width="180">
<template #default="scope">
<span>{{scope.row.startTime}}</span>
</template>
</el-table-column>
<el-table-column label="结束时间" align="center" prop="endTime" width="180">
<template #default="scope">
<span>{{ scope.row.endTime}}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['personnel:schedulingDate:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['personnel:schedulingDate:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改运维-排班时间类型对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="schedulingDateFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="排班名称" prop="schedulingName">
<el-input v-model="form.schedulingName" placeholder="请输入排班名称" />
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
<el-time-select clearable
v-model="form.startTime"
value-format="HH:mm:ss"
step="00:10:00"
start="00:00"
end="23:59"
placeholder="请选择开始时间">
</el-time-select>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-time-select clearable
v-model="form.endTime"
value-format="HH:mm:ss"
step="00:10:00"
start="00:00"
end="23:59"
placeholder="请选择结束时间">
</el-time-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="SchedulingDate" lang="ts">
import { ref, reactive, toRefs, onMounted, onUnmounted, watch, getCurrentInstance } from 'vue';
import { listSchedulingDate, getSchedulingDate, delSchedulingDate, addSchedulingDate, updateSchedulingDate } from '@/api/renyuan/schedulingDate';
import { SchedulingDateVO, SchedulingDateQuery, SchedulingDateForm } from '@/api/renyuan/schedulingDate/types';
// 导入用户store
import { useUserStore } from '@/store/modules/user';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 获取用户store
const userStore = useUserStore();
const schedulingDateList = ref<SchedulingDateVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const queryFormRef = ref<ElFormInstance>();
const schedulingDateFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: SchedulingDateForm = {
id: undefined,
schedulingName: undefined,
startTime: undefined,
endTime: undefined,
projectId: undefined,
}
const data = reactive<PageData<SchedulingDateForm, SchedulingDateQuery>>({
form: {...initFormData},
queryParams: {
pageNum: 1,
pageSize: 10,
schedulingName: undefined,
startTime: undefined,
endTime: undefined,
projectId: undefined,
params: {
}
},
rules: {
schedulingName: [
{ required: true, message: "排班名称不能为空", trigger: "blur" }
],
startTime: [
{ required: true, message: "开始时间不能为空", trigger: "blur" }
],
endTime: [
{ required: true, message: "结束时间不能为空", trigger: "blur" }
]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运维-排班时间类型列表 */
const getList = async () => {
loading.value = true;
const res = await listSchedulingDate(queryParams.value);
schedulingDateList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = {...initFormData};
schedulingDateFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: SchedulingDateVO[]) => {
ids.value = selection.map(item => item.id);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
// 显式设置projectId为当前选中的项目ID
if (userStore.selectedProject && userStore.selectedProject.id) {
form.value.projectId = userStore.selectedProject.id;
}
dialog.visible = true;
dialog.title = "添加运维-排班时间类型";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: SchedulingDateVO) => {
reset();
const _id = row?.id || ids.value[0]
const res = await getSchedulingDate(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改运维-排班时间类型";
}
/** 提交按钮 */
const submitForm = () => {
schedulingDateFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateSchedulingDate(form.value).finally(() => buttonLoading.value = false);
} else {
await addSchedulingDate(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: SchedulingDateVO) => {
const _ids = row?.id || ids.value;
await proxy?.$modal.confirm('是否确认删除运维-排班时间类型编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
await delSchedulingDate(_ids);
proxy?.$modal.msgSuccess("删除成功");
await getList();
}
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
}
}, { immediate: true, deep: true });
onMounted(() => {
getList();
});
// 组件卸载时清空projectId
onUnmounted(() => {
queryParams.value.projectId = undefined;
form.value.projectId = undefined;
});
</script>

View File

@ -0,0 +1,349 @@
<template>
<div class="manage-form-container">
<!-- 搜索和筛选区域 -->
<div class="search-filter-section">
<el-row gutter="12" align="middle">
<el-col :span="2">
<el-select v-model="searchForm.deviceType" placeholder="设备类型" clearable>
<el-option label="全部类型" value="" />
<el-option label="逆变器" value="逆变器" />
<el-option label="传感器" value="传感器" />
<el-option label="电表" value="电表" />
<el-option label="摄像头" value="摄像头" />
<el-option label="控制器" value="控制器" />
</el-select>
</el-col>
<el-col :span="2">
<el-select v-model="searchForm.status" placeholder="设备状态" clearable>
<el-option label="全部状态" value="" />
<el-option label="正常" value="normal" />
<el-option label="异常" value="abnormal" />
<el-option label="中断" value="interrupt" />
</el-select>
</el-col>
<el-col :span="2">
<el-select v-model="searchForm.protocol" placeholder="通讯状态" clearable>
<el-option label="全部状态" value="" />
<el-option label="Modbus TCP" value="Modbus TCP" />
<el-option label="其他协议" value="其他" />
</el-select>
</el-col>
<el-col :span="2">
<el-select v-model="searchForm.station" placeholder="所属电站" clearable>
<el-option label="全部电站" value="" />
<el-option label="兴电基站1" value="兴电基站1" />
<el-option label="兴电基站2" value="兴电基站2" />
<el-option label="兴电基站3" value="兴电基站3" />
<el-option label="兴电基站4" value="兴电基站4" />
<el-option label="兴电基站5" value="兴电基站5" />
</el-select>
</el-col>
<el-col :span="2">
<el-button type="primary" @click="handleSearch" style="width: 100%">
<el-icon><Search /></el-icon>
搜索
</el-button>
</el-col>
<el-col :span="3">
<el-button type="primary" @click="handleAddDevice" style="width: 100%">
<el-icon><CirclePlus /></el-icon>
添加设备
</el-button>
</el-col>
<el-col :span="3">
<el-button type="primary" @click="handleBatchConfig" style="width: 100%">
<el-icon><Setting /></el-icon>
批量配置
</el-button>
</el-col>
</el-row>
</div>
<!-- 设备信息表格 -->
<el-table v-loading="loading" :data="deviceList" style="width: 100%">
<el-table-column prop="deviceId" label="设备ID" min-width="120" align="center" />
<el-table-column prop="deviceName" label="设备名称" min-width="120" align="center" />
<el-table-column prop="deviceType" label="类型" min-width="100" align="center">
<template #default="scope">
<el-tag :type="getDeviceTypeTagType(scope.row.deviceType)" :effect="'light'">
{{ scope.row.deviceType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="station" label="所属电站" min-width="120" align="center" />
<el-table-column prop="protocol" label="通讯协议" min-width="100" align="center" />
<el-table-column prop="ipAddress" label="IP地址" min-width="120" align="center" />
<el-table-column prop="lastOnlineTime" label="最后在线时间" min-width="150" align="center" />
<el-table-column prop="status" label="状态" min-width="80">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status)" :effect="light">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="150" fixed="right">
<template #default="scope">
<span style="color: #1890ff; cursor: pointer; margin-right: 15px;" @click="handleDetails(scope.row)">
查看
</span>
<span style="color: #666666; cursor: pointer; margin-right: 15px;" @click="handleConfig(scope.row)">
配置
</span>
<span style="color: #666666; cursor: pointer;" @click="handleDelete(scope.row)">
删除
</span>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<div class="pagination-container">
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]" layout="prev, pager, next, jumper"
:total="pagination.total" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</div>
</template>
<script setup>
import { Search, CirclePlus, Setting } from '@element-plus/icons-vue';
import { ref, reactive, watch } from 'vue';
// 定义props接收数据
const props = defineProps({
tableData: {
type: Object,
default: () => ({
list: [],
total: 0
})
}
});
// 搜索表单数据
const searchForm = reactive({
deviceType: '',
station: '',
protocol: '',
status: '',
keyword: ''
});
// 表格加载状态
const loading = ref(false);
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
});
// 设备列表数据
const deviceList = ref([]);
// 监听props变化并更新设备列表
watch(() => props.tableData, (newData) => {
deviceList.value = newData.list || [];
pagination.total = newData.total || 0;
}, { immediate: true, deep: true });
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
normal: '正常',
interrupt: '中断',
abnormal: '异常'
};
return statusMap[status] || status;
};
// 获取状态标签类型
const getStatusTagType = (status) => {
const typeMap = {
normal: 'success',
interrupt: 'warning',
abnormal: 'danger'
};
return typeMap[status] || 'default';
};
// 获取设备类型标签类型
const getDeviceTypeTagType = (deviceType) => {
const typeMap = {
'逆变器': 'primary',
'传感器': 'success',
'电表': 'warning',
'摄像头': 'info',
'控制器': 'danger'
};
return typeMap[deviceType] || 'default';
};
// 处理搜索
const handleSearch = () => {
loading.value = true;
pagination.currentPage = 1;
// 模拟搜索请求
setTimeout(() => {
loading.value = false;
ElMessage.success('搜索成功');
// 实际项目中这里应该调用API获取数据
}, 500);
};
// 处理添加设备
const handleAddDevice = () => {
// 实际项目中这里应该打开添加设备的弹窗或跳转到添加页面
ElMessage.success('打开添加设备窗口');
};
// 处理批量配置
const handleBatchConfig = () => {
// 实际项目中这里应该打开批量配置的弹窗
ElMessage.success('打开批量配置窗口');
};
// 处理查看详情
const handleDetails = (row) => {
// 实际项目中这里应该打开设备详情的弹窗或跳转到详情页面
ElMessage.success(`查看设备${row.deviceId}详情`);
};
// 处理配置
const handleConfig = (row) => {
// 实际项目中这里应该打开设备配置的弹窗
ElMessage.success(`配置设备${row.deviceId}`);
};
// 处理删除
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除设备${row.deviceId}吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(() => {
// 实际项目中这里应该调用API删除设备
ElMessage.success('删除成功');
})
.catch(() => {
ElMessage.info('已取消删除');
});
};
// 处理分页大小变化
const handleSizeChange = (size) => {
pagination.pageSize = size;
// 实际项目中这里应该重新请求数据
};
// 处理分页页码变化
const handleCurrentChange = (current) => {
pagination.currentPage = current;
// 实际项目中这里应该重新请求数据
};
</script>
<style scoped>
.manage-form-container {
background-color: #fff;
padding: 20px;
border-radius: 8px;
min-height: 100%;
}
.search-filter-section {
margin-bottom: 24px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
}
/* 下拉选择框样式优化 */
:deep(.el-select) {
width: 100%;
}
:deep(.el-input__wrapper) {
border-radius: 6px;
}
/* 按钮样式优化 */
:deep(.el-button) {
border-radius: 6px;
font-size: 14px;
transition: all 0.3s ease;
}
:deep(.el-button--primary) {
background-color: #1890ff;
border-color: #1890ff;
}
:deep(.el-button--primary:hover) {
background-color: #40a9ff;
border-color: #40a9ff;
}
/* 响应式设计 */
@media screen and (max-width: 1200px) {
.search-filter-section .el-col {
margin-bottom: 12px;
}
}
@media screen and (max-width: 768px) {
.search-filter-section {
padding: 12px;
}
.search-filter-section .el-row {
display: flex;
flex-direction: column;
}
.search-filter-section .el-col {
width: 100% !important;
margin-bottom: 12px;
}
}
.action-buttons {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
align-items: center;
}
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.el-table__header-wrapper) {
background-color: #fafafa;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
/* 分页样式优化 */
:deep(.el-pagination) {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,195 @@
<template>
<div class="chart-container">
<div ref="chartRef" class="chart-content"></div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts';
// 定义props接收数据
const props = defineProps({
trendData: {
type: Object,
default: () => ({
dates: [],
series: []
})
}
});
// 图表DOM引用
const chartRef = ref(null);
// 图表实例
let chartInstance = null;
// 初始化图表
const initChart = () => {
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
legend: {
show: true,
left: '8%',
icon: 'square',
itemWidth: 12,
itemHeight: 12,
textStyle: {
color: '#4E5969'
}
},
xAxis: {
type: 'category',
axisTick: {
show: false
},
axisLabel: {
textStyle: {
color: '#4E5969'
}
},
axisLine: {
lineStyle: {
color: '#EAEBF0'
}
},
data: props.trendData.dates || []
},
yAxis: {
type: 'value',
max: 150,
interval: 50,
axisLabel: {
textStyle: {
color: '#4E5969'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#EAEBF0',
type: 'dashed'
}
}
},
series: props.trendData.series.map((item, index) => ({
name: item.name,
data: item.data,
type: 'bar',
stack: 'one',
color: item.color,
itemStyle: {
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 1)',
barBorderRadius: 8
},
barWidth: index === 0 ? '20' : index === 2 ? '12' : '20',
}))
}
chartInstance.setOption(option);
};
// 响应窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
};
});
</script>
<style scoped>
.chart-container {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
height: 400px;
width: 100%;
padding: 5px;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
}
.chart-header h2 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-content {
width: 100%;
height: 100%;
padding: 5px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.chart-container {
height: 350px;
}
}
@media (max-width: 480px) {
.chart-container {
height: 300px;
}
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.chart-actions {
width: 100%;
display: flex;
justify-content: space-between;
}
.chart-actions button {
margin: 0;
flex: 1;
margin-right: 5px;
}
.chart-actions button:last-child {
margin-right: 0;
}
}
</style>

View File

@ -0,0 +1,229 @@
<template>
<div class="chart-container">
<!--组件温度 图表内容区域 -->
<div ref="chartRef" class="chart-content"></div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts';
// 定义props接收数据
const props = defineProps({
pieData: {
type: Object,
default: () => ({
normal: {
value: 28,
name: '提示信息',
displayName: '设备正常'
},
interrupt: {
value: 45,
name: '一般告警',
displayName: '设备中断'
},
abnormal: {
value: 55,
name: '重要告警',
displayName: '设备异常'
}
})
}
});
// 默认的三种颜色
const defaultColors = {
normal: '#43CF7C',
interrupt: '#00B3FF',
abnormal: '#FB3E7A'
};
// 图表DOM引用
const chartRef = ref(null);
// 图表实例
let chartInstance = null;
// 初始化图表
const initChart = () => {
if (chartRef.value && !chartInstance) {
chartInstance = echarts.init(chartRef.value);
}
const option = {
tooltip: {
trigger: 'item',
formatter: function(params) {
// 使用ECharts提供的百分比值
const percentage = params.percent.toFixed(1);
// 返回格式化后的文本
return `${params.data.displayName}: ${params.value}台 (${percentage}%)`;
}
},
grid: {
left: '0%',
right: '20%',
bottom: '0%',
top: '0%',
containLabel: true
},
legend: {
top: 'middle',
orient: 'vertical',
right: '5%', // 调整图例位置,使其更靠近左侧
itemWidth: 15,
itemHeight: 15,
formatter: function(name) {
const data = props.pieData.normal.name === name ? props.pieData.normal :
props.pieData.interrupt.name === name ? props.pieData.interrupt :
props.pieData.abnormal;
// 返回格式化后的文本
return `${data.displayName}(${data.value})`;
}
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
center:['40%','50%'],
label: {
show: false
},
labelLine: {
show: true
},
color: [
defaultColors.normal,
defaultColors.interrupt,
defaultColors.abnormal
],
data: [
{
value: props.pieData.normal.value,
name: props.pieData.normal.name,
displayName: props.pieData.normal.displayName
},
{
value: props.pieData.interrupt.value,
name: props.pieData.interrupt.name,
displayName: props.pieData.interrupt.displayName
},
{
value: props.pieData.abnormal.value,
name: props.pieData.abnormal.name,
displayName: props.pieData.abnormal.displayName
}
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
chartInstance.setOption(option);
};
// 响应窗口大小变化
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 生命周期钩子
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
};
});
</script>
<style scoped>
.chart-container {
background-color: #fff;
border-radius: 8px;
overflow: hidden;
height: 250px;
width: 100%;
padding: 5px;
box-sizing: border-box;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
}
.chart-header h2 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.chart-content {
width: 100%;
height: 100%;
padding: 5px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.chart-container {
height: 350px;
}
}
@media (max-width: 480px) {
.chart-container {
height: 300px;
}
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.chart-actions {
width: 100%;
display: flex;
justify-content: space-between;
}
.chart-actions button {
margin: 0;
flex: 1;
margin-right: 5px;
}
.chart-actions button:last-child {
margin-right: 0;
}
}
.model {
padding: 20px;
background-color: rgba(242, 248, 252, 1);
}
</style>

View File

@ -0,0 +1,174 @@
<template>
<div class="chart-container">
<el-row style="padding: 0 0 0 20px;box-sizing: border-box;">
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/rebot.png" alt="">
</div>
<div class="item-title">设备总数</div>
<div class="item-value">{{ totalData.deviceCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/up.png" alt="">
<span class="trend-num">+{{ totalData.increase || 8 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/wifi.png" alt="">
</div>
<div class="item-title">正常设备</div>
<div class="item-value">{{ totalData.normalCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/up.png" alt="">
<span class="trend-num">+{{ totalData.normalIncrease || 5 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/wifiwarn.png" alt="">
</div>
<div class="item-title">异常设备</div>
<div class="item-value">{{ totalData.abnormalCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/down.png" alt="">
<span class="trend-num">-{{ totalData.abnormalDecrease || 3 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
<el-col :span="6">
<div class="item-box">
<div class="item-icon">
<img src="@/assets/demo/nowifi.png" alt="">
</div>
<div class="item-title">中断设备</div>
<div class="item-value">{{ totalData.interruptCount }}</div>
<div class="item-unit"></div>
<div class="item-trend">
<img src="@/assets/demo/down.png" alt="" class="trend-icon">
<span class="trend-num">-{{ totalData.interruptDecrease || 2 }}</span>
<span class="trend-des">较上月同期</span>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 定义props接收数据
const props = defineProps({
totalData: {
type: Object,
default: () => ({
deviceCount: 0,
normalCount: 0,
abnormalCount: 0,
interruptCount: 0,
increase: 0,
normalIncrease: 0,
abnormalDecrease: 0,
interruptDecrease: 0
})
}
});
</script>
<style scoped lang="scss">
.item-box {
padding: 20px;
width: 216px;
height: 282px;
border-radius: 10px;
// border: 1px solid red;
background: rgba(255, 255, 255, 1);
display: flex;
flex-direction: column;
justify-content: space-between;
.item-icon {
width: 49.94px;
height: 49.91px;
border-radius: 8px;
background: rgba(24, 109, 245, 1);
display: flex;
align-items: center;
justify-content: center;
}
.item-title {
width: 129.85px;
height: 21.21px;
/** 文本1 */
font-size: 16px;
font-weight: 500;
letter-spacing: 0px;
line-height: 23.17px;
color: rgba(182, 182, 182, 1);
text-align: left;
vertical-align: top;
}
.item-value {
border-radius: 10px;
background: rgba(255, 255, 255, 1);
font-size: 24px;
font-weight: 400;
letter-spacing: 0px;
line-height: 34.75px;
color: rgba(0, 0, 0, 1);
text-align: left;
vertical-align: top;
}
.item-unit {
width: 38.71px;
height: 21.21px;
opacity: 1;
/** 文本1 */
font-size: 16px;
font-weight: 500;
letter-spacing: 0px;
line-height: 23.17px;
color: rgba(182, 182, 182, 1);
text-align: left;
vertical-align: top;
}
.item-trend {
.trend-num {
font-size: 14px;
font-weight: 500;
letter-spacing: 0px;
line-height: 14.48px;
color: rgba(0, 184, 122, 1);
text-align: center;
vertical-align: middle;
margin-right: 10px;
margin-left: 10px;
}
.trend-des {
font-size: 14px;
font-weight: 500;
letter-spacing: 0px;
line-height: 14.48px;
color: rgba(154, 154, 154, 1);
text-align: left;
vertical-align: middle;
}
}
}
</style>

View File

@ -0,0 +1,296 @@
<template>
<div class="model">
<!-- 标题栏 -->
<el-row :gutter="24">
<el-col :span="12">
<TitleComponent title="设备状态管理" subtitle="监控和管理所有设备的运行状态" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<el-col :span="4">
<el-button type="primary">
导出数据
<el-icon class="el-icon--right">
<UploadFilled />
</el-icon>
</el-button>
</el-col>
</el-col>
</el-row>
<!-- 第一行设备统计和状态分布 -->
<el-row :gutter="20" class="content-row equal-height-row">
<el-col :span="16">
<el-card shadow="hover" class="custom-card">
<totalView :totalData="totalData" />
</el-card>
</el-col>
<el-col :span="8">
<!-- 设备状态分布 -->
<el-card shadow="hover" class="custom-card">
<TitleComponent title="设备状态分布" :font-level="2" />
<statusPie :pieData="pieData" />
</el-card>
</el-col>
</el-row>
<!-- 第二行设备状态趋势 -->
<el-row :gutter="20" class="content-row">
<el-col :span="24">
<el-card shadow="hover" class="custom-card">
<TitleComponent title="设备状态趋势" :font-level="2" />
<stateTrend :trendData="trendData" />
</el-card>
</el-col>
</el-row>
<!-- 第三行设备管理表单 -->
<el-row :gutter="20" class="content-row">
<el-col :span="24">
<el-card shadow="hover" class="custom-card">
<TitleComponent title="设备管理表单" :font-level="2" />
<manageForm :tableData="tableData" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref } from 'vue';
import TitleComponent from '@/components/TitleComponent/index.vue';
import totalView from '@/views/integratedManage/stateManage/components/totalView.vue';
import stateTrend from '@/views/integratedManage/stateManage/components/stateTrend.vue'
import statusPie from '@/views/integratedManage/stateManage/components/statusPie.vue'
import manageForm from '@/views/integratedManage/stateManage/components/manageForm.vue';
// Mock数据 - 设备统计数据
const totalData = ref({
deviceCount: 545,
normalCount: 436,
abnormalCount: 65,
interruptCount: 44,
increase: 8,
normalIncrease: 5,
abnormalDecrease: 3,
interruptDecrease: 2
});
// Mock数据 - 饼图数据
const pieData = ref({
normal: {
value: 436,
name: 'normal',
displayName: '设备正常',
percent: '80%'
},
abnormal: {
value: 65,
name: 'abnormal',
displayName: '设备异常',
percent: '12%'
},
interrupt: {
value: 44,
name: 'interrupt',
displayName: '设备中断',
percent: '8%'
}
});
// Mock数据 - 趋势图数据
const trendData = ref({
dates: ['9-12', '9-13', '9-14', '9-15', '9-16', '9-17', '9-18'],
series: [
{
name: '正常',
data: [20, 10, 50, 80, 70, 10, 30],
color: '#7339F5'
},
{
name: '中断',
data: [80, 30, 50, 80, 70, 10, 30],
color: '#FF8A00'
},
{
name: '异常',
data: [50, 30, 50, 80, 70, 10, 30],
color: '#DE4848'
}
]
});
// Mock数据 - 表格数据
const tableData = ref({
list: [
{
deviceId: 'WO-2023-0620-056',
deviceName: '逆变器-01',
deviceType: '逆变器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-057',
deviceName: '温度传感器-45',
deviceType: '传感器',
station: '兴电基站2',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'interrupt'
},
{
deviceId: 'WO-2023-0620-058',
deviceName: '智能电表-03',
deviceType: '电表',
station: '兴电基站3',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'abnormal'
},
{
deviceId: 'WO-2023-0620-059',
deviceName: '监控摄像头-02',
deviceType: '摄像头',
station: '兴电基站4',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-060',
deviceName: '控制器-07',
deviceType: '控制器',
station: '兴电基站5',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-061',
deviceName: '逆变器-02',
deviceType: '逆变器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-062',
deviceName: '电流传感器-08',
deviceType: '传感器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-063',
deviceName: '多功能电表-12',
deviceType: '电表',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-064',
deviceName: '门禁摄像头-05',
deviceType: '摄像头',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
},
{
deviceId: 'WO-2023-0620-065',
deviceName: '开关控制器-15',
deviceType: '控制器',
station: '兴电基站1',
protocol: 'Modbus TCP',
ipAddress: '192.168.1.101',
lastOnlineTime: '2023-06-30 17:00',
status: 'normal'
}
],
total: 545
});
</script>
<style scoped>
.model {
padding: 20px 15px;
background-color: rgba(242, 248, 252, 1);
}
.content-row {
margin-bottom: 20px;
}
.custom-card {
border-radius: 8px;
transition: all 0.3s ease;
border: none;
}
.custom-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.equal-height-row {
display: flex;
align-items: stretch;
}
.equal-height-row .el-col {
display: flex;
}
.equal-height-row .custom-card {
flex: 1;
display: flex;
flex-direction: column;
}
.equal-height-row .el-card__body {
flex: 1;
display: flex;
flex-direction: column;
}
/* 响应式布局调整 */
@media (max-width: 1200px) {
.content-row {
margin-bottom: 15px;
}
.equal-height-row {
flex-direction: column;
}
.equal-height-row .el-col {
width: 100%;
margin-bottom: 20px;
}
}
@media (max-width: 768px) {
.model {
padding: 15px 10px;
}
.content-row {
margin-bottom: 10px;
}
}
</style>

View File

@ -0,0 +1,318 @@
<template>
<div class="centerPage">
<div class="centerPage_list">
<div class="card">
<div class="title">今日总发电量</div>
<div class="value">
<span style="color: rgba(29, 214, 255, 1)">{{ data?.dayEnergy ?? '0' }}</span>
<span style="color: rgba(156, 163, 175, 1); font-size: 12px">kMh</span>
<div class="icon">
<img src="@/assets/large/center4.png" style="width: 16px; height: 16px" alt="" />
</div>
</div>
<div class="compare" v-if="Number(data?.dayEnergy) - Number(data?.dayEnergyOld) != 0">
<el-icon color="rgba(0, 227, 150, 1)" v-if="Number(data?.dayEnergy) - Number(data?.dayEnergyOld) > 0"><Top /></el-icon>
<el-icon color="rgba(255, 77, 79, 1)" v-else><Bottom /></el-icon>
<span>{{ Number(data?.dayEnergy) - Number(data?.dayEnergyOld) > 0 ? '新增' : '减少' }}</span>
<span>{{ (Math.abs(Number(data?.dayEnergy) - Number(data?.dayEnergyOld)) / Number(data?.dayEnergy)) * 100 }} %</span>
</div>
<div class="compare" v-else></div>
<div class="target">目标: 14,200 kWh</div>
</div>
<div class="card">
<div class="title">发电效率</div>
<div class="value">
<span style="color: rgba(0, 227, 150, 1)">{{ data?.generateElectricity ?? '0' }}</span>
<span style="color: rgba(156, 163, 175, 1); font-size: 12px">%</span>
<div class="icon">
<img src="@/assets/large/center3.png" style="width: 16px; height: 16px" alt="" />
</div>
</div>
<div class="compare" v-if="Number(data?.generateElectricity) - Number(data?.generateElectricityOld) != 0">
<el-icon color="rgba(0, 227, 150, 1)" v-if="Number(data?.generateElectricity) - Number(data?.generateElectricityOld) > 0"><Top /></el-icon>
<el-icon color="rgba(255, 77, 79, 1)" v-else><Bottom /></el-icon>
<span>{{ Number(data?.generateElectricity) - Number(data?.generateElectricityOld) > 0 ? '新增' : '减少' }}</span>
<span
>{{
(Math.abs(Number(data?.generateElectricity) - Number(data?.generateElectricityOld)) / Number(data?.generateElectricity)) * 100
}}
%</span
>
</div>
<div class="compare" v-else></div>
<div class="target">基准: 90.0%</div>
</div>
<div class="card">
<div class="title">设备健康度</div>
<div class="value">
<span style="color: rgba(54, 207, 201, 1)">{{ data?.health ?? '0' }}</span>
<span style="color: rgba(156, 163, 175, 1); font-size: 12px">%</span>
<div class="icon">
<img src="@/assets/large/center2.png" style="width: 16px; height: 16px" alt="" />
</div>
</div>
<div class="compare" v-if="Number(data?.health) - Number(data?.healthOld) != 0">
<el-icon color="rgba(0, 227, 150, 1)" v-if="Number(data?.health) - Number(data?.healthOld) > 0"><Top /></el-icon>
<el-icon color="rgba(255, 77, 79, 1)" v-else><Bottom /></el-icon>
<span>{{ Number(data?.health) - Number(data?.healthOld) > 0 ? '新增' : '减少' }}</span>
<span>{{ (Math.abs(Number(data?.health) - Number(data?.healthOld)) / Number(data?.health)) * 100 }} %</span>
</div>
<div class="compare" v-else></div>
<div class="target">检测: 24分钟前</div>
</div>
<div class="card">
<div class="title">CO2减排量</div>
<div class="value">
<span style="color: rgba(179, 0, 255, 1)">{{ data?.powerStationAvoidedCo2 ?? '0' }}</span>
<span style="color: rgba(156, 163, 175, 1); font-size: 12px"></span>
<div class="icon">
<img src="@/assets/large/center1.png" style="width: 16px; height: 16px" alt="" />
</div>
</div>
<div class="compare" v-if="Number(data?.powerStationAvoidedCo2) - Number(data?.powerStationAvoidedCo2Old) != 0">
<el-icon color="rgba(0, 227, 150, 1)" v-if="Number(data?.powerStationAvoidedCo2) - Number(data?.powerStationAvoidedCo2Old) > 0"
><Top
/></el-icon>
<el-icon color="rgba(255, 77, 79, 1)" v-else><Bottom /></el-icon>
<span>{{ Number(data?.powerStationAvoidedCo2) - Number(data?.powerStationAvoidedCo2Old) > 0 ? '新增' : '减少' }}</span>
<span
>{{
(Math.abs(Number(data?.powerStationAvoidedCo2) - Number(data?.powerStationAvoidedCo2Old)) / Number(data?.powerStationAvoidedCo2)) * 100
}}
%</span
>
</div>
<div class="compare" v-else></div>
<div class="target">目标: 12560</div>
</div>
</div>
<div class="centerPage_map">
<div ref="mapRef" class="map-container" style="width: 100%; height: 98%" />
</div>
</div>
</template>
<script setup lang="ts">
import { getPowerStationOverview } from '@/api/large';
import * as echarts from 'echarts';
import china from '@/assets/china.json';
const data = ref<any>({});
// 地图容器引用
const mapRef = ref<HTMLDivElement | null>(null);
// ECharts实例
let myChart: any = null;
// 响应窗口大小变化
const handleResize = () => {
if (myChart) {
myChart.resize();
}
};
// 初始化地图
const initEcharts = () => {
if (!mapRef.value) {
console.error('未找到地图容器元素');
return;
}
// 注册地图
echarts.registerMap('china', china as any);
// 地图数据
const mapData: any = [{ name: '田东县', value: 1, itemStyle: { color: '#fff' } }];
// 散点数据
// 散点数据 - 使用图片标记并调整名称位置
const scatterData: any[] = [
{
name: '田东光伏智慧生态工地开发项目',
value: [107.15, 23.63],
// 使用图片作为标记(注意:需替换为你的图片实际路径)
symbol: 'diamond',
// 标记颜色
itemStyle: {
color: '#0166d6'
},
// 图片标记大小(宽, 高)
symbolSize: [20, 20],
// 名称样式设置
label: {
show: true,
formatter: '{b}', // 显示名称
position: 'top', // 名称在图片上方
color: '#fff',
fontSize: 12,
// 可选:添加文字背景以增强可读性
backgroundColor: 'rgba(3, 26, 52, 0.7)',
padding: [3, 6],
borderRadius: 3
}
}
];
// 初始化新实例,强制清除缓存
myChart = echarts.init(mapRef.value, null, {
renderer: 'canvas', // 明确指定渲染器
useDirtyRect: false // 禁用脏矩形渲染,避免样式缓存
});
// 配置项
const option: any = {
roam: true, // 关键配置:允许鼠标滚轮缩放和拖拽平移
geo: {
type: 'map',
map: 'china',
zoom: 5,
center: [107.15, 23.63],
label: {
show: false,
color: '#fff'
},
itemStyle: {
areaColor: '#031a34', // 地图区域底色
borderColor: '#1e3a6e', // 区域边框颜色
borderWidth: 1
}
},
tooltip: {
trigger: 'item',
formatter: function (params: any) {
return params.name + (params.value ? `${params.value}` : '');
}
},
series: [
{
type: 'map',
map: 'china',
geoIndex: 0,
// 关键在series级别定义emphasis优先级最高
emphasis: {
areaColor: '#fff', // 强制设置hover颜色
label: {
show: true,
color: '#fff'
},
itemStyle: {
areaColor: '#02417e' // 重复设置确保生效
}
},
// 确保没有使用默认样式
select: {
itemStyle: {
areaColor: '#02417e'
}
},
data: mapData
},
{
type: 'scatter',
coordinateSystem: 'geo',
data: scatterData
}
]
};
// 设置配置项
myChart.setOption(option);
};
// 组件挂载时初始化
onMounted(() => {
// 确保DOM渲染完成
nextTick(() => {
initEcharts();
window.addEventListener('resize', handleResize);
});
});
// 组件卸载时清理
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (myChart) {
myChart.dispose();
myChart = null;
}
});
const getDataList = () => {
getPowerStationOverview().then((res) => {
console.log(res);
if (res.code == 200) {
data.value = res.data;
}
});
};
getDataList();
</script>
<style scoped lang="scss">
.centerPage {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0 10px 10px 10px;
box-sizing: border-box;
.centerPage_list {
width: 100%;
height: 20%;
padding: 0 0px 10px 0px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 13px;
.card {
width: 220px;
background: rgba(12, 30, 53, 0.4); /* 深色背景,模拟科技感 */
color: #fff;
border-radius: 8px;
padding: 16px;
font-family: sans-serif;
}
.title {
font-size: 14px;
margin-bottom: 8px;
opacity: 0.8;
}
.value {
font-size: 24px;
// font-weight: bold;
display: flex;
align-items: flex-end;
}
.value span {
margin-right: 15px;
}
.icon {
width: 40px;
height: 40px;
background: rgba(29, 214, 255, 0.1);
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.compare {
font-size: 12px;
margin-top: 4px;
color: #4ee44e; /* 绿色标识增长 */
padding-top: 20px;
display: flex;
align-items: center;
// justify-content: center;
}
.target {
font-size: 12px;
margin-top: 4px;
opacity: 0.8;
}
}
.centerPage_map {
width: 100%;
height: 80%;
}
}
</style>

View File

@ -0,0 +1,180 @@
<template>
<div class="header">
<div class="header_left">
<div class="header_left_img">
<img src="@/assets/large/secure.png" style="width: 100%; height: 100%" />
</div>
<div style="font-size: 12px; padding-left: 10px">安全生产天数</div>
<div class="header_left_text">
1,235
<span style="font-size: 12px"></span>
</div>
</div>
<div class="title">
<div class="title_text">智慧运维管理平台</div>
<div>Intelligent Operations Management Platform</div>
</div>
<div class="right">
<div class="top-bar">
<!-- 左侧天气图标 + 日期文字 -->
<div class="left-section">
<img src="@/assets/large/weather.png" alt="天气图标" />
<span>
<span>多云 9°/18°</span>
<span style="padding-left: 20px"> {{ week[date.week] }} {{ date.ymd }}</span>
</span>
</div>
<!-- 分割线 -->
<div class="divider">
<div class="top-block"></div>
<div class="bottom-block"></div>
</div>
<!-- 右侧管理系统图标 + 文字 -->
<div class="right-section">
<img src="@/assets/large/setting.png" alt="设置图标" />
<span>管理系统</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const date = ref({
ymd: '',
hms: '',
week: 0
});
const setTime = () => {
let date1 = new Date();
let year: any = date1.getFullYear();
let month: any = date1.getMonth() + 1;
let day: any = date1.getDate();
let hours: any = date1.getHours();
if (hours < 10) {
hours = '0' + hours;
}
let minutes: any = date1.getMinutes();
if (minutes < 10) {
minutes = '0' + minutes;
}
let seconds: any = date1.getSeconds();
if (seconds < 10) {
seconds = '0' + seconds;
}
date.value.ymd = year + '-' + month + '-' + day;
date.value.hms = hours + ':' + minutes + ':' + seconds;
date.value.week = date1.getDay();
};
// 添加定时器,每秒更新一次时间
const timer = setInterval(setTime, 1000);
// 组件卸载时清除定时器
onUnmounted(() => {
clearInterval(timer);
});
</script>
<style scoped lang="scss">
.header {
width: 100%;
height: 80px;
box-sizing: border-box;
padding: 10px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
color: #fff;
}
.header_left {
display: flex;
align-items: center;
.header_left_img {
width: 48px;
height: 48px;
box-sizing: border-box;
// padding-right: 10px;
}
.header_left_text {
font-weight: 500;
text-shadow: 0px 1.24px 6.21px rgba(25, 179, 250, 1);
}
}
.title {
color: #fff;
font-family: 'Rang_men_zheng_title', sans-serif;
text-align: center;
}
.title > div:first-child {
/* 第一个子元素的样式 */
font-size: 38px;
// font-weight: 300;
}
.title > div:last-child {
/* 最后一个子元素的样式 */
font-size: 14px;
letter-spacing: 0.3em; /* 调整这个值来控制间距大小 */
}
.right {
width: 100%;
height: 100%;
display: flex;
}
/* 顶部栏容器Flex 水平布局 + 垂直居中 */
.top-bar {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
// background-color: #1e2128;
color: #fff;
padding: 8px 16px;
font-size: 14px;
}
/* 左侧区域(天气 + 日期):自身也用 Flex 水平排列,确保元素在一行 */
.left-section {
display: flex;
align-items: center;
// margin-right: auto; /* 让右侧元素(管理系统)居右 */
}
.left-section img {
width: 32px;
height: 32px;
margin-right: 8px; /* 图标与文字间距 */
}
/* 分割线(视觉分隔,可根据需求调整样式) */
.divider {
display: grid;
grid-template-rows: 1fr 1fr;
height: 100%; /* 根据需要调整高度 */
padding: 15px 10px;
}
.divider .top-block {
width: 3px;
height: 7px;
background: #19b5fb;
align-self: start;
}
.divider .bottom-block {
width: 3px;
height: 7px;
background: #19b5fb;
align-self: end;
}
/* 右侧区域(管理系统):图标 + 文字水平排列 */
.right-section {
display: flex;
align-items: center;
font-family: 'Rang_men_zheng_title', sans-serif;
font-size: 20px;
}
.right-section img {
width: 20px;
height: 20px;
margin-right: 6px; /* 图标与文字间距 */
}
</style>

View File

@ -0,0 +1,616 @@
<template>
<div class="left_page">
<div class="power">
<div class="left_title">
<div style="display: flex; align-items: center">
<div class="left_title_img">
<img src="@/assets/large/power.png" alt="" />
</div>
<div class="left_title_text">电站总览</div>
</div>
</div>
<div class="left_title_list">
<div class="left_title_item">
<div>总装机容量</div>
<div>
<span style="font-size: 24px; color: rgba(29, 214, 255, 1); padding-right: 10px">{{ data?.capacity ?? '0' }}</span>
<span style="color: rgba(156, 163, 175, 1)">MW</span>
</div>
<div style="display: flex; align-items: center" v-if="Number(data?.capacity) - Number(data.capacityOld) != 0">
<el-icon color="rgba(0, 227, 150, 1)" v-if="Number(data?.capacity) - Number(data.capacityOld) > 0"><Top /></el-icon>
<el-icon color="rgba(255, 77, 79, 1)" v-else><Bottom /></el-icon>
<span style="letter-spacing: 0.1em; color: rgba(0, 227, 150, 1)"
>{{ (Math.abs(Number(data?.capacity) - Number(data.capacityOld)) / Number(data?.capacity)) * 100 }}%较上月</span
>
</div>
<div v-else></div>
</div>
<div class="left_title_item">
<div>光伏板数量</div>
<div>
<span style="font-size: 24px; color: rgba(29, 214, 255, 1); padding-right: 10px">{{ data?.module ?? '0' }}</span>
<span style="color: rgba(156, 163, 175, 1)"></span>
</div>
<div style="display: flex; align-items: center">
<!-- <el-icon><Top /></el-icon>
<span style="letter-spacing: 0.1em; color: rgba(0, 227, 150, 1)">2.4%较上月</span> -->
<span style="letter-spacing: 0.1em; color: rgba(156, 163, 175, 1)">- -</span>
</div>
</div>
<div class="left_title_item">
<div>电站数量</div>
<div>
<span style="font-size: 24px; color: rgba(29, 214, 255, 1); padding-right: 10px">{{ data?.operatingRate ?? '0' }}</span>
<span style="color: rgba(156, 163, 175, 1)"></span>
</div>
<div style="display: flex; align-items: center" v-if="Number(data?.operatingRate) - Number(data?.operatingRateOld) != 0">
<el-icon color="rgba(0, 227, 150, 1)" v-if="Number(data?.operatingRate) - Number(data?.operatingRateOld) > 0"><Top /></el-icon>
<el-icon color="rgba(255, 77, 79, 1)" v-else><Bottom /></el-icon>
<span style="letter-spacing: 0.1em; color: rgba(0, 227, 150, 1)"
>{{ Math.abs(Number(data?.operatingRate) - Number(data?.operatingRateOld)) }}{{
Number(data?.operatingRate) - Number(data?.operatingRateOld) > 0 ? '新增' : '减少'
}}</span
>
</div>
<div v-else></div>
</div>
</div>
</div>
<div style="box-sizing: border-box; padding: 0 10px; border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 10px; margin-top: 5px">
<div class="inverter">
<div class="left_title">
<div style="display: flex; align-items: center">
<div class="left_title_img">
<img src="@/assets/large/monitor.png" alt="" />
</div>
<div class="left_title_text">逆变器监控</div>
</div>
</div>
<div class="selectTime">
<div class="tab-container">
<div class="tab active" @click="switchTab(1)"></div>
<div class="tab" @click="switchTab(2)"></div>
<div class="tab" @click="switchTab(3)"></div>
<!-- <div class="tab" @click="switchTab(4)"></div> -->
</div>
<el-date-picker
v-model="value1"
type="date"
placeholder="请选择"
size="small"
style="width: 120px"
@change="changeTime"
value-format="YYYY-MM-DD"
v-if="active == 1"
/>
<el-date-picker
v-model="value2"
type="month"
placeholder="请选择"
size="small"
style="width: 120px"
@change="changeTime"
value-format="YYYY-MM"
v-if="active == 2"
/>
<el-date-picker
v-model="value3"
type="year"
placeholder="请选择"
size="small"
style="width: 120px"
@change="changeTime"
value-format="YYYY"
v-if="active == 3"
/>
</div>
<div class="bix">运行状态</div>
<div class="chart-container">
<div ref="chart" style="width: 100%; height: 50px"></div>
</div>
<div class="left_title">
<div style="display: flex; align-items: center">
<div class="left_title_img">
<img src="@/assets/large/Inversion.png" alt="" />
</div>
<div class="left_title_text1">逆变器运行曲线</div>
</div>
</div>
<div class="date_select">
<el-select v-model="value" clearable placeholder="请选择" style="width: 120px; margin-left: 20px" size="small">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
<div class="brokenLine">
<EchartBoxTwo :option="lineOption" ref="lineChart"></EchartBoxTwo>
</div>
<div class="left_title">
<div style="display: flex; align-items: center">
<div class="left_title_img">
<img src="@/assets/large/income.png" alt="" />
</div>
<div class="left_title_text">能源收益分析</div>
</div>
</div>
<div class="income">
<EchartBoxTwo :option="barOption" ref="barChart"></EchartBoxTwo>
</div>
<div class="income_list">
<div style="display: flex; justify-content: space-between">
<div style="width: 50%">累计收益</div>
<div style="width: 50%; color: rgba(29, 214, 255, 1)">{{ Number(data2.allInCome).toFixed(2) }}</div>
</div>
<div style="display: flex">
<div style="width: 50%">本月收益</div>
<div style="width: 50%; color: rgba(0, 227, 150, 1)">{{ Number(data2.monthInCome).toFixed(2) }}</div>
</div>
<div style="display: flex">
<div style="width: 50%">度电成本</div>
<div style="width: 50%; color: rgba(54, 207, 201, 1)">{{ Number(data2.price).toFixed(2) }}</div>
</div>
<div style="display: flex">
<div style="width: 50%">预计年收入</div>
<div style="width: 50%; color: rgba(179, 0, 255, 1)">{{ Number(data2.yearInCome).toFixed(2) }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
import EchartBoxTwo from '@/components/EchartBox/index.vue';
import { formatDate } from '@/utils/index';
import { getLineOption, getBarOptions } from './optionList';
import { getPowerStationOverview, getStationMonthOverview, getInverterListOverview } from '@/api/large/index';
// 直接在组件内部定义数据
const chartData = ref({
normal: '', // 正常设备数量
abnormal: '', // 异常设备数量
fault: '' // 故障设备数量
});
const value1: any = ref('');
const value2: any = ref('');
const value3: any = ref('');
const value = ref('1');
const options = [
{
value: '1',
label: '交流有功功率'
}
];
const active: any = ref('1');
const data = ref<any>({});
const getDataList = () => {
getPowerStationOverview().then((res) => {
if (res.code == 200) {
data.value = res.data;
}
});
};
getDataList();
const changeTime = () => {
getEnergyData();
getInverterData();
};
const data2 = ref<any>({});
const getEnergyData = () => {
let date: any;
if (active.value == 2) {
date = value2.value;
value3.value = '';
value1.value = '';
} else if (active.value == 3) {
date = value3.value;
value1.value = '';
value2.value = '';
}
const today = new Date();
const formattedDate = `${today.getFullYear()}-${today.getMonth() + 1}`;
const params = {
type: active.value == 1 ? 2 : active.value,
date: date ? date : formattedDate
};
getStationMonthOverview(params).then((res) => {
if (res.code == 200) {
getTurnoverList(res.data.data);
data2.value = res.data;
}
});
};
const data3 = ref<any>({});
const getInverterData = () => {
let date: any;
if (active.value == 1) {
date = value1.value;
value2.value = '';
value3.value = '';
} else if (active.value == 2) {
date = value2.value;
value3.value = '';
value1.value = '';
} else if (active.value == 3) {
date = value3.value;
value1.value = '';
value2.value = '';
}
const today = new Date();
const formattedDate = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
const params = {
type: active.value,
date: date ? date : formattedDate
};
getInverterListOverview(params).then((res) => {
if (res.code == 200) {
pedestrianFlow(res.data.data);
chartData.value.fault = res.data.fault ?? 0;
chartData.value.normal = res.data.normal ?? 0;
chartData.value.abnormal = res.data.offline ?? 0;
renderChart();
}
});
};
const switchTab = (tabNumber: number) => {
const tabs = document.querySelectorAll('.tab');
tabs.forEach((tab) => tab.classList.remove('active'));
// 给对应数值的标签添加active类索引=数值-1
tabs[tabNumber - 1].classList.add('active');
// 可以根据数值执行不同的操作
active.value = tabNumber;
// getInverterData();
};
const chart = ref<HTMLElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const totalAll = ref(0);
// 计算百分比数据处理0值不占位
const calculatePercentages = () => {
const { normal, abnormal, fault } = chartData.value;
const total = Number(normal) + Number(abnormal) + Number(fault);
totalAll.value = total;
if (total === 0) {
return {
normal: 0,
abnormal: 0,
fault: 0
};
}
return {
normal: Number(normal) ?? 0,
abnormal: Number(abnormal) ?? 0,
fault: Number(fault) ?? 0
};
};
const lineOption = ref({});
const barOption = ref({});
const pedestrianFlow = (data?: any) => {
const xData = data.map((item) => item.time);
const yData = data.map((item) => item.content);
const lineData = {
xLabel: xData,
line1: yData
// line2: ['20', '50', '12', '65', '30', '60']
};
lineOption.value = getLineOption(lineData);
};
const getTurnoverList = (data?: any) => {
const xData = data.map((item) => item.time);
const yData = data.map((item) => {
// 先将content转换为数字再调用toFixed
const num = Number(item.content);
return isNaN(num) ? 0 : Number(num.toFixed(2));
});
const barData = {
name: xData,
value: yData
};
barOption.value = getBarOptions(barData);
};
// 初始化图表
const initChart = () => {
if (!chart.value) return;
chartInstance = echarts.init(chart.value);
getEnergyData();
getInverterData();
// pedestrianFlow();
// getTurnoverList();
};
// 渲染图表逆变器柱状图
const renderChart = () => {
// if (!chartInstance) return;
const percentages = calculatePercentages();
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `${params.marker} ${params.seriesName}: ${params.value}`;
}
},
legend: {
orient: 'horizontal',
right: 10,
top: 0,
data: [
{ name: '正常', icon: 'circle' },
{ name: '异常', icon: 'circle' },
{ name: '故障', icon: 'circle' }
],
textStyle: {
color: '#fff',
fontSize: 12
}
},
grid: {
left: '-3%',
right: '3%',
top: '30px',
bottom: '3%'
// containLabel: true
},
xAxis: {
type: 'value',
max: totalAll.value,
axisLabel: {
formatter: '{value}%',
show: false
},
splitLine: {
show: false
},
axisLine: { show: false }, // 隐藏轴线
axisTick: { show: false } // 隐藏刻度
},
yAxis: {
type: 'category',
show: false,
data: ['设备状态分布']
},
series: [
{
name: '正常',
type: 'bar',
stack: 'total',
data: [percentages.normal],
barWidth: 10,
itemStyle: {
color: 'rgba(0, 227, 150, 1)'
},
label: {
show: false,
position: 'insideLeft',
// formatter: `{c}%`,
color: '#fff',
fontWeight: 'bold'
}
},
{
name: '异常',
type: 'bar',
stack: 'total',
data: [percentages.abnormal],
barWidth: 10,
itemStyle: {
color: 'rgba(255, 171, 0, 1)'
},
label: {
show: false,
position: 'inside',
// formatter: `{c}%`,
color: '#fff',
fontWeight: 'bold'
}
},
{
name: '故障',
type: 'bar',
stack: 'total',
data: [percentages.fault],
barWidth: 10,
itemStyle: {
color: 'rgba(255, 77, 79, 1)'
},
label: {
show: false,
position: 'insideRight',
// formatter: `{c}%`,
color: '#fff',
fontWeight: 'bold'
}
}
]
};
chartInstance.setOption(option);
};
const lineChart = ref();
onMounted(() => {
initChart();
window.addEventListener('resize', () => chartInstance?.resize());
});
</script>
<style scoped lang="scss">
.left_page {
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0 10px 0 10px;
}
.power {
width: 100%;
// height: 20%;
box-sizing: border-box;
padding: 0 10px 10px 10px;
border: 1px solid rgba(29, 214, 255, 0.1);
border-radius: 10px;
.left_title_list {
width: 100%;
// padding-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.left_title_item {
width: 30%;
height: 100%;
display: flex;
flex-direction: column;
// align-items: center;
justify-content: space-between;
background-color: rgba(29, 214, 255, 0.1);
border-radius: 10px;
padding: 10px;
box-sizing: border-box;
:deep(.el-icon .top-icon) {
font-weight: bold;
}
}
.left_title_item > div:first-child {
/* 第一个子元素的样式 */
font-size: 12px;
padding-bottom: 5px;
}
.left_title_item > div:nth-child(2) {
/* 第二个子元素的样式 */
padding-bottom: 5px;
/* 添加其他需要的样式 */
}
.left_title_item > div:last-child {
/* 第一个子元素的样式 */
font-size: 12px;
}
}
}
.left_title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
.left_title_img {
height: 20px;
width: 20px;
}
.left_title_text {
font-size: 20px;
font-family: 'Rang_men_zheng_title', sans-serif;
display: flex;
align-items: flex-end;
margin-left: 15px;
padding-top: 2px;
box-sizing: border-box;
}
.left_title_text1 {
font-size: 14px;
display: flex;
align-items: flex-end;
margin-left: 15px;
padding-top: 2px;
box-sizing: border-box;
color: #fff;
}
}
.tab-container {
display: flex;
// gap: 4px;
font-size: 12px;
margin-right: 20px;
}
.tab {
padding: 4px;
border: 0.1px solid rgba(10, 79, 84, 1);
// border-radius: 6px;
cursor: pointer;
background-color: transparent;
// font-family: Arial, sans-serif;
transition: all 0.2s ease;
}
.tab.active {
background-color: #3b93fd;
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.tab:hover:not(.active) {
background-color: #3b93fd;
}
img {
width: 100%;
height: 100%;
}
.inverter {
width: 100%;
position: relative;
// height: 10%;
.selectTime {
position: absolute;
right: 0;
top: 12px;
display: flex;
align-items: center;
}
.bix {
position: absolute;
font-size: 12px;
color: rgba(156, 163, 175, 1);
top: 50px;
}
.date_select {
position: absolute;
top: 100px;
right: 0;
z-index: 9;
}
}
.chart-container {
width: 100%;
height: 50px;
}
.brokenLine {
width: 100%;
height: 23vh;
// margin-top: 10px;
}
.income {
width: 100%;
height: 24vh;
// margin-top: 20px;
}
.income_list {
width: 100%;
height: 7vh;
display: grid;
grid-template-columns: repeat(2, 1fr);
align-items: center; /* 垂直居中 */
// grid-gap: 10px;
// background-color: rgba(29, 214, 255, 0.1);
// border-radius: 10px;
padding: 0 10px;
box-sizing: border-box;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,735 @@
import * as echarts from 'echarts/core';
// import { PictorialBarChart } from 'echarts/charts'
// 客流量图
export const getOption = (xData: any, yData: any) => {
const data = {
xData,
yData
};
const maxData = Math.ceil(Math.max(...data.yData));
const barData = data.yData.map((item) => {
return maxData;
});
const option = {
grid: {
top: '10%',
left: '8%',
right: '5%',
bottom: '20%'
// containLabel: true
},
xAxis: {
type: 'category',
data: data.xData,
axisLine: {
show: false
},
axisTick: {
show: true
},
axisLabel: {
textStyle: {
color: '#fff'
}
}
},
yAxis: {
show: true,
type: 'value',
max: maxData,
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: 'rgba(73, 169, 191, 0.2)'
}
}
},
tooltip: {
trigger: 'axis',
backgroundColor: '',
textStyle: {
color: '#fff'
}
},
dataZoom: [
{
// show: true,
start: 0,
end: 30,
bottom: 2, // 下滑块距离x轴底部的距离
height: 23
},
{
type: 'inside'
}
],
series: [
{
name: '柱图',
type: 'bar',
// barWidth: '10%',
data: barData,
tooltip: {
show: false
},
barGap: '-50%',
itemStyle: {
normal: {
color: 'rgba(73, 169, 191, 0.2)'
}
}
},
{
name: '客单价',
type: 'line',
showAllSymbol: true,
symbol: 'circle',
symbolSize: 8,
lineStyle: {
normal: {
color: 'rgba(217, 231, 255, 0.3)',
shadowColor: 'rgba(0, 0, 0, .3)',
shadowBlur: 0
// shadowOffsetY: 5,
// shadowOffsetX: 5,
}
},
itemStyle: {
color: 'rgba(224, 194, 22, 1)',
borderWidth: 0,
shadowBlur: 0
},
label: {
show: false, // 显示数据标签
color: 'rgba(255, 208, 59, 1)'
},
data: data.yData
}
]
};
return option;
};
// 上菜分析图
export const getOption2 = (data: any) => {
const maxData = Math.max(...data.yData);
const option = {
// backgroundColor: "#38445E",
grid: {
left: '10%',
top: '13%',
bottom: '16%',
right: '10%'
},
xAxis: {
data: data.xData,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: 'rgba(255, 129, 109, 0.1)',
width: 1 //这里是为了突出显示加上的
}
},
axisLabel: {
textStyle: {
color: '#999',
fontSize: 12
}
}
},
yAxis: [
{
splitNumber: 2,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: 'rgba(255, 129, 109, 0.1)',
width: 1 //这里是为了突出显示加上的
}
},
axisLabel: {
textStyle: {
color: '#999'
}
},
splitArea: {
areaStyle: {
color: 'rgba(255,255,255,.5)'
}
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(255,255,255,.5)',
width: 0.5,
type: 'dashed'
}
}
}
],
dataZoom: [
{
// show: true,
start: 0,
end: 30,
bottom: 2, // 下滑块距离x轴底部的距离
height: 23
},
{
type: 'inside'
}
],
tooltip: {
trigger: 'axis', // 设置为 'item',表示鼠标悬浮在图形上时显示 tooltip
// formatter: function (params) {
// return `订单数: ${params.data}` // 显示鼠标悬浮项的数量
// },
backgroundColor: '', // 设置提示框的背景颜色
textStyle: {
color: '#fff' // 设置文字颜色
// fontSize: 14 // 设置文字大小
}
},
series: [
{
name: '订单数',
type: 'pictorialBar',
barCategoryGap: '0%',
symbol: 'path://M0,10 L10,10 C5.5,10 5.5,5 5,0 C4.5,5 4.5,10 0,10 z',
label: {
show: false,
position: 'top',
distance: 15,
color: 'rgba(255, 235, 59, 1)',
// fontWeight: "bolder",
fontSize: 16
},
itemStyle: {
normal: {
// color: {
// type: "linear",
// x: 0,
// y: 0,
// x2: 0,
// y2: 1,
// colorStops: [
// {
// offset: 0,
// color: "rgba(232, 94, 106, .8)", // 0% 处的颜色
// },
// {
// offset: 1,
// color: "rgba(232, 94, 106, .1)", // 100% 处的颜色
// },
// ],
// global: false, // 缺省为 false
// },
color: function (params: any) {
if (params.data === maxData) {
return 'rgba(255, 219, 103, 0.6)';
} else {
return 'rgba(239, 244, 255, 0.45)';
}
}
},
emphasis: {
opacity: 1
}
},
data: data.yData,
z: 10
}
]
};
return option;
};
//食堂周报图
export const getLineOption = (lineData: any) => {
const maxData = Math.ceil(Math.max(...lineData.line1));
const option = {
backgroundColor: '',
tooltip: {
trigger: 'axis',
backgroundColor: 'transparent',
color: '#7ec7ff',
textStyle: {
color: '#fff'
},
borderColor: '#7ec7ff'
},
// legend: {
// align: 'left',
// right: '5%',
// top: '1%',
// type: 'plain',
// textStyle: {
// color: '#fff',
// fontSize: 12
// },
// // icon:'rect',
// itemGap: 15,
// itemWidth: 18,
// data: [
// {
// name: '上周销售量'
// },
// {
// name: '本周销售量'
// }
// ]
// },
grid: {
top: '12%',
left: '1%',
right: '3%',
bottom: '12%',
containLabel: true
},
xAxis: {
type: 'category',
data: lineData.xLabel,
axisLine: {
show: false
},
axisTick: {
show: true
},
axisLabel: {
textStyle: {
color: '#fff'
}
}
},
yAxis: {
show: true,
type: 'value',
max: maxData,
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: 'rgba(73, 169, 191, 0.2)'
}
}
},
dataZoom: [
{
// show: true,
start: 0,
end: 30,
bottom: 2, // 下滑块距离x轴底部的距离
height: 23
},
{
type: 'inside'
}
],
series: [
{
name: '逆变器功率',
type: 'line',
symbol: 'circle', // 默认是空心圆(中间是白色的),改成实心圆
showAllSymbol: false,
symbolSize: 0,
smooth: true,
lineStyle: {
width: 1,
color: 'rgba(80, 164, 225, 1)', // 线条颜色
borderColor: 'rgba(0,0,0,.4)'
},
itemStyle: {
color: 'rgba(80, 164, 225, 1)',
borderWidth: 2,
show: false
},
tooltip: {
show: true
},
areaStyle: {
//线性渐变前4个参数分别是x0,y0,x2,y2(范围0~1);相当于图形包围盒中的百分比。如果最后一个参数是true则该四个值是绝对像素位置。
color: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: 'rgba(80, 164, 225, 0.4)'
},
{
offset: 1,
color: 'rgba(80, 164, 225, 0)'
}
],
false
),
shadowColor: 'rgba(25,163,223, 0.5)', //阴影颜色
shadowBlur: 20 //shadowBlur设图形阴影的模糊大小。配合shadowColor,shadowOffsetX/Y, 设置图形的阴影效果。
},
data: lineData.line1
}
]
};
return option;
};
//#endregion
// 菜品销售图
export const getDishesOption = (data?: any) => {
const res = data;
const dataIndex = 1;
const option = {
xAxis: {
type: 'value',
axisTick: {
show: false
},
splitLine: {
show: false
},
axisLabel: {
show: false
}
},
yAxis: {
type: 'category',
axisTick: {
show: false
},
axisLabel: {
margin: 10 // 增大标签与轴线间距
},
width: 60, // 增大Y轴宽度
data: res.name,
axisLine: {
lineStyle: {
color: '#93C9C3'
}
}
},
grid: {
top: '5%', // 设置网格区域与容器之间的边距
bottom: '5%', // 同理
left: '5%',
containLabel: true
},
series: [
{
type: 'bar',
data: res.ratio,
barMaxWidth: 25,
itemStyle: {
barBorderRadius: 3,
color: 'rgba(12, 242, 216, 0.2)'
},
label: {
show: false
}
},
{
type: 'bar',
data: res.data,
barGap: '-100%',
barMaxWidth: 25,
itemStyle: {
barBorderRadius: 3,
color: function (params: any) {
if (params.data <= 300) {
return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ color: 'rgba(252, 105, 0, 1)', offset: 0 },
{ color: 'rgba(250, 42, 42, 1)', offset: 1 }
]);
} else {
return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ color: 'rgba(73, 169, 191, 1)', offset: 0 },
{ color: 'rgba(108, 248, 236, 1)', offset: 1 }
]);
}
}
},
label: {
show: true,
position: [200, -15],
formatter: function (params: any) {
if (params.data <= 300) {
return `{a| ${params.value}g/${res.ratio[params.dataIndex]}g}`;
} else {
return `{b| ${params.value}g/${res.ratio[params.dataIndex]}g}`;
}
},
rich: {
a: {
color: 'rgba(255, 78, 51, 1)'
},
b: {
color: 'rgba(255, 235, 59, 1)'
}
}
}
}
]
};
return option;
};
// 菜品库存图
export const getInventoryOption = () => {
const res = {
data: [2800, 300, 3900, 3000, 2450, 2670, 3320],
name: ['麻辣牛肉', '水煮肉片', '酸菜鱼', '辣子鸡丁', '烧白', '冬瓜排骨汤', '清炒油麦菜'],
ratio: [4000, 4000, 4000, 4000, 4000, 4000, 4000]
},
dataIndex = 1;
const option = {
xAxis: {
type: 'value',
axisTick: {
show: false
},
splitLine: {
show: false
},
axisLabel: {
show: false
}
},
yAxis: {
type: 'category',
show: false,
axisTick: {
show: false
},
axisLabel: {
margin: 10 // 增大标签与轴线间距
},
width: 20, // 增大Y轴宽度
data: res.name,
axisLine: {
show: false,
lineStyle: {
color: '#93C9C3'
}
}
},
grid: {
top: '5%', // 设置网格区域与容器之间的边距
bottom: '5%', // 同理
left: '5%',
containLabel: true
},
series: [
{
type: 'bar',
data: res.ratio,
barMaxWidth: 6,
itemStyle: {
barBorderRadius: 3,
color: 'rgba(12, 242, 216, 0.2)'
},
label: {
show: true,
position: [0, -15],
fontSize: 14,
color: '#fff',
formatter: function (params: any) {
return params.name;
}
// rich: {
// a: {
// color: "rgba(255, 78, 51, 1)",
// },
// b: {
// color: "rgba(255, 235, 59, 1)",
// },
// },
}
},
{
type: 'bar',
data: res.data,
barGap: '-100%',
barMaxWidth: 6,
itemStyle: {
barBorderRadius: 0,
color: function (params: any) {
if (params.dataIndex === dataIndex) {
return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ color: 'rgba(255, 78, 51, 1)', offset: 0 },
{ color: 'rgba(252, 105, 0, 0)', offset: 1 }
]);
} else {
return new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ color: 'rgba(242, 224, 27, 1)', offset: 0 },
{ color: 'rgba(236, 227, 127, 0.55)', offset: 0.5 },
{ color: 'rgba(230, 229, 227, 0.1)', offset: 1 }
]);
}
}
},
label: {
show: true,
position: [200, -15],
formatter: function (params: any) {
if (params.dataIndex === dataIndex) {
return `{a| ${params.value}g}`;
} else {
return `{b| ${params.value}g}`;
}
},
rich: {
a: {
color: 'rgba(255, 78, 51, 1)'
},
b: {
color: 'rgba(255, 235, 59, 1)'
}
}
}
}
]
};
return option;
};
export const getBarOptions = (data: any) => {
const option = {
backgroundColor: '',
grid: {
left: '7%',
top: '10%',
bottom: '23%',
right: '2%'
},
tooltip: {
show: true,
backgroundColor: '',
trigger: 'axis',
formatter: '{b0}{c0}万元',
textStyle: {
color: '#fff'
}
// borderColor: 'rgba(252, 217, 18, 1)'
},
xAxis: [
{
type: 'category',
data: data.name,
axisLine: {
lineStyle: {
color: 'rgba(108, 128, 151, 0.3)'
}
},
axisLabel: {
textStyle: {
color: '#999',
fontSize: 12
}
},
axisTick: {
// show: true,
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(108, 128, 151, 0.3)',
type: 'dashed'
}
}
}
],
yAxis: [
{
axisLabel: {
formatter: function (value) {
if (value >= 1000) {
value = (value / 1000).toFixed(1) + 'k'; // 大于等于1000的数字显示为1k、2.5k等
}
return value;
},
color: 'rgba(255, 255, 255, 0.8)'
},
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: 'rgba(108, 128, 151, 0.3)'
}
},
splitLine: {
show: true,
lineStyle: {
color: 'rgba(108, 128, 151, 0.3)',
type: 'dashed'
}
}
}
],
dataZoom: [
{
// show: true,
start: 0,
end: 30,
bottom: 2, // 下滑块距离x轴底部的距离
height: 23
},
{
type: 'inside'
}
],
series: [
{
type: 'bar',
data: data.value,
stack: '合并',
barWidth: '15',
itemStyle: {
color: new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{
offset: 0,
color: 'rgba(0, 111, 255, 0)' // 0% 处的颜色
},
{
offset: 0.7,
color: 'rgba(0, 111, 255, 0.5)' // 0% 处的颜色
},
{
offset: 1,
color: 'rgba(0, 111, 255, 1)' // 100% 处的颜色
}
],
false
)
},
label: {
show: true,
formatter: '{c}',
position: 'top',
color: '#fff',
fontSize: 10
// padding: 5
}
}
// {
// type: 'bar',
// stack: '合并',
// data: topData,
// barWidth: '15',
// itemStyle: {
// color: 'rgba(252, 217, 18, 1)'
// }
// }
]
};
return option;
};

View File

@ -0,0 +1,442 @@
<template>
<div class="rightPage">
<div class="alarm-container">
<!-- 顶部标题栏 -->
<div class="header">
<img src="@/assets/large/right1.png" style="width: 17px; height: 18px" alt="" />
<span class="title">告警信息中心</span>
<!-- <el-badge :value="unhandledCount" class="unhandled-badge" type="danger"> {{ unhandledCount }}条未处理 </el-badge> -->
<span class="jgao">{{ alarmData.length }}条信息未处理</span>
</div>
<!-- 告警卡片列表可循环渲染这里演示单条 -->
<div class="alarm_list">
<el-card class="alarm-card" shadow="hover" v-for="(item, index) in alarmData" :key="index">
<div class="card-header">
<img src="@/assets/large/right2.png" style="width: 15px; height: 15px" alt="" />
<span class="card-title">{{ item.alarmMsg }}</span>
<span class="time">{{ formatDate(item.alarmBeginTime) }}</span>
</div>
<div class="card-content">
{{ item.advice }}
</div>
<div class="card-footer">
<el-tag type="danger" size="small">紧急</el-tag>
<el-tag type="danger" size="small">处理</el-tag>
</div>
</el-card>
</div>
</div>
<div class="overview">
<div class="left_title">
<div style="display: flex; align-items: center">
<div class="left_title_img">
<img src="@/assets/large/right4.png" alt="" />
</div>
<div class="left_title_text">项目概述</div>
</div>
</div>
<div class="overview_content">
<div>项目名称田东光伏智慧生态工地开发项目</div>
<div>项目位置广西壮族自治区百色市田东县平马镇东宁东路97号百通</div>
<div>项目位置广西壮族自治区百色市田东县平马镇东宁东路97号百通</div>
<div>占地面积约10000亩</div>
<div>土地性质城镇住宅用地兼容商业用地容积率2.5</div>
<div>建设单位这里是建设单位的名称</div>
<div>项目类型集中式光伏电站</div>
<div>总装机容量200MW</div>
</div>
</div>
<div class="monitor">
<div class="left_title">
<div style="display: flex; align-items: center">
<div class="left_title_img">
<img src="@/assets/large/right3.png" alt="" />
</div>
<div class="left_title_text">设备状态监控</div>
</div>
</div>
<div class="stats-container">
<div class="container_item" v-for="(item, index) in deviceStats" :key="index">
<div class="container_item_one">
<div class="container_item_one_box">
<div class="box_img">
<img src="@/assets/large/right6.png" style="width: 20px; height: 20px" />
</div>
<div class="box_text">
<div>{{ item.name }}</div>
<div style="font-size: 12px">{{ item.total }}</div>
</div>
</div>
<div class="card-right">
<div class="progress-top">
<span
class="progress-percent"
:class="{
green1: item.rate >= 99, // 可根据需求调整颜色规则
orange1: item.rate < 99 && item.rate >= 90
}"
>{{ item.rate }}%</span
>
</div>
<div class="progress-bg">
<div
class="progress-fg"
:style="{ width: item.rate + '%' }"
:class="{
green: item.rate >= 99, // 可根据需求调整颜色规则
orange: item.rate < 99 && item.rate >= 90
}"
></div>
</div>
</div>
</div>
<div class="container_item_two">
<div>正常{{ item.normal }}</div>
<div>异常{{ item.abnormal }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { getAlarmListOverview } from '@/api/large';
import { formatDate } from '@/utils/index';
const alarmData: any = ref({});
const deviceStats = ref([
{
name: '光伏组件',
icon: '../../../assets/large/right5.png', // 示例图标
total: '25,680',
unit: '块',
rate: 99.2,
normal: '25,472',
abnormal: 208
},
{
name: '逆变器',
icon: '@/assets/large/right6.png',
total: '1,246',
unit: '台',
rate: 98.6,
normal: '1,230',
abnormal: 16
},
{
name: '汇流箱',
icon: '@/assets/large/right7.png',
total: '128',
unit: '台',
rate: 100,
normal: '128',
abnormal: 0
},
{
name: '变压器',
icon: '@/assets/large/right8.png',
total: '32',
unit: '台',
rate: 96.8,
normal: '31',
abnormal: 1
},
{
name: '通信设备',
icon: '@/assets/large/right9.png',
total: '246',
unit: '台',
rate: 95.2,
normal: '234',
abnormal: 12
}
]);
const getAlarm = () => {
getAlarmListOverview().then((res) => {
console.log(res);
alarmData.value = res.data;
});
};
getAlarm();
</script>
<style scoped lang="scss">
.rightPage {
width: 100%;
height: 100%;
}
.alarm-container {
border: 1px solid #1e2b3d; /* 深色背景模拟,可替换成项目背景 */
border-radius: 8px;
color: #fff;
// box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 10px;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
}
.title {
font-size: 16px;
font-weight: 500;
color: #fff;
margin-left: 8px;
}
.unhandled-badge {
margin-left: auto; /* 右对齐 */
}
.jgao {
font-size: 12px;
color: #f56c6c;
background: rgba(255, 77, 79, 0.2);
padding: 5px 6px;
border-radius: 10px;
margin-left: auto; /* 右对齐 */
}
.alarm_list {
width: 100%;
padding: 5px 0;
height: 14vh;
overflow-y: auto; /* 垂直方向超出时显示滚动条 */
}
// 滚动条优化
.alarm_list::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.alarm_list::-webkit-scrollbar-thumb {
background-color: #0ff !important;
border-radius: 5px;
}
.alarm_list::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.2);
}
/* 告警卡片 */
.alarm-card {
background: rgba(12, 30, 53, 0.3);
color: #fff;
border: none;
border-radius: 8px;
border: 1px solid #f56c6c;
margin-top: 10px;
}
.card-header {
display: flex;
align-items: center;
// justify-content: space-between;
margin-bottom: 12px;
}
.card-title {
font-size: 16px;
font-weight: bold;
color: #f56c6c;
margin-left: 10px;
}
.time {
font-size: 12px;
color: #909399;
margin-left: auto; /* 右对齐 */
}
.card-content {
font-size: 13px;
color: #dcdfe6;
margin-bottom: 12px;
line-height: 1.6;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.left_title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
.left_title_img {
height: 20px;
width: 20px;
}
.left_title_text {
font-size: 20px;
font-family: 'Rang_men_zheng_title', sans-serif;
display: flex;
align-items: flex-end;
margin-left: 15px;
padding-top: 2px;
box-sizing: border-box;
}
.left_title_text1 {
font-size: 14px;
display: flex;
align-items: flex-end;
margin-left: 15px;
padding-top: 2px;
box-sizing: border-box;
color: #fff;
}
}
img {
width: 100%;
height: 100%;
}
.overview {
width: 100%;
height: 28vh;
padding: 10px;
border-radius: 10px;
border: 1px solid #1e2b3d;
margin-top: 20px;
.overview_content {
height: 80%;
width: 100%;
font-size: 14px;
line-height: 30px;
overflow-y: auto; /* 垂直方向超出时显示滚动条 */
}
// 滚动条优化
.overview_content::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.overview_content::-webkit-scrollbar-thumb {
background-color: #0ff !important;
border-radius: 5px;
}
.overview_content::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.2);
}
}
.monitor {
width: 100%;
height: 39vh;
border: 1px solid #1e2b3d;
margin-top: 20px;
padding: 10px;
border-radius: 10px;
.stats-container {
width: 100%; /* 可根据实际场景调整宽度 */
height: 87%;
padding: 10px;
border-radius: 8px;
box-sizing: border-box;
overflow-y: auto; /* 垂直方向超出时显示滚动条 */
.container_item {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
.container_item_one {
width: 100%;
display: flex;
justify-content: space-between;
.container_item_one_box {
width: 50%;
display: flex;
.box_img {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(12, 30, 53, 0.6);
display: flex;
justify-content: center;
align-items: center;
}
.box_text {
color: rgba(156, 163, 175, 1);
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 10px;
// align-items: center;
}
}
/* 右侧区域:进度条 + 数据 */
.card-right {
display: flex;
margin-left: 10px;
justify-content: space-between;
align-items: center;
}
.progress-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 6px;
font-size: 14px;
}
.progress-percent {
font-weight: bold;
}
.abnormal {
color: #ff9900; /* 异常数据颜色 */
}
.progress-bg {
height: 6px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
width: 100px;
text-align: right;
}
.progress-fg {
height: 100%;
width: 100px;
transition: width 0.3s;
}
/* 进度条颜色区分(可扩展更多规则) */
.green {
background-color: #28a745;
}
.orange {
background-color: #ffc107;
}
.green1 {
color: #28a745;
}
.orange1 {
color: #ffc107;
}
}
}
.container_item_two {
width: 90%;
height: 100%;
display: flex;
justify-content: space-between;
padding: 10px 0;
margin-left: auto;
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
}
// 滚动条优化
.stats-container::-webkit-scrollbar {
width: 5px;
height: 5px;
}
.stats-container::-webkit-scrollbar-thumb {
background-color: #0ff;
border-radius: 5px;
}
.stats-container::-webkit-scrollbar-track {
background-color: rgba(0, 255, 255, 0.2);
}
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="large-screen">
<Header />
<div class="nav">
<div class="nav_left">
<leftPage />
</div>
<div class="nav_center">
<centerPage />
</div>
<div class="nav_right">
<rightPage />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Header from './components/header.vue';
import leftPage from './components/leftPage.vue';
import centerPage from './components/centerPage.vue';
import rightPage from './components/rightPage.vue';
import '@/assets/styles/element.scss';
</script>
<style scoped lang="scss">
.large-screen {
width: 100vw;
height: 100vh;
background: url('@/assets/large/bg.png') no-repeat;
background-size: 100% 100%;
background-color: rgba(4, 7, 17, 1);
}
.nav {
width: 100%;
height: calc(100vh - 80px);
box-sizing: border-box;
// padding: 10px;
display: grid;
grid-template-columns: 1fr 2fr 1fr;
color: #fff;
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<div class="dashboard-container">
<!-- 第一个图表本月出入库统计 -->
<div class="chart-item">
<div class="title">
本月出入库统计
</div>
<div ref="lineChartRef" class="chart-container"></div>
</div>
<!-- 第二个图表出入库类型分布 -->
<div class="chart-item">
<div class="title">
出入库类型分布
</div>
<div ref="barChartRef" class="chart-container"></div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
// 图表容器引用
const lineChartRef = ref(null);
const barChartRef = ref(null);
// 图表实例
let lineChart = null;
let barChart = null;
onMounted(() => {
// 初始化折线图
initLineChart();
// 初始化柱状图
initBarChart();
// 监听窗口大小变化,自适应图表
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
// 销毁图表实例
if (lineChart) {
lineChart.dispose();
}
if (barChart) {
barChart.dispose();
}
// 移除事件监听
window.removeEventListener('resize', handleResize);
});
// 初始化折线图
const initLineChart = () => {
if (lineChartRef.value) {
lineChart = echarts.init(lineChartRef.value);
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['入库数量', '出库数量'],
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
},
yAxis: {
type: 'value'
},
series: [
{
name: '入库数量',
type: 'line',
data: [5, 40, 20, 75, 60, 80, 40, 55, 30, 65, 5, 80],
symbol: 'none',
smooth: true,
lineStyle: {
color: 'rgba(22, 93, 255, 1)'
},
itemStyle: {
color: 'rgba(22, 93, 255, 1)'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(22, 93, 255, 0.2)' },
{ offset: 1, color: 'rgba(22, 93, 255, 0)' }
])
}
},
{
name: '出库数量',
type: 'line',
data: [30, 40, 30, 30, 30, 15, 55, 50, 40, 60, 25, 90],
symbol: 'none',
smooth: true,
lineStyle: {
color: 'rgba(255, 153, 0, 1)'
},
itemStyle: {
color: 'rgba(255, 153, 0, 1)'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(59, 179, 70, 0.2)' },
{ offset: 1, color: 'rgba(59, 179, 70, 0)' }
])
}
}
]
};
lineChart.setOption(option);
}
};
// 初始化柱状图
const initBarChart = () => {
if (barChartRef.value) {
barChart = echarts.init(barChartRef.value);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['入库数量', '出库数量'],
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
top: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['电器部件', '机械部件', '电子元件', '控制模块', '结构部件', '其他'],
axisLabel: {
interval: 0, // 强制显示所有标签
rotate: 30, // 标签旋转30度
margin: 15, // 增加与轴线的距离
align: 'right', // 文字右对齐
verticalAlign: 'top' // 垂直方向顶部对齐
}
},
yAxis: {
type: 'value'
},
series: [
{
name: '入库数量',
type: 'bar',
data: [650, 480, 510, 280, 650, 220],
itemStyle: {
color: 'rgba(22, 93, 255, 1)' // 入库数量颜色
},
barWidth: '45%',
barGap: '0' // 柱子之间的间距
},
{
name: '出库数量',
type: 'bar',
data: [850, 400, 770, 590, 540, 310],
itemStyle: {
color: 'rgba(15, 198, 194, 1)' // 出库数量颜色
},
barWidth: '45%',
barGap: '0' // 柱子之间的间距
}
]
};
barChart.setOption(option);
}
};
// 处理窗口大小变化,让图表自适应
const handleResize = () => {
if (lineChart) {
lineChart.resize();
}
if (barChart) {
barChart.resize();
}
};
</script>
<style scoped>
.dashboard-container {
padding: 20px;
}
.chart-item {
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.title {
color: rgba(29, 33, 41, 1);
font-weight: bold;
font-size: 16px;
margin-bottom: 16px;
}
.chart-container {
width: 100%;
height: 300px;
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="approval-process-container">
<div class="approval-process-card">
<h2>审批流程</h2>
<!-- 时间线组件展示审批步骤 -->
<el-timeline class="custom-timeline">
<!-- 步骤1创建采购单 -->
<el-timeline-item timestamp="11月1日 10:18" placement="top" :icon="renderCustomIcon('/assets/yes.png')"
color="green" type="danger" class="timeline-item">
<h3 style="color: rgba(24, 109, 245, 1);font-weight: bold;">创建采购单</h3>
<p>申请人张三提交采购单</p>
<p>计划Q2风电轴承采购计划</p>
<p>附件<a href="#" class="attachment-link">一个图片.jpg</a></p>
</el-timeline-item>
<!-- 步骤2审批未通过 -->
<el-timeline-item timestamp="11月1日 10:18" placement="top" :icon="renderCustomIcon('/assets/no.png')"
color="red" class="timeline-item">
<h3 style="color: rgba(24, 109, 245, 1);font-weight: bold;">审批未通过</h3>
<p>部门经理李四审批不通过</p>
<p>计划Q2风电轴承采购计划</p>
<p>不通过原因</p>
<ul class="reason-list">
<li>1. 出货时间过长</li>
<li>2. 单价高于市场价</li>
<li>3. 损耗重新评估</li>
<li>4. 付款方式更改</li>
<li>5. 发票开具方式更改</li>
</ul>
</el-timeline-item>
<!-- 步骤3未进行财务主管 -->
<el-timeline-item timestamp="" placement="top" :icon="renderCustomIcon('/assets/re.png')" color="gray"
class="timeline-item">
<h3>未进行</h3>
<p>财务主管王五</p>
<p>计划Q2风电轴承采购计划</p>
<p>备注</p>
</el-timeline-item>
<!-- 步骤4未进行总经理 -->
<el-timeline-item timestamp="" placement="top" :icon="renderCustomIcon('/assets/re.png')" color="gray"
class="timeline-item">
<h3>未进行</h3>
<p>总经理赵六</p>
<p>计划Q2风电轴承采购计划</p>
<p>备注</p>
</el-timeline-item>
</el-timeline>
</div>
</div>
</template>
<script setup>
import { Check, Close, Clock } from '@element-plus/icons-vue'; // 引入Element Plus图标
const renderCustomIcon = (iconSrc) => {
return h('img', { src: iconSrc, class: 'custom-icon' });
};
</script>
<style scoped>
.approval-process-container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
/* padding: 40px 20px; */
}
.approval-process-card {
padding: 30px;
width: 100%;
max-width: 800px;
transition: transform 0.3s ease;
}
.approval-process-card:hover {
transform: translateY(-5px);
}
h2 {
margin-bottom: 30px;
font-size: 24px;
color: #333;
text-align: center;
position: relative;
}
h2::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background-color: #409eff;
}
.custom-timeline {
margin-top: 20px;
}
.timeline-item {
margin-bottom: 25px;
transition: all 0.3s ease;
}
.timeline-item:hover {
transform: translateX(5px);
}
.el-timeline-item__content {
padding-top: 15px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 8px;
padding: 15px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
ul.reason-list {
margin: 0;
padding-left: 20px;
color: red;
list-style: none;
}
li {
margin: 8px 0;
line-height: 1.6;
}
.attachment-link {
color: #409eff;
text-decoration: none;
transition: all 0.3s ease;
display: inline-block;
}
.attachment-link:hover {
text-decoration: underline;
transform: translateY(-2px);
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<div class="bill-list">
<!-- 循环渲染单据列表 -->
<div v-for="(bill, index) in billList" :key="index" class="bill-item">
<!-- 左侧图标 + 单据类型 + 编号 -->
<div class="left">
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="40"
height="40" viewBox="0 0 40 40" fill="none">
<circle cx="20" cy="20" r="20" fill="#186DF5"></circle>
<path fill="rgba(255, 255, 255, 1)"
d="M15.1666 18.3333L15.1666 22.5L14.1666 22.5L14.1666 21.5L25.8333 21.5L25.8333 22.5L24.8333 22.5L24.8333 18.3333L25.8333 18.3333L24.8333 18.3333C24.8333 15.664 22.6693 13.5 20 13.5L20 13.5C17.3306 13.5 15.1666 15.664 15.1666 18.3333L15.1666 18.3333ZM13.1666 18.3333L14.1666 18.3333L13.1666 18.3333C13.1666 14.5594 16.226 11.5 20 11.5L20 12.5L20 11.5C23.7739 11.5 26.8333 14.5594 26.8333 18.3333L26.8333 18.3333L26.8333 22.5L25.8333 23.5L14.1666 23.5L13.1666 22.5L13.1666 18.3333ZM13.1666 22.5L14.1666 22.5L14.1666 23.5C13.8921 23.4961 13.6564 23.3985 13.4595 23.2071C13.2681 23.0102 13.1705 22.7745 13.1666 22.5ZM25.8333 23.5L25.8333 22.5L26.8333 22.5C26.8294 22.7745 26.7318 23.0102 26.5404 23.2071C26.3435 23.3985 26.1078 23.4961 25.8333 23.5Z">
</path>
<path fill="rgba(255, 255, 255, 1)"
d="M13.5 23.75C13.5 23.8881 13.6119 24 13.75 24L13.75 25L13.75 24L26.25 24L26.25 25L26.25 24C26.3881 24 26.5 23.8881 26.5 23.75L27.5 23.75L26.5 23.75C26.5 23.6119 26.3881 23.5 26.25 23.5L26.25 23.5L13.75 23.5L13.75 22.5L13.75 23.5C13.6119 23.5 13.5 23.6119 13.5 23.75L13.5 23.75ZM11.5 23.75L12.5 23.75L11.5 23.75C11.5 22.5074 12.5074 21.5 13.75 21.5L13.75 21.5L26.25 21.5L26.25 22.5L26.25 21.5C27.4926 21.5 28.5 22.5074 28.5 23.75L28.5 23.75C28.5 24.9926 27.4926 26 26.25 26L26.25 26L13.75 26L13.75 26C12.5074 26 11.5 24.9926 11.5 23.75Z">
</path>
<path stroke="rgba(255, 255, 255, 1)" stroke-width="2" stroke-linejoin="round"
stroke-linecap="round"
d="M22.5 25C22.5 26.3807 21.3807 27.5 20 27.5C18.6193 27.5 17.5 26.3807 17.5 25"></path>
</svg>
</div>
<div class="info">
<div class="type">{{ bill.type }}</div>
<div class="number">{{ bill.number }}</div>
</div>
</div>
<!-- 右侧时间 + 状态 -->
<div class="right">
<div class="time">{{ bill.time }}</div>
<div class="status" :class="{
'approved': bill.status === '审批通过',
'pending': bill.status === '待审批'
}">
{{ bill.status }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 定义单据数据
const billList = ref([
{
type: '出库单',
number: 'IN-2023-0615-001',
time: '10月20日 12:00:06',
status: '审批通过'
},
{
type: '入库单',
number: 'IN-2023-0615-001',
time: '10月20日 12:00:06',
status: '待审批'
},
{
type: '入库单',
number: 'IN-2023-0615-001',
time: '10月20日 12:00:06',
status: '待审批'
},
{
type: '入库单',
number: 'IN-2023-0615-001',
time: '10月20日 12:00:06',
status: '待审批'
},
{
type: '入库单',
number: 'IN-2023-0615-001',
time: '10月20日 12:00:06',
status: '待审批'
}
]);
</script>
<style scoped>
.bill-list {
display: flex;
flex-direction: column;
margin-top: 10px;
/* gap: 1rem; */
/* padding: 1rem; */
}
.bill-item {
display: flex;
justify-content: space-between;
align-items: center;
/* padding: 0.5rem; */
padding: 10px;
border-bottom: 1px solid #e5e7eb;
}
.left {
display: flex;
align-items: center;
gap: 0.5rem;
}
.info {
display: flex;
flex-direction: column;
}
.type {
font-weight: 600;
}
.number {
font-size: 0.875rem;
color: #6b7280;
}
.right {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.time {
font-size: 0.875rem;
color: #6b7280;
}
.status {
font-size: 0.875rem;
/* padding: 0.25rem 0.5rem; */
border-radius: 0.25rem;
}
.approved {
color: #10b981;
/* background-color: rgba(16, 185, 129, 0.1); */
}
.pending {
color: #f59e0b;
/* background-color: rgba(245, 158, 11, 0.1); */
}
</style>

View File

@ -0,0 +1,264 @@
<template>
<div class="approval-form">
<!-- 基础信息 -->
<el-card class="card" shadow="hover">
<template #header>
<h3>基础信息</h3>
</template>
<el-form :model="detailInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="采购单号">
<el-input v-model="detailInfo.id" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="创建时间">
<el-input v-model="detailInfo.createTime" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="经办人">
<el-input v-model="detailInfo.jingbanrenName" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属部门">
<el-select v-model="detailInfo.caigouDanweiName" placeholder="请选择">
<el-option label="运维部" value="运维部" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购类型">
<el-select v-model="detailInfo.caigouType" placeholder="请选择">
<el-option label="项目业务" value="项目业务" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="申请原因">
<el-input v-model="basicInfo.applyReason" type="textarea" :rows="2" placeholder="请输入申请原因" />
</el-form-item>
</el-form>
</el-card>
<!-- 供应商信息 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>供应商信息</h3>
</template>
<el-form :model="detailInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="供应商单位">
<el-select v-model="detailInfo.gonyingshangId" placeholder="请选择">
<el-option label="AAAA精密仪器制造有限公司" value="AAAA精密仪器制造有限公司" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出货时间">
<el-select v-model="detailInfo.chouhuoTime" placeholder="请选择">
<el-option label="2年零4个月" value="2年零4个月" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 产品信息 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>产品信息</h3>
</template>
<el-table :data="detailInfo.opsCaigouPlanChanpinVos" border style="width: 100%">
<el-table-column prop="chanpinName" label="产品名称" />
<el-table-column prop="chanpinType" label="产品型号" />
<el-table-column prop="chanpinMonovalent" label="产品单价" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="goumaiNumber" label="购买数量" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="yontu" label="用途" />
<el-table-column prop="totalPrice" label="合计" />
</el-table>
</el-card>
<!-- 合同条款 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>合同条款</h3>
</template>
<el-form :model="contractInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="付款条件">
<el-select v-model="detailInfo.fukuantiaojian" placeholder="请选择">
<el-option label="银行卡" value="银行卡" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发票开具方式">
<el-select v-model="detailInfo.fapiaoKjfs" placeholder="请选择">
<el-option label="请选择" value="请选择" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 附件 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>附件</h3>
</template>
<el-upload class="upload-demo" action="#" :file-list="fileList" :auto-upload="false"
:on-preview="handlePreview">
<el-table :data="fileList" border style="width: 100%">
<el-table-column prop="name" label="文件名" width="300" />
<el-table-column prop="size" label="大小" width="100" />
<el-table-column label="操作" width="100">
<template #default="scope">
<!-- <el-link type="primary" @click="handlePreview(scope.row)"> -->
<el-link type="primary">
预览
</el-link>
</template>
</el-table-column>
</el-table>
</el-upload>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance, toRefs } from 'vue';
import { useRoute } from 'vue-router';
import type { ComponentInternalInstance } from 'vue';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wz_invoicing_way, wz_payment_terms, wz_purchase_type, wz_contract_type, wz_caigou_examine } = toRefs<any>(proxy?.useDict('wz_invoicing_way', 'wz_payment_terms', 'wz_purchase_type', 'wz_contract_type', 'wz_caigou_examine'));
import { caigouPlanDetail } from '@/api/wuziguanli/caigouPlan';
import { CaigouPlanVO, CaigouPlanQuery, CaigouPlanForm } from '@/api/wuziguanli/caigouPlan/types';
// 存储计划详情数据
const detailInfo = ref<CaigouPlanVO>({} as CaigouPlanVO);
// 存储计划编号
const id = ref('');
const getDetailInfo = async () => {
const res = await caigouPlanDetail(id.value);
if (res.code === 200) {
detailInfo.value = res.data;
console.log(detailInfo.value);
}
}
onMounted(() => {
// 接收路由参数
id.value = route.query.id as string;
getDetailInfo();
});
// 基础信息数据
const basicInfo = ref({
orderNo: '0035455',
createTime: '2023-11-02 16:32',
handler: '李四',
dept: '运维部',
purchaseType: '项目业务',
applyReason:
'随着业务拓展光伏电站业务负责增加现有设备已运行5年部分出现效率下降情况。为保证电站正常运行计划采购一批新的逆变器替换老旧设备并补充备件库存。',
});
// 供应商信息数据
const supplierInfo = ref({
supplierName: 'AAAA精密仪器制造有限公司',
deliveryTime: '2年零4个月',
remark: '',
});
// 产品信息数据
const productInfo = ref({
tableData: [
{
productName: 'AAABBBCCC',
productModel: '15-42',
productPrice: 500,
buyQuantity: 10,
usage: '组件',
total: 5000,
},
],
remark: '',
});
// 合同条款数据
const contractInfo = ref({
paymentCondition: '银行卡',
invoiceWay: '请选择',
remark: '',
});
// 附件数据
const fileList = ref([
{
name: 'MWwwwww.jpg',
size: '30kb',
url: '',
},
{
name: '231234124w.zip',
size: '50kb',
url: '',
},
{
name: '12451asdas.doc',
size: '80kb',
url: '',
},
{
name: '21seasda.xls',
size: '29kb',
url: '',
},
{
name: '12kjaklskw.png',
size: '16kb',
url: '',
},
]);
// 预览文件
const handlePreview = (file) => {
console.log('预览文件:', file);
// 实际场景可在这里处理文件预览逻辑,如打开新窗口等
};
</script>
<style scoped>
.approval-form {
padding: 20px;
}
.card {
border-radius: 8px;
}
.error-tip {
color: red;
font-size: 12px;
margin-top: 5px;
}
::v-deep(.el-input__inner) {
color: red;
}
</style>

View File

@ -0,0 +1,261 @@
<template>
<div class="approval-form">
<!-- 基础信息 -->
<el-card class="card" shadow="hover">
<template #header>
<h3>基础信息</h3>
</template>
<el-form :model="basicInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="订单编号">
<el-input v-model="basicInfo.orderNo" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="创建时间">
<el-input v-model="basicInfo.createTime" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="经办人">
<el-input v-model="basicInfo.handler" disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="所属部门">
<el-select v-model="basicInfo.dept" placeholder="请选择">
<el-option label="运维部" value="运维部" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购类型">
<el-select v-model="basicInfo.purchaseType" placeholder="请选择">
<el-option label="项目业务" value="项目业务" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="申请原因">
<el-input v-model="basicInfo.applyReason" type="textarea" :rows="2" placeholder="请输入申请原因" />
</el-form-item>
</el-form>
</el-card>
<!-- 供应商信息 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>供应商信息</h3>
</template>
<el-form :model="supplierInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="供应商单位">
<el-select v-model="supplierInfo.supplierName" placeholder="请选择">
<el-option label="AAAA精密仪器制造有限公司" value="AAAA精密仪器制造有限公司" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出货时间">
<el-select v-model="supplierInfo.deliveryTime" placeholder="请选择">
<el-option label="2年零4个月" value="2年零4个月" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 产品信息 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>产品信息</h3>
</template>
<el-table :data="productInfo.tableData" border style="width: 100%">
<el-table-column prop="productName" label="产品名称" />
<el-table-column prop="productModel" label="产品型号" />
<el-table-column prop="productPrice" label="产品单价" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="buyQuantity" label="购买数量" align="center" :cell-style="{ background: 'pink' }" />
<el-table-column prop="usage" label="用途" />
<el-table-column prop="total" label="合计" />
</el-table>
</el-card>
<!-- 合同条款 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>合同条款</h3>
</template>
<el-form :model="contractInfo" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="付款条件">
<el-select v-model="contractInfo.paymentCondition" placeholder="请选择">
<el-option label="银行卡" value="银行卡" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="发票开具方式">
<el-select v-model="contractInfo.invoiceWay" placeholder="请选择">
<el-option label="请选择" value="请选择" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 附件 -->
<el-card class="card" shadow="hover" style="margin-top: 20px">
<template #header>
<h3>附件</h3>
</template>
<el-upload class="upload-demo" action="#" :file-list="fileList" :auto-upload="false"
:on-preview="handlePreview">
<el-table :data="fileList" border style="width: 100%">
<el-table-column prop="name" label="文件名" width="300" />
<el-table-column prop="size" label="大小" width="100" />
<el-table-column label="操作" width="100">
<template #default="scope">
<!-- <el-link type="primary" @click="handlePreview(scope.row)"> -->
<el-link type="primary">
预览
</el-link>
</template>
</el-table-column>
</el-table>
</el-upload>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance, toRefs } from 'vue';
import { useRoute } from 'vue-router';
import type { ComponentInternalInstance } from 'vue';
const route = useRoute();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wz_invoicing_way, wz_payment_terms, wz_purchase_type, wz_contract_type, wz_caigou_examine } = toRefs<any>(proxy?.useDict('wz_invoicing_way', 'wz_payment_terms', 'wz_purchase_type', 'wz_contract_type', 'wz_caigou_examine'));
import { caigouPlanDetail } from '@/api/wuziguanli/caigouPlan';
import { CaigouPlanVO, CaigouPlanQuery, CaigouPlanForm } from '@/api/wuziguanli/caigouPlan/types';
// 存储计划编号
const id = ref('');
const getDetailInfo = async () => {
const res = await caigouPlanDetail(id.value);
if (res.code === 200) {
console.log(res);
}
}
onMounted(() => {
// 接收路由参数
id.value = route.query.id as string;
getDetailInfo();
});
// 基础信息数据
const basicInfo = ref({
orderNo: '0035455',
createTime: '2023-11-02 16:32',
handler: '李四',
dept: '运维部',
purchaseType: '项目业务',
applyReason:
'随着业务拓展光伏电站业务负责增加现有设备已运行5年部分出现效率下降情况。为保证电站正常运行计划采购一批新的逆变器替换老旧设备并补充备件库存。',
});
// 供应商信息数据
const supplierInfo = ref({
supplierName: 'AAAA精密仪器制造有限公司',
deliveryTime: '2年零4个月',
remark: '',
});
// 产品信息数据
const productInfo = ref({
tableData: [
{
productName: 'AAABBBCCC',
productModel: '15-42',
productPrice: 500,
buyQuantity: 10,
usage: '组件',
total: 5000,
},
],
remark: '',
});
// 合同条款数据
const contractInfo = ref({
paymentCondition: '银行卡',
invoiceWay: '请选择',
remark: '',
});
// 附件数据
const fileList = ref([
{
name: 'MWwwwww.jpg',
size: '30kb',
url: '',
},
{
name: '231234124w.zip',
size: '50kb',
url: '',
},
{
name: '12451asdas.doc',
size: '80kb',
url: '',
},
{
name: '21seasda.xls',
size: '29kb',
url: '',
},
{
name: '12kjaklskw.png',
size: '16kb',
url: '',
},
]);
// 预览文件
const handlePreview = (file) => {
console.log('预览文件:', file);
// 实际场景可在这里处理文件预览逻辑,如打开新窗口等
};
</script>
<style scoped>
.approval-form {
padding: 20px;
}
.card {
border-radius: 8px;
}
.error-tip {
color: red;
font-size: 12px;
margin-top: 5px;
}
::v-deep(.el-input__inner) {
color: red;
}
</style>

View File

@ -0,0 +1,654 @@
<template>
<div class="inventoryManagement">
<!-- <TitleComponent title="出入库单管理" subtitle="管理光伏和风电设备备品备件的出入库记录" /> -->
<el-row :gutter="20">
<el-col :span="16" class="list" style="flex-grow: 1;display: flex;">
<el-card style="border-radius: 10px;height: 100%;display: flex;flex-direction: column;flex: 1;">
<div style="height: 100%;flex: 1;">
<div class="top">
<div class="title">单据列表</div>
<div class="button-actions">
<button :class="{ active: type === 'chuku' }" @click="changeType('chuku')">出库单</button>
<button :class="{ active: type === 'ruku' }" @click="changeType('ruku')">入库单</button>
</div>
</div>
<div class="content" style="height: 100%;flex: 1;">
<!-- 第一排四个输入项 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="单据编号" prop="danjvNumber">
<el-input v-model="queryParams.danjvNumber" placeholder="请输入单据编号"
clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="设备类型" prop="shebeiType">
<el-select v-model="queryParams.shebeiType" placeholder="请选择设备类型"
clearable>
<el-option v-for="dict in wz_device_type" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="审核状态" prop="auditStatus">
<el-select v-model="queryParams.auditStatus" placeholder="请选择审核状态"
clearable>
<el-option v-for="dict in shenheStatus" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker v-model="queryParams.startDate" type="date"
placeholder="请选择开始日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 100%" />
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker v-model="queryParams.endDate" type="date"
placeholder="请选择结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
style="width: 100%" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search"
@click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<div style="margin-top: 10px; display: flex; justify-content: flex-end;">
<el-button type="primary" @click="handleAdd">+{{ type === 'chuku' ? '添加出库单'
: '添加入库单' }}</el-button>
</div>
<el-table v-loading="loading" border :data="churukudanList"
style="width: 100%;margin-top: 15px;">
<el-table-column label="单据编号" align="center" prop="danjvNumber" />
<el-table-column label="设备类型" align="center" prop="shebeiType">
<template #default="scope">
<span>{{ getTagLabel(wz_device_type, scope.row.shebeiType) }}</span>
</template>
</el-table-column>
<el-table-column label="经手人" align="center" prop="jingshourenName" />
<el-table-column label="操作时间" align="center" prop="updateTime" />
<el-table-column label="总数量" align="center" prop="zonNumber" width="80px" />
<el-table-column label="审核状态" align="center" prop="shenheStatus">
<template #default="scope">
<el-tag :type="getTagType(shenheStatus, scope.row.shenheStatus)" as="span">
{{ getTagLabel(shenheStatus, scope.row.shenheStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="单据类型" align="center" prop="danjvType">
<template #default="scope">
<el-tag :type="getTagType(danjvType, scope.row.danjvType)">
{{ getTagLabel(danjvType, scope.row.danjvType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleUpdate(scope.row)"
v-hasPermi="['personnel:churukudan:edit']">修改</el-button>
<el-button link type="primary" @click="handleDetail(scope.row)"
v-hasPermi="['personnel:churukudan:query']">详情</el-button>
<el-button link type="primary" @click="handleDelete(scope.row)"
v-hasPermi="['personnel:churukudan:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="tool">
<div class="pagination-section">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card style="border-radius: 10px;">
<div class="item-box">
<div class="title">系统信息</div>
<div class="content">
<SystemInfo />
</div>
</div>
<div class="item-box">
<div class="title">数据分析</div>
<div class="content">
<DataAnalysis />
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 添加或修改运维-物资-出入库单管理对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="churukudanFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="单据类型" prop="danjvType">
<el-select v-model="form.danjvType" placeholder="请选择单据类型">
<el-option v-for="dict in danjvType" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="单据编号" prop="danjvNumber">
<el-input v-model="form.danjvNumber" placeholder="请输入单据编号" />
</el-form-item>
<el-form-item label="设备类型" prop="shebeiType">
<el-select v-model="form.shebeiType" placeholder="请选择设备类型">
<el-option v-for="dict in wz_device_type" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="经手人id" prop="jingshourenId">
<el-input v-model="form.jingshourenId" placeholder="请输入经手人id" />
</el-form-item>
<el-form-item label="经手人" prop="jingshourenName">
<el-input v-model="form.jingshourenName" placeholder="请输入经手人" />
</el-form-item>
<el-form-item label="联系电话" prop="contactNumber">
<el-input v-model="form.contactNumber" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="总数量" prop="zonNumber">
<el-input v-model="form.zonNumber" placeholder="请输入总数量" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="出入库单详情" v-model="detailVisible" width="500px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="单据类型">{{ getTagLabel(danjvType, detailData.danjvType)
}}</el-descriptions-item>
<el-descriptions-item label="单据编号">{{ detailData.danjvNumber }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ getTagLabel(wz_device_type, detailData.shebeiType)
}}</el-descriptions-item>
<el-descriptions-item label="经手人">{{ detailData.jingshourenName }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailData.contactNumber }}</el-descriptions-item>
<el-descriptions-item label="总数量">{{ detailData.zonNumber }}</el-descriptions-item>
<el-descriptions-item label="审核状态">
<dict-tag :options="shenheStatus" :value="detailData.shenheStatus"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.inventoryManagement {
background-color: #F2F8FC;
padding: 20px;
}
.button-actions button {
background: none;
border: 1px solid #e0e0e0;
padding: 5px 12px;
border-radius: 4px;
margin-left: 8px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.button-actions button.active {
background-color: #186DF5;
color: white;
border-color: #186DF5;
}
.top {
display: flex;
justify-content: space-between;
.title {
font-family: "Alibaba-PuHuiTi-Bold";
color: rgba(0, 30, 59, 1);
font-weight: bold;
}
}
.list .content {
margin-top: 20px;
}
.menu {
background-color: #F2F2F2;
padding: 20px;
}
/* 分页区域样式 */
.pagination-section {
background-color: #fff;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination-info {
font-size: 14px;
color: #606266;
}
.pagination-controls .el-pagination {
margin: 0;
}
.pagination-controls .el-pagination button {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 4px;
}
.pagination-controls .el-pagination .el-pager li {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 4px;
}
.pagination-controls .el-pagination .el-pager li.active {
background-color: #409eff;
color: #fff;
}
.tool {
margin-top: 10px;
display: flex;
justify-content: space-between;
}
.item-box {
.title {
font-family: "Alibaba-PuHuiTi-Bold";
font-size: 18px;
font-weight: 400;
letter-spacing: 0px;
line-height: 24px;
color: rgba(0, 30, 59, 1);
margin-top: 10px;
}
}
/* 详情弹窗样式 */
.detail-container {
padding: 10px 0;
}
.detail-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.detail-label {
font-weight: 500;
color: #606266;
width: 120px;
}
.detail-value {
color: #303133;
flex: 1;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
padding: 12px 0;
}
::v-deep(.el-card__body) {
height: 100%;
}
</style>
<script setup lang="ts">
import SystemInfo from './components/SystemInfo.vue';
import DataAnalysis from './components/DataAnalysis.vue';
import { ref, computed } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { listChurukudan, getChurukudan, delChurukudan, addChurukudan, updateChurukudan, getChuRuKuCountBar } from '@/api/wuziguanli/churuku/index';
import { ChurukudanVO, ChurukudanQuery, ChurukudanForm } from '@/api/wuziguanli/churuku/types';
const { wz_device_type } = toRefs<any>(proxy?.useDict('wz_device_type'));
import { getCurrentMonthDates } from '@/utils/getDate';
const currentMonthDates = getCurrentMonthDates();
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 获取用户store
const userStore = useUserStore();
const churukudanList = ref<ChurukudanVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const total = ref(0);
// 单据类型切换变量 - 默认出库单
const type = ref<string>('chuku');
/** 切换单据类型 */
const changeType = (newType: string) => {
type.value = newType;
// 更新查询参数
queryParams.value.pageNum = 1;
queryParams.value.danjvType = newType === 'chuku' ? '1' : '2';
// 重新加载数据
getList();
}
// 单据类型
const danjvType = ref([
{
value: '1',
label: '出库单',
type: 'primary'
},
{
value: '2',
label: '入库单',
type: 'success'
}
]);
// 审核类型
const shenheStatus = ref([
{
value: 'draft',
label: '草稿',
type: 'primary'
},
{
value: 'waiting',
label: '待审核',
type: 'warning',
},
{
value: 'finish',
label: '已完成',
type: 'success'
}
])
// 根据字典数组和值获取标签类型
const getTagType = (dictArray: any[], value: any): string => {
if (!dictArray || !value) return '';
const item = dictArray.find(item => item.value === value);
return item?.type || '';
}
// 根据字典数组和值获取标签文本
const getTagLabel = (dictArray: any[], value: any): string => {
if (!dictArray || !value) return '';
const item = dictArray.find(item => item.value === value);
return item?.label || value;
}
const queryFormRef = ref<ElFormInstance>();
const churukudanFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
// 详情弹窗显示状态
const detailVisible = ref(false);
// 详情数据
const detailData = ref<ChurukudanVO>({} as ChurukudanVO);
const initFormData: ChurukudanForm = {
id: undefined,
projectId: undefined,
danjvNumber: undefined,
shebeiType: undefined,
jingshourenId: undefined,
jingshourenName: undefined,
contactNumber: undefined,
zonNumber: undefined,
shenheStatus: undefined,
danjvType: undefined,
updateTime: undefined,
auditStatus: undefined,
}
const data = reactive<PageData<ChurukudanForm, ChurukudanQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
danjvNumber: undefined,
shebeiType: undefined,
shenheStatus: undefined,
startDate: undefined,
endDate: undefined,
auditStatus: undefined,
danjvType: '1', // 默认显示出库单
params: {
}
},
rules: {
shebeiType: [
{ required: true, message: "设备类型不能为空", trigger: "change" }
],
jingshourenId: [
{ required: true, message: "经手人id不能为空", trigger: "blur" }
],
jingshourenName: [
{ required: true, message: "经手人不能为空", trigger: "blur" }
],
zonNumber: [
{ required: true, message: "总数量不能为空", trigger: "blur" }
],
danjvType: [
{ required: true, message: "单据状态不能为空", trigger: "change" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运维-物资-出入库单管理列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listChurukudan(queryParams.value);
churukudanList.value = res.rows || [];
total.value = res.total || 0;
} catch (error) {
console.error('获取出入库单列表失败:', error);
proxy?.$modal.msgError("获取数据失败,请稍后重试");
churukudanList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
churukudanFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
// 检查日期范围筛选条件
if ((queryParams.value.startDate && !queryParams.value.endDate) ||
(!queryParams.value.startDate && queryParams.value.endDate)) {
proxy?.$modal.msgWarning("时间范围筛选必须同时选择开始日期和结束日期");
return;
}
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
if (userStore.selectedProject && userStore.selectedProject.id) {
form.value.projectId = userStore.selectedProject.id;
}
// 根据当前选择的类型自动设置单据类型
form.value.danjvType = type.value === 'chuku' ? '1' : '2';
dialog.visible = true;
dialog.title = type.value === 'chuku' ? "添加出库单" : "添加入库单";
}
/** 修改按钮操作 */
const handleUpdate = async (row?: ChurukudanVO) => {
reset();
const _id = row?.id || ids.value[0];
if (!_id) {
proxy?.$modal.msgWarning("请选择要修改的数据");
return;
}
try {
const res = await getChurukudan(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改运维-物资-出入库单管理";
} catch (error) {
console.error('获取出入库单详情失败:', error);
proxy?.$modal.msgError("获取数据失败,请稍后重试");
}
}
/** 提交按钮 */
const submitForm = () => {
churukudanFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateChurukudan(form.value);
proxy?.$modal.msgSuccess("修改成功");
} else {
await addChurukudan(form.value);
proxy?.$modal.msgSuccess("添加成功");
}
dialog.visible = false;
await getList();
} catch (error) {
console.error('保存出入库单失败:', error);
proxy?.$modal.msgError("操作失败,请稍后重试");
} finally {
buttonLoading.value = false;
}
}
});
}
/** 详情按钮操作 */
const handleDetail = async (row?: ChurukudanVO) => {
if (!row?.id) {
proxy?.$modal.msgWarning("请选择要查看详情的数据");
return;
}
try {
const res = await getChurukudan(row.id);
detailData.value = res.data || {} as ChurukudanVO;
detailVisible.value = true;
} catch (error) {
console.error('获取出入库单详情失败:', error);
proxy?.$modal.msgError("获取详情失败,请稍后重试");
}
}
/** 删除按钮操作 */
const handleDelete = async (row?: ChurukudanVO) => {
const _ids = row?.id || ids.value;
if (!_ids || (_ids instanceof Array && _ids.length === 0)) {
proxy?.$modal.msgWarning("请选择要删除的数据");
return;
}
try {
const confirmed = await proxy?.$modal.confirm('是否确认删除运维-物资-出入库单管理编号为"' + _ids + '"的数据项?');
if (!confirmed) return;
loading.value = true;
await delChurukudan(_ids);
proxy?.$modal.msgSuccess("删除成功");
await getList();
} catch (error) {
console.error('删除出入库单失败:', error);
proxy?.$modal.msgError("删除失败,请稍后重试");
} finally {
loading.value = false;
}
}
// 柱状图数据获取
const fetchChuRuKuCountBarData = async () => {
if (!queryParams.value.projectId) {
return;
}
let data = {
projectId: queryParams.value.projectId,
startDate: currentMonthDates[0].fullDate,
endDate: currentMonthDates[currentMonthDates.length - 1].fullDate,
}
try {
const res = await getChuRuKuCountBar(data);
console.log(res);
// 这里可以添加数据处理和图表更新的逻辑
} catch (error) {
console.error('获取柱状图数据失败:', error);
// 可以选择是否显示错误提示根据UI需求决定
// proxy?.$modal.msgError("获取统计数据失败");
}
}
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
fetchChuRuKuCountBarData();
}
}, { immediate: true, deep: true });
onMounted(() => {
getList();
fetchChuRuKuCountBarData();
});
// 组件卸载时清空projectId
onUnmounted(() => {
queryParams.value.projectId = undefined;
form.value.projectId = undefined;
});
</script>

View File

@ -0,0 +1,57 @@
<template>
<div class="plan-details">
<el-row>
<el-col>
<el-card>
<div class="header">
<span class="back-arrow" @click="handleBack">
<el-icon>
<ArrowLeft />
</el-icon>
</span>
<h2>Q2风电轴承采购计划</h2>
</div>
</el-card>
</el-col>
</el-row>
<el-row gutter="10">
<el-col :span="18">
<el-card>
<detailInfo />
</el-card>
</el-col>
<el-col :span="6" style="flex-grow: 1;">
<el-card style="height: 100%;">
<DetailsProcess />
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped lang="scss">
.plan-details {
padding: 20px;
background-color: #F1F7FB;
}
.header {
display: flex;
align-items: center;
}
.back-arrow {
font-size: 20px;
margin-right: 10px;
cursor: pointer;
}
</style>
<script setup>
import detailInfo from './components/detailInfo.vue';
import DetailsProcess from './components/DetailsProcess.vue';
const router = useRouter();
const handleBack = () => {
router.back();
}
</script>

View File

@ -0,0 +1,818 @@
<template>
<div class="procurementPlan">
<el-row :gutter="20">
<el-col :span="13">
<el-card>
<div style="display: flex;align-items: center;height: 120px;justify-content: space-around;">
<div class="img">
<img src="/assets/caigou.png" alt="">
</div>
<div class="item">
<div class="text">
待审批计划
</div>
<div class="count" style="color: rgba(255, 178, 30, 1);">
12
</div>
</div>
<div class="item">
<div class="text">
已批准计划
</div>
<div class="count" style="color: rgba(67, 101, 220, 1);">
28
</div>
</div>
<div class="item">
<div class="text">
采购中计划
</div>
<div class="count" style="color: rgba(113, 214, 213, 1);">
15
</div>
</div>
<div class="item">
<div class="text">
已完成计划
</div>
<div class="count" style="color: rgba(0, 184, 122, 1);">
86
</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="11">
<el-card>
<div style="display: flex;align-items: center;height: 120px;justify-content: space-around;">
<div class="img">
<img src="/assets/qian.jpg" alt="">
</div>
<div class="item">
<div class="text">
本年度已采购金额
</div>
<div class="count" style="color: rgba(255, 153, 0, 1);">
520,000.00
</div>
</div>
<div class="item">
<div class="text">
本年度采购预算金额
</div>
<div class="count" style="color: rgba(67, 101, 220, 1);">
3,000,000.00
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row style="margin-top: 20px;">
<el-col :span="24">
<el-card style="border-radius: 10px;">
<div class="content">
<div class="tabs">
<el-button type="success">导出</el-button>
<el-button type="primary" @click="isNewProcurementDialogVisible = true">新建采购申请单</el-button>
</div>
<!-- 标签页导航 -->
<div class="tabs">
<!-- <el-badge :value="pendingCount" type="warning">
<el-button :type="activeTab === 'pending' ? 'primary' : ''"
@click="changeTab('pending')">待审批</el-button>
</el-badge>
<el-badge :value="purchasingCount" type="info">
<el-button :type="activeTab === 'purchasing' ? 'primary' : ''"
@click="changeTab('purchasing')">采购中</el-button>
</el-badge>
<el-badge :value="rejectedCount" type="danger">
<el-button :type="activeTab === 'rejected' ? 'primary' : ''"
@click="changeTab('rejected')">
未通过
</el-button>
</el-badge>
<el-badge :value="approvedCount" type="primary">
<el-button :type="activeTab === 'approved' ? 'primary' : ''"
@click="changeTab('approved')">已通过</el-button>
</el-badge>
<el-badge :value="completedCount" type="success">
<el-button :type="activeTab === 'completed' ? 'primary' : ''"
@click="changeTab('completed')">已完成</el-button>
</el-badge> -->
</div>
<!-- 表格 -->
<el-table :data="caigouPlanList" border style="width: 100%;margin-top: 15px;">
<el-table-column label="计划编号" align="center" prop="jihuaBianhao" />
<el-table-column label="计划名称" align="center" prop="jihuaName" />
<el-table-column label="申请部门" align="center" prop="caigouDanweiName" />
<el-table-column label="申请人" align="center" prop="jingbanrenName" />
<el-table-column prop="createTime" label="申请日期" align="center" />
<el-table-column label="预计金额" align="center" prop="yujiJine" />
<el-table-column label="状态" align="center" prop="status">
<template #default="scope">
<dict-tag :options="wz_caigou_examine" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="80" align="center">
<template #default="scope">
<el-button type="text" @click="handleView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-section">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-dialog v-model="isNewProcurementDialogVisible" title="新建采购申请单" width="60%" :close-on-click-modal="false">
<div class="new-procurement-form">
<!-- 基础信息 -->
<div class="form-section">
<h3>基础信息</h3>
<!-- 输入框行 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划名称">
<el-input v-model="form.jihuaName" placeholder="请填写计划名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合同名称">
<el-input v-model="form.hetonName" placeholder="请填写合同名称" />
</el-form-item>
</el-col>
</el-row>
<!-- 下拉框行 -->
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="合同类型">
<el-select v-model="form.hetonType" placeholder="请选择">
<el-option v-for="option in wz_contract_type" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="采购类型">
<el-select v-model="form.caigouType" placeholder="请选择">
<el-option v-for="option in wz_purchase_type" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="仓库地址">
<el-input v-model="form.cangkuUrl" placeholder="请输入仓库地址" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 供应商信息 -->
<div class="form-section">
<h3>供应商信息</h3>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="供应商单位">
<el-select v-model="form.danwei" placeholder="请选择">
<!-- <el-option v-for="option in supplierList" :key="option.value" :label="option.label"
:value="option.value" /> -->
<el-option label="供应商1" value="供应商1" />
<el-option label="供应商1" value="供应商1" />
<el-option label="供应商1" value="供应商1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="送货时间">
<el-date-picker v-model="form.chuhuoTime" type="date" placeholder="请选择送货日期"
value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 产品信息 -->
<div class="form-section">
<h3>产品信息</h3>
<el-table :data="form.opsCaigouPlanChanpinBos" border style="width: 100%">
<el-table-column prop="chanpinName" label="产品名称">
<template #default="scope">
<el-input v-model="scope.row.chanpinName" placeholder="请填写" />
</template>
</el-table-column>
<el-table-column prop="chanpinType" label="产品型号">
<template #default="scope">
<el-input v-model="scope.row.chanpinType" placeholder="请填写" />
</template>
</el-table-column>
<el-table-column prop="chanpinMonovalent" label="产品单价">
<template #default="scope">
<el-input v-model="scope.row.chanpinMonovalent" placeholder="请填写" type="number"
@change="calculateTotalPrice(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="goumaiNumber" label="购买数量">
<template #default="scope">
<el-input v-model="scope.row.goumaiNumber" placeholder="请填写" type="number"
@change="calculateTotalPrice(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="danwei" label="单位">
<template #default="scope">
<el-input v-model="scope.row.danwei" placeholder="请填写" />
</template>
</el-table-column>
<el-table-column prop="totalPrice" label="合计" :formatter="calculateTotalPrice">
<template #default="scope">
<span>{{ calculateTotalPrice(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="80">
<template #default="scope">
<el-button type="text" @click="removeProduct(scope.$index)"
:disabled="form.opsCaigouPlanChanpinBos.length <= 1">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-button type="primary" size="small" @click="addProduct" style="margin-top: 10px">添加产品</el-button>
</div>
<!-- 合同条款 -->
<div class="form-section">
<h3>合同条款</h3>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="付款方式">
<el-select v-model="form.fukuantiaojian" placeholder="请选择">
<el-option v-for="option in wz_payment_terms" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="发票开具方式">
<el-select v-model="form.fapiaoKjfs" placeholder="请选择">
<el-option v-for="option in wz_invoicing_way" :key="option.value"
:label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</div>
<!-- 附件上传 -->
<div class="form-section">
<h3>附件上传</h3>
<file-upload ref="fileUploadRef" :isDrag="true" :file-list="form.opsCaigouPlanFilesBos"
:is-show-tip="false"
@update:file-list="handleUpdateFileList"
:file-type="['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf', 'png', 'jpg', 'jpeg']" />
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelNewProcurement">取消</el-button>
<el-button @click="saveDraft" :loading="buttonLoading">保存草稿</el-button>
<el-button type="primary" @click="submitProcurement" :loading="buttonLoading">提交申请</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style sc oped lang="scss">
.procurementPlan {
background-color: #F2F8FC;
padding: 20px;
}
.img {
img {
display: block;
width: 80px;
height: 80px;
}
}
.item {
text-align: center;
.text {
font-size: 14px;
}
.count {
font-size: 25px;
font-weight: 600;
text-align: left;
margin-top: 10px;
}
}
.tabs {
display: flex;
gap: 10px;
padding: 10px 0;
}
.content {
padding: 10px 0;
}
/* 分页区域样式 */
.pagination-section {
background-color: #fff;
border-radius: 8px;
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
padding: 10px 0;
}
.pagination-controls .el-pagination {
margin: 0;
}
.pagination-controls .el-pagination button {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 4px;
}
.pagination-controls .el-pagination .el-pager li {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 4px;
}
.pagination-controls .el-pagination .el-pager li.active {
background-color: #409eff;
color: #fff;
}
</style>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useProcurementDraftStore } from '@/store/modules/procurementDraft';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { wz_invoicing_way, wz_payment_terms, wz_purchase_type, wz_contract_type, wz_caigou_examine } = toRefs<any>(proxy?.useDict('wz_invoicing_way', 'wz_payment_terms', 'wz_purchase_type', 'wz_contract_type', 'wz_caigou_examine'));
import { listCaigouPlan, getSupplierList, addCaigouPlan } from '@/api/wuziguanli/caigouPlan';
import { CaigouPlanVO, CaigouPlanQuery, CaigouPlanForm } from '@/api/wuziguanli/caigouPlan/types';
import { useRouter } from 'vue-router';
const router = useRouter();
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 获取用户store
const userStore = useUserStore();
const caigouPlanList = ref<CaigouPlanVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const total = ref(0);
const initFormData: CaigouPlanForm = {
id: undefined,
projectId: undefined,
jihuaName: undefined,
jihuaBianhao: undefined,
caigouDanwei: undefined,
caigouDanweiName: undefined,
jingbanren: undefined,
jingbanrenName: undefined,
hetonType: undefined,
caigouType: undefined,
cangkuUrl: undefined,
hetonName: undefined,
gonyingshangId: 1,
chuhuoTime: undefined,
fukuantiaojian: undefined,
fapiaoKjfs: undefined,
status: undefined,
shenheStatus: undefined,
yujiJine: undefined,
shijiJine: undefined,
opsCaigouPlanFilesBos: [],
opsCaigouPlanChanpinBos: [
{
chanpinName: '',
chanpinType: '',
chanpinMonovalent: 0,
goumaiNumber: 0,
danwei: '',
totalPrice: 0
}
],
}
const data = reactive<PageData<CaigouPlanForm, CaigouPlanQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
jihuaName: undefined,
jihuaBianhao: undefined,
caigouDanwei: undefined,
caigouDanweiName: undefined,
jingbanren: undefined,
jingbanrenName: undefined,
hetonType: undefined,
caigouType: undefined,
cangkuUrl: undefined,
hetonName: undefined,
gonyingshangId: 1,
chuhuoTime: undefined,
fukuantiaojian: undefined,
fapiaoKjfs: undefined,
status: undefined,
shenheStatus: undefined,
yujiJine: undefined,
shijiJine: undefined,
opsCaigouPlanChanpinBos: [
{
chanpinName: '',
chanpinType: '',
chanpinMonovalent: 0,
goumaiNumber: 0,
danwei: '',
totalPrice: 0
}
],
opsCaigouPlanFilesBos: undefined,
params: {
}
},
rules: {}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询运维-物资-采购计划单列表 */
const getList = async () => {
loading.value = true;
const res = await listCaigouPlan(queryParams.value);
caigouPlanList.value = res.rows;
total.value = res.total;
loading.value = false;
}
// 新增采购计划单
const addCaigouPlans = async () => {
buttonLoading.value = true; // 显示按钮加载状态
try {
// 提交表单数据到后端
const res = await addCaigouPlan(form.value);
if (res.code === 200) {
ElMessage({ message: '采购申请单已成功提交,等待审批!', type: 'success' });
// 刷新列表数据
getList();
// 关闭对话框并重置表单
resetNewProcurementForm();
isNewProcurementDialogVisible.value = false;
} else {
// 显示详细的错误信息
ElMessage({
message: res.msg || '新增采购计划单失败,请重试',
type: 'error'
});
}
} catch (error) {
ElMessage({ message: '失败', type: 'error' });
} finally {
buttonLoading.value = false; // 无论成功失败,都关闭加载状态
}
}
// 采购商列表
const supplierList = ref([]);
const getSupplierLists = async () => {
const res = await getSupplierList({
projectId: userStore.selectedProject.id
});
supplierList.value = res.rows;
}
onMounted(() => {
getList();
getSupplierLists();
});
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
}
}, { immediate: true, deep: true });
// 新建采购申请单对话框是否可见
const isNewProcurementDialogVisible = ref(false);
// 跳转查看详情
const handleView = (row) => {
router.push({
path: '/materialManagement/planDetails',
query: {
id: row.id
}
});
};
// 计算产品总价
const calculateTotalPrice = (row) => {
if (!row.chanpinMonovalent || !row.goumaiNumber) {
row.totalPrice = '0.00'; // 保存计算结果到对象中
return '0.00';
}
const price = parseFloat(row.chanpinMonovalent);
const quantity = parseInt(row.goumaiNumber);
if (isNaN(price) || isNaN(quantity)) {
row.totalPrice = '0.00'; // 保存计算结果到对象中
return '0.00';
}
const result = (price * quantity).toFixed(2);
row.totalPrice = result; // 保存计算结果到对象中
return result;
};
// 添加产品
const addProduct = () => {
form.value.opsCaigouPlanChanpinBos.push({
chanpinName: '',
chanpinType: '',
chanpinMonovalent: 0,
goumaiNumber: 0,
danwei: '',
totalPrice: 0
});
};
// 删除产品
const removeProduct = (index) => {
if (form.value.opsCaigouPlanChanpinBos.length <= 1) {
ElMessage({ message: '至少保留一个产品信息', type: 'warning' });
return;
}
form.value.opsCaigouPlanChanpinBos.splice(index, 1);
};
// 重置新建采购申请表单
const resetNewProcurementForm = () => {
form.value.jihuaName = '';
form.value.hetonName = '';
form.value.hetonType = '';
form.value.caigouType = '';
form.value.cangkuUrl = '';
form.value.danwei = '';
form.value.chuhuoTime = '';
form.value.fukuantiaojian = '';
form.value.fapiaoKjfs = '';
form.value.opsCaigouPlanChanpinBos = [{
chanpinName: '',
chanpinType: '',
chanpinMonovalent: '',
goumaiNumber: '',
danwei: '',
totalPrice: ''
}];
form.value.opsCaigouPlanFilesBos = [];
};
// 取消新建采购申请
const cancelNewProcurement = () => {
// 检查是否有未保存的内容
const hasContent = Object.values(form.value).some(value => {
if (Array.isArray(value)) {
return value.length > 0 &&
value.some(item =>
typeof item === 'object' &&
Object.values(item).some(v => v)
);
}
return !!value;
});
if (hasContent) {
ElMessageBox.confirm('表单内容尚未保存,确定要关闭吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
resetNewProcurementForm();
isNewProcurementDialogVisible.value = false;
});
} else {
resetNewProcurementForm();
isNewProcurementDialogVisible.value = false;
}
};
// 草稿校验函数
const validateDraft = () => {
// 草稿只需要计划名称作为必填项
if (!form.value.jihuaName.trim()) {
ElMessage({ message: '请填写计划名称', type: 'error' });
return false;
}
// 检查已填写的产品信息的有效性
for (let i = 0; i < form.value.opsCaigouPlanChanpinBos.length; i++) {
const product = form.value.opsCaigouPlanChanpinBos[i];
if (product.chanpinName && !product.chanpinType) {
ElMessage({ message: `${i + 1}行产品:填写了产品名称,请也填写产品型号`, type: 'warning' });
}
if (product.productPrice && parseFloat(product.productPrice) <= 0) {
ElMessage({ message: `${i + 1}行产品产品单价应大于0`, type: 'warning' });
}
if (product.purchaseQuantity && parseInt(product.purchaseQuantity) <= 0) {
ElMessage({ message: `${i + 1}行产品购买数量应大于0`, type: 'warning' });
}
}
return true;
};
// 保存草稿
const saveDraft = async () => {
// 验证草稿
if (!validateDraft()) {
return;
}
buttonLoading.value = true; // 显示按钮加载状态
try {
// 使用pinia store保存草稿
const draftStore = useProcurementDraftStore();
const savedDraft = draftStore.saveDraft(form.value.jihuaName, form.value);
console.log('保存草稿:', {
draftNumber: savedDraft.draftNumber,
saveTime: savedDraft.saveTime,
content: savedDraft.content
});
ElMessage({ message: `草稿已成功保存(编号:${savedDraft.draftNumber}),您可以在草稿箱中查看`, type: 'success' });
} catch (error) {
console.error('保存草稿失败:', error);
ElMessage({
message: '保存草稿失败,请重试',
type: 'error'
});
} finally {
buttonLoading.value = false; // 无论成功失败,都关闭加载状态
}
};
// 表单校验函数
const validateForm = () => {
// 基础信息校验
if (!form.value.jihuaName.trim()) {
ElMessage({ message: '请填写计划名称', type: 'error' });
return false;
}
if (!form.value.hetonName.trim()) {
ElMessage({ message: '请填写合同名称', type: 'error' });
return false;
}
if (!form.value.hetonType) {
ElMessage({ message: '请选择合同类型', type: 'error' });
return false;
}
if (!form.value.caigouType) {
ElMessage({ message: '请选择采购类型', type: 'error' });
return false;
}
if (!form.value.cangkuUrl) {
ElMessage({ message: '请选择仓库地址', type: 'error' });
return false;
}
if (!form.value.danwei) {
ElMessage({ message: '请选择供应商单位', type: 'error' });
return false;
}
// 产品信息校验
const hasValidProduct = form.value.opsCaigouPlanChanpinBos.some(product => {
return product.chanpinName &&
product.chanpinType &&
product.chanpinMonovalent && parseFloat(product.chanpinMonovalent) > 0 &&
product.goumaiNumber && parseInt(product.goumaiNumber) > 0 &&
product.danwei;
});
if (!hasValidProduct) {
ElMessage({ message: '请至少填写一个有效的产品信息', type: 'error' });
return false;
}
// 检查每个产品的有效性
for (let i = 0; i < form.value.opsCaigouPlanChanpinBos.length; i++) {
const product = form.value.opsCaigouPlanChanpinBos[i];
if (product.chanpinName || product.chanpinType || product.chanpinMonovalent || product.goumaiNumber) {
if (!product.chanpinName) {
ElMessage({ message: `${i + 1}行产品:请填写产品名称`, type: 'error' });
return false;
}
if (!product.chanpinType) {
ElMessage({ message: `${i + 1}行产品:请填写产品型号`, type: 'error' });
return false;
}
if (!product.chanpinMonovalent) {
ElMessage({ message: `${i + 1}行产品:请填写产品单价`, type: 'error' });
return false;
}
if (parseFloat(product.chanpinMonovalent) <= 0) {
ElMessage({ message: `${i + 1}行产品产品单价必须大于0`, type: 'error' });
return false;
}
if (!product.goumaiNumber) {
ElMessage({ message: `${i + 1}行产品:请填写购买数量`, type: 'error' });
return false;
}
if (parseInt(product.goumaiNumber) <= 0) {
ElMessage({ message: `${i + 1}行产品购买数量必须大于0`, type: 'error' });
return false;
}
if (!product.danwei) {
ElMessage({ message: `${i + 1}行产品:请填写单位`, type: 'error' });
return false;
}
}
}
return true;
};
// 提交申请
const submitProcurement = async () => {
// 在提交前,为所有产品行重新计算并保存总价
form.value.opsCaigouPlanChanpinBos.forEach(product => {
calculateTotalPrice(product);
});
// 表单验证
if (!validateForm()) {
return;
}
try {
// 确认提交
await ElMessageBox.confirm(
'确定要提交采购申请单吗?提交后将进入审批流程,不可撤销。',
'确认提交',
{
confirmButtonText: '确认提交',
cancelButtonText: '取消',
type: 'warning'
}
);
// 调用提交函数
await addCaigouPlans();
} catch (error) {
// 处理用户取消或其他错误
if (error !== 'cancel') {
console.error('提交采购申请单时发生错误:', error);
ElMessage({ message: '提交过程中发生错误,请重试', type: 'error' });
}
}
}
// });
// 处理文件上传完成后获取完整文件列表
const handleUpdateFileList = (fileList) => {
form.value.opsCaigouPlanFilesBos = fileList.map(file => ({
fileId: file.ossId,
fileName: file.name,
fileUrl: file.url,
}));
};
</script>

View File

@ -0,0 +1,654 @@
<template>
<div style="padding: 20px;background-color: #F2F8FC;">
<TitleComponent title="备品备件管理" subtitle="管理光伏和风电设备的所有备品备件信息" />
<el-card style="border-radius: 10px;">
<div class="title">
数据总览
</div>
<div class="list">
<div class="item">
<div class="left">
<div style="font-size: 14px;color:rgba(102, 102, 102, 1)">总备件数量</div>
<div style="margin: 10px 0;">
<span style="font-size: 24px;font-weight: bold;margin-right: 10px;">2,548</span>
<span></span>
</div>
<div>
<div style="display: flex;align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 5.7748L15.15 5.69982L15.15 5.62481L15.075 5.62481C15.075 5.62481 15 5.62481 14.925 5.5498L11.25 5.5498C10.875 5.5498 10.575 5.84981 10.575 6.2248C10.575 6.59982 10.875 6.8998 11.25 6.8998L13.125 6.8998L9.52501 10.4998L7.72501 8.6998C7.35 8.32481 6.60001 8.32481 6.225 8.6998L3.075 11.8498C2.85 12.0748 2.85 12.5248 3.075 12.7498C3.22501 12.8998 3.37501 12.9748 3.525 12.9748C3.67501 12.9748 3.82501 12.9748 3.975 12.7498L6.9 9.8248L8.7 11.6248C9.07501 11.9998 9.825 11.9998 10.2 11.6248L13.95 7.87481L13.95 9.74981C13.95 10.1248 14.25 10.4248 14.625 10.4248C15 10.4248 15.3 10.1248 15.3 9.74981L15.3 6.37482L15.3 6.14982L15.15 5.7748Z"
fill="#00B87A"></path>
</svg>
<div>
<span
style="color: rgba(0, 184, 122, 1);font-size: 14px;margin: 0 5px;margin-right: 10px;">8.2%</span>
<span style="color: rgba(154, 154, 154, 1);font-size: 14px;">较昨日同期</span>
</div>
</div>
</div>
</div>
<div class="right">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34"
height="34" viewBox="0 0 34 34" fill="none">
<rect x="0" y="0" width="34" height="34" rx="4" fill="#186DF5"></rect>
<path
d="M8.72815 24.0051C8.31696 24.0051 7.9743 24.3478 7.9743 24.759C7.9743 25.1702 8.31696 25.5128 8.72815 25.5128L16.2256 25.5128C16.6368 25.5128 16.9794 25.1702 16.9794 24.759C16.9794 24.3478 16.6368 24.0051 16.2256 24.0051L8.72815 24.0051ZM8.74186 19.4957C8.33067 19.4957 7.988 19.8384 7.988 20.2496C7.988 20.6608 8.33067 21.0034 8.74186 21.0034L13.2513 21.0034C13.6625 21.0034 14.0051 20.6608 14.0051 20.2496C14.0051 19.8384 13.6625 19.4957 13.2513 19.4957L8.74186 19.4957ZM5 16.494C5 15.6716 5.67162 15 6.494 15L27.506 15C28.3284 15 29 15.6716 29 16.494L29 26.6505C29 27.6648 28.1776 28.5009 27.1496 28.5009L6.85037 28.5009C5.83609 28.5009 5 27.6785 5 26.6505L5 16.494Z"
fill="#FFFFFF"></path>
<path
d="M17.7242 12.4974L17.7242 6.494C17.7242 5.67162 18.3959 5 19.2182 5L23.4124 5C24.0018 5 24.5638 5.28784 24.9201 5.76756L28.9772 12.4974L28.9772 12.5385C29.0458 13.2787 28.3467 13.9366 27.6066 13.9914L19.2182 13.9914C18.3959 13.9914 17.7242 13.3198 17.7242 12.4974ZM14.7499 13.9914L6.3753 13.9914C5.63515 13.9229 4.93612 13.2787 5.00466 12.5385L5.00466 12.5248L5.00466 12.4974L9.06177 5.76756C9.40443 5.28784 9.96639 5 10.5695 5L14.7637 5C15.586 5 16.2577 5.67162 16.2577 6.494L16.2577 12.4974C16.2577 13.3198 15.586 13.9914 14.7499 13.9914Z"
fill="#FFFFFF"></path>
</svg>
</div>
</div>
<div class="item">
<div class="left">
<div style="font-size: 14px;color:rgba(102, 102, 102, 1)">总备件数量</div>
<div style="margin: 10px 0;">
<span style="font-size: 24px;font-weight: bold;margin-right: 10px;">2,548</span>
<span></span>
</div>
<div>
<div style="display: flex;align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 5.7748L15.15 5.69982L15.15 5.62481L15.075 5.62481C15.075 5.62481 15 5.62481 14.925 5.5498L11.25 5.5498C10.875 5.5498 10.575 5.84981 10.575 6.2248C10.575 6.59982 10.875 6.8998 11.25 6.8998L13.125 6.8998L9.52501 10.4998L7.72501 8.6998C7.35 8.32481 6.60001 8.32481 6.225 8.6998L3.075 11.8498C2.85 12.0748 2.85 12.5248 3.075 12.7498C3.22501 12.8998 3.37501 12.9748 3.525 12.9748C3.67501 12.9748 3.82501 12.9748 3.975 12.7498L6.9 9.8248L8.7 11.6248C9.07501 11.9998 9.825 11.9998 10.2 11.6248L13.95 7.87481L13.95 9.74981C13.95 10.1248 14.25 10.4248 14.625 10.4248C15 10.4248 15.3 10.1248 15.3 9.74981L15.3 6.37482L15.3 6.14982L15.15 5.7748Z"
fill="#E32727"></path>
</svg>
<div>
<span
style="color: rgba(227, 39, 39, 1);font-size: 14px;margin: 0 5px;margin-right: 10px;">8.2%</span>
<span style="color: rgba(154, 154, 154, 1);font-size: 14px;">较昨日同期</span>
</div>
</div>
</div>
</div>
<div class="right">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34"
height="34" viewBox="0 0 34 34" fill="none">
<rect x="0" y="0" width="34" height="34" rx="4" fill="#186DF5"></rect>
<path
d="M8.72815 24.0051C8.31696 24.0051 7.9743 24.3478 7.9743 24.759C7.9743 25.1702 8.31696 25.5128 8.72815 25.5128L16.2256 25.5128C16.6368 25.5128 16.9794 25.1702 16.9794 24.759C16.9794 24.3478 16.6368 24.0051 16.2256 24.0051L8.72815 24.0051ZM8.74186 19.4957C8.33067 19.4957 7.988 19.8384 7.988 20.2496C7.988 20.6608 8.33067 21.0034 8.74186 21.0034L13.2513 21.0034C13.6625 21.0034 14.0051 20.6608 14.0051 20.2496C14.0051 19.8384 13.6625 19.4957 13.2513 19.4957L8.74186 19.4957ZM5 16.494C5 15.6716 5.67162 15 6.494 15L27.506 15C28.3284 15 29 15.6716 29 16.494L29 26.6505C29 27.6648 28.1776 28.5009 27.1496 28.5009L6.85037 28.5009C5.83609 28.5009 5 27.6785 5 26.6505L5 16.494Z"
fill="#FFFFFF"></path>
<path
d="M17.7242 12.4974L17.7242 6.494C17.7242 5.67162 18.3959 5 19.2182 5L23.4124 5C24.0018 5 24.5638 5.28784 24.9201 5.76756L28.9772 12.4974L28.9772 12.5385C29.0458 13.2787 28.3467 13.9366 27.6066 13.9914L19.2182 13.9914C18.3959 13.9914 17.7242 13.3198 17.7242 12.4974ZM14.7499 13.9914L6.3753 13.9914C5.63515 13.9229 4.93612 13.2787 5.00466 12.5385L5.00466 12.5248L5.00466 12.4974L9.06177 5.76756C9.40443 5.28784 9.96639 5 10.5695 5L14.7637 5C15.586 5 16.2577 5.67162 16.2577 6.494L16.2577 12.4974C16.2577 13.3198 15.586 13.9914 14.7499 13.9914Z"
fill="#FFFFFF"></path>
</svg>
</div>
</div>
<div class="item">
<div class="left">
<div style="font-size: 14px;color:rgba(102, 102, 102, 1)">总备件数量</div>
<div style="margin: 10px 0;">
<span style="font-size: 24px;font-weight: bold;margin-right: 10px;">2,548</span>
<span></span>
</div>
<div>
<div style="display: flex;align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 5.7748L15.15 5.69982L15.15 5.62481L15.075 5.62481C15.075 5.62481 15 5.62481 14.925 5.5498L11.25 5.5498C10.875 5.5498 10.575 5.84981 10.575 6.2248C10.575 6.59982 10.875 6.8998 11.25 6.8998L13.125 6.8998L9.52501 10.4998L7.72501 8.6998C7.35 8.32481 6.60001 8.32481 6.225 8.6998L3.075 11.8498C2.85 12.0748 2.85 12.5248 3.075 12.7498C3.22501 12.8998 3.37501 12.9748 3.525 12.9748C3.67501 12.9748 3.82501 12.9748 3.975 12.7498L6.9 9.8248L8.7 11.6248C9.07501 11.9998 9.825 11.9998 10.2 11.6248L13.95 7.87481L13.95 9.74981C13.95 10.1248 14.25 10.4248 14.625 10.4248C15 10.4248 15.3 10.1248 15.3 9.74981L15.3 6.37482L15.3 6.14982L15.15 5.7748Z"
fill="#00B87A"></path>
</svg>
<div>
<span
style="color: rgba(0, 184, 122, 1);font-size: 14px;margin: 0 5px;margin-right: 10px;">8.2%</span>
<span style="color: rgba(154, 154, 154, 1);font-size: 14px;">较昨日同期</span>
</div>
</div>
</div>
</div>
<div class="right">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34"
height="34" viewBox="0 0 34 34" fill="none">
<rect x="0" y="0" width="34" height="34" rx="4" fill="#186DF5"></rect>
<path
d="M8.72815 24.0051C8.31696 24.0051 7.9743 24.3478 7.9743 24.759C7.9743 25.1702 8.31696 25.5128 8.72815 25.5128L16.2256 25.5128C16.6368 25.5128 16.9794 25.1702 16.9794 24.759C16.9794 24.3478 16.6368 24.0051 16.2256 24.0051L8.72815 24.0051ZM8.74186 19.4957C8.33067 19.4957 7.988 19.8384 7.988 20.2496C7.988 20.6608 8.33067 21.0034 8.74186 21.0034L13.2513 21.0034C13.6625 21.0034 14.0051 20.6608 14.0051 20.2496C14.0051 19.8384 13.6625 19.4957 13.2513 19.4957L8.74186 19.4957ZM5 16.494C5 15.6716 5.67162 15 6.494 15L27.506 15C28.3284 15 29 15.6716 29 16.494L29 26.6505C29 27.6648 28.1776 28.5009 27.1496 28.5009L6.85037 28.5009C5.83609 28.5009 5 27.6785 5 26.6505L5 16.494Z"
fill="#FFFFFF"></path>
<path
d="M17.7242 12.4974L17.7242 6.494C17.7242 5.67162 18.3959 5 19.2182 5L23.4124 5C24.0018 5 24.5638 5.28784 24.9201 5.76756L28.9772 12.4974L28.9772 12.5385C29.0458 13.2787 28.3467 13.9366 27.6066 13.9914L19.2182 13.9914C18.3959 13.9914 17.7242 13.3198 17.7242 12.4974ZM14.7499 13.9914L6.3753 13.9914C5.63515 13.9229 4.93612 13.2787 5.00466 12.5385L5.00466 12.5248L5.00466 12.4974L9.06177 5.76756C9.40443 5.28784 9.96639 5 10.5695 5L14.7637 5C15.586 5 16.2577 5.67162 16.2577 6.494L16.2577 12.4974C16.2577 13.3198 15.586 13.9914 14.7499 13.9914Z"
fill="#FFFFFF"></path>
</svg>
</div>
</div>
<div class="item">
<div class="left">
<div style="font-size: 14px;color:rgba(102, 102, 102, 1)">总备件数量</div>
<div style="margin: 10px 0;">
<span style="font-size: 24px;font-weight: bold;margin-right: 10px;">2,548</span>
<span></span>
</div>
<div>
<div style="display: flex;align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 5.7748L15.15 5.69982L15.15 5.62481L15.075 5.62481C15.075 5.62481 15 5.62481 14.925 5.5498L11.25 5.5498C10.875 5.5498 10.575 5.84981 10.575 6.2248C10.575 6.59982 10.875 6.8998 11.25 6.8998L13.125 6.8998L9.52501 10.4998L7.72501 8.6998C7.35 8.32481 6.60001 8.32481 6.225 8.6998L3.075 11.8498C2.85 12.0748 2.85 12.5248 3.075 12.7498C3.22501 12.8998 3.37501 12.9748 3.525 12.9748C3.67501 12.9748 3.82501 12.9748 3.975 12.7498L6.9 9.8248L8.7 11.6248C9.07501 11.9998 9.825 11.9998 10.2 11.6248L13.95 7.87481L13.95 9.74981C13.95 10.1248 14.25 10.4248 14.625 10.4248C15 10.4248 15.3 10.1248 15.3 9.74981L15.3 6.37482L15.3 6.14982L15.15 5.7748Z"
fill="#00B87A"></path>
</svg>
<div>
<span
style="color: rgba(0, 184, 122, 1);font-size: 14px;margin: 0 5px;margin-right: 10px;">8.2%</span>
<span style="color: rgba(154, 154, 154, 1);font-size: 14px;">较昨日同期</span>
</div>
</div>
</div>
</div>
<div class="right">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34"
height="34" viewBox="0 0 34 34" fill="none">
<rect x="0" y="0" width="34" height="34" rx="4" fill="#186DF5"></rect>
<path
d="M8.72815 24.0051C8.31696 24.0051 7.9743 24.3478 7.9743 24.759C7.9743 25.1702 8.31696 25.5128 8.72815 25.5128L16.2256 25.5128C16.6368 25.5128 16.9794 25.1702 16.9794 24.759C16.9794 24.3478 16.6368 24.0051 16.2256 24.0051L8.72815 24.0051ZM8.74186 19.4957C8.33067 19.4957 7.988 19.8384 7.988 20.2496C7.988 20.6608 8.33067 21.0034 8.74186 21.0034L13.2513 21.0034C13.6625 21.0034 14.0051 20.6608 14.0051 20.2496C14.0051 19.8384 13.6625 19.4957 13.2513 19.4957L8.74186 19.4957ZM5 16.494C5 15.6716 5.67162 15 6.494 15L27.506 15C28.3284 15 29 15.6716 29 16.494L29 26.6505C29 27.6648 28.1776 28.5009 27.1496 28.5009L6.85037 28.5009C5.83609 28.5009 5 27.6785 5 26.6505L5 16.494Z"
fill="#FFFFFF"></path>
<path
d="M17.7242 12.4974L17.7242 6.494C17.7242 5.67162 18.3959 5 19.2182 5L23.4124 5C24.0018 5 24.5638 5.28784 24.9201 5.76756L28.9772 12.4974L28.9772 12.5385C29.0458 13.2787 28.3467 13.9366 27.6066 13.9914L19.2182 13.9914C18.3959 13.9914 17.7242 13.3198 17.7242 12.4974ZM14.7499 13.9914L6.3753 13.9914C5.63515 13.9229 4.93612 13.2787 5.00466 12.5385L5.00466 12.5248L5.00466 12.4974L9.06177 5.76756C9.40443 5.28784 9.96639 5 10.5695 5L14.7637 5C15.586 5 16.2577 5.67162 16.2577 6.494L16.2577 12.4974C16.2577 13.3198 15.586 13.9914 14.7499 13.9914Z"
fill="#FFFFFF"></path>
</svg>
</div>
</div>
</div>
<div style="margin-top: 30px;">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<!-- 第一排输入框 -->
<div style="width: 100%; margin-bottom: 10px;">
<el-form-item label="备件编号" prop="beijianNumber" style="margin-right: 20px;">
<el-input v-model="queryParams.beijianNumber" placeholder="请输入备件编号" clearable
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="备件名称" prop="beijianName" style="margin-right: 20px;">
<el-input v-model="queryParams.beijianName" placeholder="请输入备件名称" clearable
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="规格型号" prop="guigexinghao">
<el-input v-model="queryParams.guigexinghao" placeholder="请输入规格型号" clearable
@keyup.enter="handleQuery" />
</el-form-item>
</div>
<!-- 第二排下拉框和按钮 -->
<div style="width: 100%;">
<el-form-item label="设备类型" prop="shebeiType" style="margin-right: 20px;">
<el-select v-model="queryParams.shebeiType" placeholder="请选择设备类型" clearable>
<el-option v-for="dict in wz_device_type" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="库存状态" prop="kucunStatus" style="margin-right: 20px;">
<el-select v-model="queryParams.kucunStatus" placeholder="请选择库存状态" clearable>
<el-option v-for="dict in wz_inventory_type" :key="dict.value"
:label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</div>
</el-form>
</el-card>
</div>
</transition>
<el-table v-loading="loading" border :data="beipinBeijianList" style="width: 100%;margin-top: 10px;">
<el-table-column label="备件编号" align="center" prop="beijianNumber" />
<el-table-column label="备件名称" align="center" prop="beijianName" />
<el-table-column label="设备类型" align="center" prop="shebeiType">
<template #default="scope">
{{ getDictLabel(wz_device_type, scope.row.shebeiType) }}
</template>
</el-table-column>
<el-table-column label="规格型号" align="center" prop="guigexinghao" />
<el-table-column label="库存数量" align="center" prop="kucunCount" />
<el-table-column label="库存状态" align="center" prop="kucunStatus">
<template #default="scope">
<dict-tag :options="wz_inventory_type" :value="scope.row.kucunStatus"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button type="text" @click="handleUpdate(scope.row)"
v-hasPermi="['personnel:beipinBeijian:edit']">编辑</el-button>
<el-button type="text" @click="handleDetail(scope.row)"
v-hasPermi="['personnel:beipinBeijian:query']">详情</el-button>
<el-button type="text" @click="handleDelete(scope.row)"
v-hasPermi="['personnel:beipinBeijian:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-section">
<div class="pagination-info">
显示第{{ (data.queryParams.pageNum - 1) * data.queryParams.pageSize + 1 }}{{ Math.min(data.queryParams.pageNum * data.queryParams.pageSize, total) }}共有{{
total }}条记录
</div>
<div class="pagination-controls">
<pagination v-show="total > 0" :total="total" v-model:page="data.queryParams.pageNum"
v-model:limit="data.queryParams.pageSize" @pagination="getList" />
</div>
</div>
</div>
</el-card>
<!-- 编辑弹窗 -->
<el-dialog title="编辑备件信息" v-model="dialog.visible" width="50%" append-to-body>
<el-form ref="beipinBeijianFormRef" :model="form" :rules="rules" label-width="120px"
style="max-width: 600px;">
<el-form-item label="备件编号" prop="beijianNumber">
<el-input v-model="form.beijianNumber" placeholder="请输入备件编号" />
</el-form-item>
<el-form-item label="备件名称" prop="beijianName">
<el-input v-model="form.beijianName" placeholder="请输入备件名称" />
</el-form-item>
<el-form-item label="规格型号" prop="guigexinghao">
<el-input v-model="form.guigexinghao" placeholder="请输入规格型号" />
</el-form-item>
<el-form-item label="库存数量" prop="kucunCount">
<el-input v-model="form.kucunCount" placeholder="请输入库存数量" />
</el-form-item>
<el-form-item label="库存状态" prop="kucunStatus">
<el-select v-model="form.kucunStatus" placeholder="请选择库存状态">
<el-option v-for="dict in wz_inventory_type" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备类型" prop="shebeiType">
<el-select v-model="form.shebeiType" placeholder="请选择设备类型">
<el-option v-for="dict in wz_device_type" :key="dict.value" :label="dict.label"
:value="dict.value"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog title="备件详情" v-model="detailDialogVisible" width="50%" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="备件编号">{{ detailData.beijianNumber }}</el-descriptions-item>
<el-descriptions-item label="备件名称">{{ detailData.beijianName }}</el-descriptions-item>
<el-descriptions-item label="规格型号">{{ detailData.guigexinghao }}</el-descriptions-item>
<el-descriptions-item label="设备类型">{{ getDictLabel(wz_device_type, detailData.shebeiType)
}}</el-descriptions-item>
<el-descriptions-item label="库存数量">{{ detailData.kucunCount }}</el-descriptions-item>
<el-descriptions-item label="库存状态">
<dict-tag :options="wz_inventory_type" :value="detailData.kucunStatus"></dict-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="closeDetailDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.title {
font-weight: bold;
font-size: 22px;
font-weight: 400;
letter-spacing: 0px;
line-height: 28.6px;
color: rgba(10, 14, 26, 1);
}
.list {
margin-top: 30px;
display: flex;
gap: 140px;
justify-content: space-between;
}
.item {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30px;
}
/* 分页区域样式 */
.pagination-section {
background-color: #fff;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination-info {
font-size: 14px;
color: #606266;
}
.pagination-controls .el-pagination {
margin: 0;
}
.pagination-controls .el-pagination__sizes {
margin-right: 20px;
}
.pagination-controls .el-pagination button {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 4px;
}
.pagination-controls .el-pagination .el-pager li {
min-width: 32px;
height: 32px;
line-height: 32px;
border-radius: 4px;
}
.pagination-controls .el-pagination .el-pager li.active {
background-color: #409eff;
color: #fff;
}
/* 详情弹窗样式 */
.detail-container {
padding: 20px 0;
}
.detail-item {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.detail-label {
width: 120px;
font-weight: 500;
color: #303133;
margin-right: 20px;
}
.detail-value {
flex: 1;
color: #606266;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
padding-top: 20px;
}
</style>
<script setup lang="ts">
import { ref, computed } from 'vue';
import TitleComponent from '@/components/TitleComponent/index.vue';
// 导入用户store
import { useUserStore } from '@/store/modules/user';
// 获取用户store
const userStore = useUserStore();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
import { listBeipinBeijian, getBeipinBeijian, delBeipinBeijian, updateBeipinBeijian } from '@/api/wuziguanli/beijian';
import { BeipinBeijianVO, BeipinBeijianQuery, BeipinBeijianForm } from '@/api/wuziguanli/beijian/types';
const { wz_inventory_type, wz_device_type } = toRefs<any>(proxy?.useDict('wz_inventory_type', 'wz_spareparts_type', 'wz_device_type'));
const beipinBeijianList = ref<BeipinBeijianVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const total = ref(0);
// 详情相关数据
const detailData = ref<BeipinBeijianVO>({
id: undefined,
projectId: undefined,
beijianNumber: undefined,
beijianName: undefined,
shebeiType: undefined,
guigexinghao: undefined,
kucunStatus: undefined,
kucunCount: undefined,
});
const detailDialogVisible = ref(false);
const queryFormRef = ref<ElFormInstance>();
const beipinBeijianFormRef = ref<ElFormInstance>();
const dialog = reactive<DialogOption>({
visible: false,
title: ''
});
const initFormData: BeipinBeijianForm = {
id: undefined,
projectId: undefined,
beijianNumber: undefined,
beijianName: undefined,
shebeiType: undefined,
guigexinghao: undefined,
kucunStatus: undefined,
kucunCount: undefined,
}
const data = reactive<PageData<BeipinBeijianForm, BeipinBeijianQuery>>({
form: { ...initFormData },
queryParams: {
pageNum: 1,
pageSize: 10,
projectId: undefined,
beijianNumber: undefined,
beijianName: undefined,
shebeiType: undefined,
guigexinghao: undefined,
kucunStatus: undefined,
kucunCount: undefined,
params: {
}
},
rules: {
beijianName: [
{ required: true, message: "备件名称不能为空", trigger: "blur" }
],
shebeiType: [
{ required: true, message: "设备类型不能为空", trigger: "change" }
],
guigexinghao: [
{ required: true, message: "规格型号不能为空", trigger: "blur" }
],
kucunStatus: [
{ required: true, message: "库存状态不能为空", trigger: "change" }
],
kucunCount: [
{ required: true, message: "库存数量不能为空", trigger: "blur" }
],
}
});
const { queryParams, form, rules } = toRefs(data);
// 根据字典值获取标签信息的辅助函数
const getDictLabel = (dictType, value) => {
// 健壮性检查
if (!value || !dictType || !Array.isArray(dictType)) {
return value;
}
// 使用find方法更高效地查找匹配项
const option = dictType.find(item => item?.value === value);
// 如果找到匹配项,返回标签,否则返回原始值
return option?.label || value;
};
/** 查询运维-物资-备品配件列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listBeipinBeijian(queryParams.value);
beipinBeijianList.value = res.rows;
total.value = res.total;
} catch (error) {
proxy?.$modal.msgError('获取数据失败,请重试');
console.error('获取备品配件列表失败:', error);
} finally {
loading.value = false;
}
}
/** 取消按钮 */
const cancel = () => {
reset();
dialog.visible = false;
}
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
beipinBeijianFormRef.value?.resetFields();
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 修改按钮操作 */
const handleUpdate = async (row?: BeipinBeijianVO) => {
reset();
const _id = row?.id || ids.value[0];
if (!_id) {
proxy?.$modal.msgWarning('请选择需要编辑的数据');
return;
}
try {
const res = await getBeipinBeijian(_id);
Object.assign(form.value, res.data);
dialog.visible = true;
} catch (error) {
proxy?.$modal.msgError('获取数据失败,请重试');
console.error('获取备品配件详情失败:', error);
}
}
/** 详情按钮操作 */
const handleDetail = async (row?: BeipinBeijianVO) => {
const _id = row?.id || ids.value[0];
if (!_id) {
proxy?.$modal.msgWarning('请选择需要查看的数据');
return;
}
try {
const res = await getBeipinBeijian(_id);
detailData.value = res.data;
detailDialogVisible.value = true;
} catch (error) {
proxy?.$modal.msgError('获取数据失败,请重试');
console.error('获取备品配件详情失败:', error);
}
}
/** 关闭详情弹窗 */
const closeDetailDialog = () => {
detailDialogVisible.value = false;
}
/** 提交按钮 */
const submitForm = () => {
beipinBeijianFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
try {
if (form.value.id) {
await updateBeipinBeijian(form.value);
}
proxy?.$modal.msgSuccess("操作成功");
dialog.visible = false;
await getList();
} catch (error) {
proxy?.$modal.msgError('操作失败,请重试');
console.error('提交表单失败:', error);
} finally {
buttonLoading.value = false;
}
}
});
}
/** 删除按钮操作 */
const handleDelete = async (row?: BeipinBeijianVO) => {
const _ids = row?.id || ids.value;
if (!_ids || (_ids instanceof Array && _ids.length === 0)) {
proxy?.$modal.msgWarning('请选择需要删除的数据');
return;
}
try {
await proxy?.$modal.confirm('是否确认删除运维-物资-备品配件编号为"' + _ids + '"的数据项?');
loading.value = true;
await delBeipinBeijian(_ids);
proxy?.$modal.msgSuccess("删除成功");
await getList();
} catch (error) {
// 如果是用户取消确认,则不显示错误信息
if (error !== 'cancel') {
proxy?.$modal.msgError('删除失败,请重试');
console.error('删除数据失败:', error);
}
} finally {
loading.value = false;
}
}
// 监听用户选择的项目变化
watch(() => userStore.selectedProject, (newProject) => {
if (newProject && newProject.id) {
queryParams.value.projectId = newProject.id;
// 只在新增表单时设置projectId编辑表单保留原有值
if (!form.value.id) {
form.value.projectId = newProject.id;
}
// 调用getList刷新数据
getList();
}
}, { immediate: true, deep: true });
onMounted(() => {
getList();
});
// 组件卸载时清空projectId
onUnmounted(() => {
queryParams.value.projectId = undefined;
form.value.projectId = undefined;
});
</script>

View File

@ -9,50 +9,19 @@
<span class="update-time">截止至2025/06/30 12:00</span>
</el-col>
</el-row>
<!-- 关键指标卡片区域 -->
<el-row class="metrics-container" :gutter="0">
<el-col :span="6">
<el-col v-for="card in cardData" :key="card.key" :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.todayAlarmTotal }}</div>
<div class="metric-label">今日报警总数</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates.todayAlarmTotal.type">
{{ props.dashboardData.updates.todayAlarmTotal.type === 'up' ? '↑' : '↓' }}{{ props.dashboardData.updates.todayAlarmTotal.value }}
</span></div>
</div>
</el-col>
<el-col :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.unhandledAlarms }}</div>
<div class="metric-label">未处理报警</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates.unhandledAlarms.type">
{{ props.dashboardData.updates.unhandledAlarms.type === 'up' ? '↑' : '↓' }}{{ props.dashboardData.updates.unhandledAlarms.value }}
</span></div>
</div>
</el-col>
<el-col :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.handledAlarms }}</div>
<div class="metric-label">已处理报警</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates.handledAlarms.type">
{{ props.dashboardData.updates.handledAlarms.type === 'up' ? '↑' : '↓' }}{{ props.dashboardData.updates.handledAlarms.value }}
</span></div>
</div>
</el-col>
<el-col :span="6">
<div class="metric-card">
<div class="metric-value">{{ props.dashboardData.avgProcessTime }}</div>
<div class="metric-label">平均处理时长</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates.avgProcessTime.type">
{{ props.dashboardData.updates.avgProcessTime.type === 'up' ? '↑' : '↓' }}{{ props.dashboardData.updates.avgProcessTime.value }}
</span></div>
<div class="metric-value">{{ props.dashboardData[card.key] }}</div>
<div class="metric-label">{{ card.label }}</div>
<div class="metric-change">较上周 <span :class="props.dashboardData.updates[card.updateKey].type">
<img v-if="props.dashboardData.updates[card.updateKey].type === 'up'" src="/src/assets/demo/up.png"
class="trend-icon" alt="上升">
<img v-else src="/src/assets/demo/down.png" class="trend-icon" alt="下降">{{
props.dashboardData.updates[card.updateKey].value }}
</span>
</div>
</div>
</el-col>
</el-row>
@ -67,7 +36,7 @@
<el-option label="近90天" value="90days" />
</el-select>
</div>
<el-col :span="24">
<div class="trend-section">
<el-row :gutter="20">
@ -76,11 +45,17 @@
<div class="chart-container">
<div class="chart-left-right-layout">
<div class="chart-info-container">
<div class="chart-title">报警数量() </div>
<div class="chart-title">报警数量() </div>
<div class="chart-total">{{ props.chartData.totals.alarmCount }}</div>
<div class="chart-value">较昨日 <span :class="props.chartData.dailyChanges.alarmCount.type">
{{ props.chartData.dailyChanges.alarmCount.type === 'up' ? '↑' : '↓' }}{{ props.chartData.dailyChanges.alarmCount.value }}
</span></div>
<div class="chart-value">
<span>较昨日</span>
<img v-if="props.chartData.dailyChanges.processEfficiency.type === 'up'"
src="/src/assets/demo/up.png" class="trend-icon" alt="上升">
<img v-else src="/src/assets/demo/down.png" class="trend-icon" alt="下降">
<span :class="props.chartData.dailyChanges.processEfficiency.type">
{{ props.chartData.dailyChanges.processEfficiency.value }}
</span>
</div>
</div>
<div ref="alarmCountRef" class="chart-content"></div>
</div>
@ -93,9 +68,15 @@
<div class="chart-info-container">
<div class="chart-title">报警处理效率(%)</div>
<div class="chart-total">{{ props.chartData.totals.processEfficiency }}</div>
<div class="chart-value">较昨日 <span :class="props.chartData.dailyChanges.processEfficiency.type">
{{ props.chartData.dailyChanges.processEfficiency.type === 'up' ? '↑' : '↓' }}{{ props.chartData.dailyChanges.processEfficiency.value }}
</span></div>
<div class="chart-value">
<span>较昨日</span>
<img v-if="props.chartData.dailyChanges.processEfficiency.type === 'up'"
src="/src/assets/demo/up.png" class="trend-icon" alt="上升">
<img v-else src="/src/assets/demo/down.png" class="trend-icon" alt="下降">
<span :class="props.chartData.dailyChanges.processEfficiency.type">
{{ props.chartData.dailyChanges.processEfficiency.value }}
</span>
</div>
</div>
<div ref="processEfficiencyRef" class="chart-content"></div>
</div>
@ -141,13 +122,37 @@ const props = defineProps({
processEfficiency: '89%'
},
dailyChanges: {
alarmCount: { value: '0.9%', type: 'down' },
alarmCount: { value: '0.9%', type: 'up' },
processEfficiency: { value: '0.9%', type: 'down' }
}
})
}
});
// 卡片数据配置
const cardData = [
{
key: 'todayAlarmTotal',
label: '今日报警总数',
updateKey: 'todayAlarmTotal'
},
{
key: 'unhandledAlarms',
label: '未处理报警',
updateKey: 'unhandledAlarms'
},
{
key: 'handledAlarms',
label: '已处理报警',
updateKey: 'handledAlarms'
},
{
key: 'avgProcessTime',
label: '平均处理时长',
updateKey: 'avgProcessTime'
}
];
const timeRange = ref('7days');
const alarmCountRef = ref(null);
const processEfficiencyRef = ref(null);
@ -172,7 +177,7 @@ const initCharts = () => {
trigger: 'axis',
},
grid: {
left: '-45px',
left: '-38px',
right: '0%',
bottom: '0%',
top: '0%',
@ -248,7 +253,7 @@ const initCharts = () => {
trigger: 'axis',
},
grid: {
left: '-45px',
left: '-38px',
right: '0%',
bottom: '0%',
top: '0%',
@ -352,7 +357,7 @@ onUnmounted(() => {
width: 100%;
height: 100%;
background: #fff;
padding:0 20px;
padding: 0 20px;
box-sizing: border-box;
}
@ -411,11 +416,18 @@ onUnmounted(() => {
}
.up {
color: #ff4d4f;
color: #00B87A;
}
.down {
color: #52c41a;
color: #ff4d4f;
}
.trend-icon {
// width: 12px;
// height: 12px;
margin-right: 2px;
vertical-align: middle;
}
.trend-container {
@ -436,7 +448,7 @@ onUnmounted(() => {
}
.trend-header {
align-items: center;
}
@ -483,6 +495,24 @@ onUnmounted(() => {
font-size: 12px;
color: #999;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 4px;
line-height: 1;
}
.chart-value span {
display: inline-flex;
align-items: center;
vertical-align: middle;
}
.chart-value .trend-icon {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
margin: 0;
}
.chart-content {

View File

@ -1,11 +1,13 @@
<template>
<div class="pie-chart-container">
<!-- 标题栏 -->
<div class="chart-header">
<TitleComponent title="报警类型分布" :fontLevel="2" />
<el-select v-model="selectedTimeRange" placeholder="选择时间范围" size="small">
<el-option label="今日" value="today" />
</el-select>
</div>
<!-- 图表 -->
<div ref="pieChartRef" class="chart-content"></div>
</div>
</template>
@ -27,6 +29,7 @@ const selectedTimeRange = ref('today');
const pieChartRef = ref(null);
let chartInstance = null;
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);

View File

@ -68,9 +68,9 @@ const dashboardData = ref({
handledAlarms: 16,
avgProcessTime: '42分钟',
updates: {
todayAlarmTotal: { value: '4.2%', type: 'down' },
unhandledAlarms: { value: '5%', type: 'up' },
handledAlarms: { value: '8%', type: 'down' },
todayAlarmTotal: { value: '4.2%', type: 'up' },
unhandledAlarms: { value: '5%', type: 'down' },
handledAlarms: { value: '8%', type: 'up' },
avgProcessTime: { value: '10%', type: 'down' }
}
});
@ -84,7 +84,7 @@ const chartData = ref({
processEfficiency: '89%'
},
dailyChanges: {
alarmCount: { value: '0.9%', type: 'down' },
alarmCount: { value: '0.9%', type: 'up' },
processEfficiency: { value: '0.9%', type: 'down' }
}
});
@ -155,7 +155,7 @@ onMounted(() => {
</script>
<style scoped lang="scss">
.model {
padding: 0px 15px;
padding: 20px 15px;
background-color: rgba(242, 248, 252, 1);
}
</style>

View File

@ -2,7 +2,7 @@
<div style="padding: 20px;">
<el-row>
<el-col :span="15">
<el-row>
<el-row style="margin: 20px 0;">
<TitleComponent title="设备情况" subtitle="电站一次监控数据" />
<sbqk />
</el-row>

View File

@ -6,14 +6,8 @@
<div class="card-title">总发电量</div>
<div class="card-value">{{ props.statusData.totalPower }}</div>
<div class="card-change positive">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18"
height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 5.77505L15.15 5.70006L15.15 5.62505L15.075 5.62505C15.075 5.62505 15 5.62505 14.925 5.55005L11.25 5.55005C10.875 5.55005 10.575 5.85005 10.575 6.22505C10.575 6.60006 10.875 6.90005 11.25 6.90005L13.125 6.90005L9.52501 10.5L7.72501 8.70005C7.35 8.32505 6.60001 8.32505 6.225 8.70005L3.075 11.85C2.85 12.075 2.85 12.525 3.075 12.75C3.22501 12.9001 3.37501 12.975 3.525 12.975C3.67501 12.975 3.82501 12.975 3.975 12.75L6.9 9.82505L8.7 11.625C9.07501 12.0001 9.825 12.0001 10.2 11.625L13.95 7.87505L13.95 9.75006C13.95 10.1251 14.25 10.4251 14.625 10.4251C15 10.4251 15.3 10.1251 15.3 9.75006L15.3 6.37506L15.3 6.15006L15.15 5.77505Z"
fill="#00B87A">
</path>
</svg>
{{ props.statusData.totalPowerChange }} 较上周
<img src="/src/assets/demo/up.png" alt="" class="change-icon">
<span>{{ props.statusData.totalPowerChange }} 较上周</span>
</div>
</div>
<div class="card-right">
@ -37,14 +31,8 @@
<div class="card-title">系统效率</div>
<div class="card-value">{{ props.statusData.efficiency }}</div>
<div class="card-change negative">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18"
height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 12.2252L15.15 12.3002L15.15 12.3752L15.075 12.3752C15.075 12.3752 15 12.3752 14.925 12.4502L11.25 12.4502C10.875 12.4502 10.575 12.1502 10.575 11.7752C10.575 11.4002 10.875 11.1002 11.25 11.1002L13.125 11.1002L9.52501 7.5002L7.72501 9.3002C7.35 9.67519 6.60001 9.67519 6.225 9.3002L3.075 6.1502C2.85 5.9252 2.85 5.4752 3.075 5.2502C3.22501 5.10019 3.37501 5.0252 3.525 5.0252C3.67501 5.0252 3.82501 5.0252 3.975 5.2502L6.9 8.1752L8.7 6.3752C9.07501 6.00019 9.825 6.00019 10.2 6.3752L13.95 10.1252L13.95 8.25019C13.95 7.87519 14.25 7.57519 14.625 7.57519C15 7.57519 15.3 7.87519 15.3 8.25019L15.3 11.6252L15.3 11.8502L15.15 12.2252Z"
fill="#E32727">
</path>
</svg>
{{ props.statusData.efficiencyChange }} 较上周
<img src="/src/assets/demo/down.png" alt="" class="change-icon">
<span>{{ props.statusData.efficiencyChange }} 较上周</span>
</div>
</div>
<div class="card-right">
@ -65,14 +53,8 @@
<div class="card-title">组件温度</div>
<div class="card-value">{{ props.statusData.temperature }}</div>
<div class="card-change positive">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18"
height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 5.77505L15.15 5.70006L15.15 5.62505L15.075 5.62505C15.075 5.62505 15 5.62505 14.925 5.55005L11.25 5.55005C10.875 5.55005 10.575 5.85005 10.575 6.22505C10.575 6.60006 10.875 6.90005 11.25 6.90005L13.125 6.90005L9.52501 10.5L7.72501 8.70005C7.35 8.32505 6.60001 8.32505 6.225 8.70005L3.075 11.85C2.85 12.075 2.85 12.525 3.075 12.75C3.22501 12.9001 3.37501 12.975 3.525 12.975C3.67501 12.975 3.82501 12.975 3.975 12.75L6.9 9.82505L8.7 11.625C9.07501 12.0001 9.825 12.0001 10.2 11.625L13.95 7.87505L13.95 9.75006C13.95 10.1251 14.25 10.4251 14.625 10.4251C15 10.4251 15.3 10.1251 15.3 9.75006L15.3 6.37506L15.3 6.15006L15.15 5.77505Z"
fill="#00B87A">
</path>
</svg>
{{ props.statusData.temperatureChange }} 较上周
<img src="/src/assets/demo/up.png" alt="" class="change-icon">
<span>{{ props.statusData.temperatureChange }} 较上周</span>
</div>
</div>
<div class="card-right">
@ -93,14 +75,8 @@
<div class="card-title">日照强度</div>
<div class="card-value">{{ props.statusData.sunlight }}</div>
<div class="card-change positive">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18"
height="18" viewBox="0 0 18 18" fill="none">
<path
d="M15.15 5.77505L15.15 5.70006L15.15 5.62505L15.075 5.62505C15.075 5.62505 15 5.62505 14.925 5.55005L11.25 5.55005C10.875 5.55005 10.575 5.85005 10.575 6.22505C10.575 6.60006 10.875 6.90005 11.25 6.90005L13.125 6.90005L9.52501 10.5L7.72501 8.70005C7.35 8.32505 6.60001 8.32505 6.225 8.70005L3.075 11.85C2.85 12.075 2.85 12.525 3.075 12.75C3.22501 12.9001 3.37501 12.975 3.525 12.975C3.67501 12.975 3.82501 12.975 3.975 12.75L6.9 9.82505L8.7 11.625C9.07501 12.0001 9.825 12.0001 10.2 11.625L13.95 7.87505L13.95 9.75006C13.95 10.1251 14.25 10.4251 14.625 10.4251C15 10.4251 15.3 10.1251 15.3 9.75006L15.3 6.37506L15.3 6.15006L15.15 5.77505Z"
fill="#00B87A">
</path>
</svg>
{{ props.statusData.sunlightChange }} 较上周
<img src="/src/assets/demo/up.png" alt="" class="change-icon">
<span>{{ props.statusData.sunlightChange }} 较上周</span>
</div>
</div>
<div class="card-right">
@ -206,7 +182,7 @@ const props = defineProps({
margin-top: 4px;
&.positive {
color: #67c23a;
color: #00B87A;
}
&.negative {
@ -216,7 +192,7 @@ const props = defineProps({
.change-icon {
font-size: 10px;
margin-right: 2px;
margin-right: 5px;
}
.card-icon {

View File

@ -142,7 +142,7 @@ onUnmounted(() => {
.chart-content {
width: 100%;
height: calc(100% - 80px);
min-height: 200px;
min-height: 220px;
}
@media (max-width: 768px) {

View File

@ -185,7 +185,7 @@ onUnmounted(() => {
.chart-content {
width: 100%;
height: calc(100% - 80px);
min-height: 200px;
min-height: 220px;
}
@media (max-width: 768px) {

View File

@ -104,7 +104,7 @@ const mockData = ref({
totalPower: '3,456.8KWh',
totalPowerChange: '8.2%',
efficiency: '18.7%',
efficiencyChange: '-0.3%',
efficiencyChange: '0.3%',
temperature: '42.3°C',
temperatureChange: '2.1°C',
sunlight: '865 W/m²',

View File

@ -8,7 +8,8 @@
</div>
<div class="power-amount">{{ value || '2,456.8' }} <span>{{ unit || 'KWh' }}</span></div>
<div class="power-growth">
<span class="growth-value">{{ growth || '+2.5%' }}</span>
<img :src="type === 'up' ? '/src/assets/demo/up.png' : '/src/assets/demo/down.png'" alt="">
<span :class="type === 'up' ? 'up' : 'down'">{{ growth+'%' || '2.5'+'%' }}</span>
<span class="growth-label">{{ growthLabel || '较昨日' }}</span>
</div>
</div>
@ -37,6 +38,14 @@ const props = defineProps({
type: String,
default: '平均效率'
},
type: {
type: String,
default: ''
},
type: {
type: String,
default: ''
},
value: {
type: String,
default: '2,456.8'
@ -247,11 +256,14 @@ onUnmounted(() => {
flex-shrink: 0;
}
.growth-value {
font-size: 14px;
color: #13C2C2;
.up{
font-size: 14px;
color:#00B87A;
}
.down{
font-size: 14px;
color:#ff4d4f;
}
.growth-label {
font-size: 12px;
color: #999;

View File

@ -43,25 +43,20 @@
</el-row>
<!-- 数据展示-->
<el-row :gutter="24">
<el-col :span="6">
<itembox title="总发电量" value="2,456.8" unit="KWh" growth="+2.5%" growthLabel="较昨日" color="#186DF5"
chartType="bar" power="" icon-src="/src/assets/demo/shandian.png"
:chartData="[30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]"></itembox>
</el-col>
<el-col :span="6">
<itembox title="平均效率" value="18.7" unit="%" growth="+2.5%" growthLabel="较昨日" color="#00B87A"
chartType="line" icon-src="/src/assets/demo/huojian.png"
:chartData="[30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]"></itembox>
</el-col>
<el-col :span="6">
<itembox title="设备温度" value="43.5" unit="℃" growth="+2.5%" growthLabel="较昨日" color="#FFC300"
chartType="line" icon-src="/src/assets/demo/wendu.png"
:chartData="[30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]"></itembox>
</el-col>
<el-col :span="6">
<itembox title="系统可用性" value="18.7" unit="%" growth="+2.5%" growthLabel="较昨日" color="#7948EA"
chartType="line" icon-src="/src/assets/demo/use.png"
:chartData="[30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]"></itembox>
<el-col :span="6" v-for="(item, index) in itemBoxData" :key="index">
<itembox
:title="item.title"
:value="item.value"
:unit="item.unit"
:growth="item.growth"
:growthLabel="item.growthLabel"
:color="item.color"
:chartType="item.chartType"
:power="item.power"
:icon-src="item.iconSrc"
:type="item.type"
:chartData="item.chartData">
</itembox>
</el-col>
</el-row>
<!-- 第一行图表 -->
@ -159,6 +154,62 @@ const fenxiLineData = ref({
}
});
// 创建itembox数据数组用于循环渲染
const itemBoxData = ref([
{
title: '总发电量',
value: '2,456.8',
unit: 'KWh',
growth: '2.5',
growthLabel: '较昨日',
color: '#186DF5',
chartType: 'bar',
power: '',
iconSrc: '/src/assets/demo/shandian.png',
type: 'up',
chartData: [30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]
},
{
title: '平均效率',
value: '18.7',
unit: '%',
growth: '2.5',
growthLabel: '较昨日',
color: '#00B87A',
chartType: 'line',
power: '',
iconSrc: '/src/assets/demo/huojian.png',
type: 'up',
chartData: [30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]
},
{
title: '设备温度',
value: '43.5',
unit: '℃',
growth: '2.5',
growthLabel: '较昨日',
color: '#FFC300',
chartType: 'line',
power: '',
iconSrc: '/src/assets/demo/wendu.png',
type: 'up',
chartData: [30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]
},
{
title: '系统可用性',
value: '18.7',
unit: '%',
growth: '2.5',
growthLabel: '较昨日',
color: '#7948EA',
chartType: 'line',
power: '',
iconSrc: '/src/assets/demo/use.png',
type: 'up',
chartData: [30, 50, 40, 60, 80, 70, 100, 90, 85, 75, 65, 55]
}
]);
const tableData = ref([
{ time: '00:00', irradiance: 0, powerGeneration: 0.0, efficiency: 24.5, moduleTemperature: 23.5, inverterTemperature: 21.2, status: '停机' },
{ time: '08:00', irradiance: 12.5, powerGeneration: 17.2, efficiency: 28.1, moduleTemperature: 26.3, inverterTemperature: 20.3, status: '正常' },

View File

@ -0,0 +1,75 @@
<template>
<div class="allAlarms">
<el-row>
<el-col :span="12">
<TitleComponent title="所有告警"></TitleComponent>
</el-col>
<el-col :span="12">
<el-row class="right-align-row">
<el-col :span="8">
<el-input prefix-icon="search" placeholder="搜索告警信息"></el-input>
</el-col>
<el-col :span="4" style="text-align: right;">
<el-button icon="download" type="primary">
导出数据
</el-button>
</el-col>
</el-row>
</el-col>
</el-row>
<el-table :data="tableData" style="width: 100%;">
<el-table-column label="告警编号" prop="id"></el-table-column>
<el-table-column label="告警名称" prop="title"></el-table-column>
<el-table-column label="告警等级" prop="level"></el-table-column>
<el-table-column label="告警时间" prop="alarmTime"></el-table-column>
<el-table-column label="负责人" prop="responsible"></el-table-column>
<el-table-column label="告警描述" prop="description"></el-table-column>
<el-table-column label="状态">
<template #default="scope">
<el-tag
:type="scope.row.status === 1 ? 'success' : scope.row.status === 2 ? 'warning' : 'danger'">{{
scope.row.status === 1 ? '已解决' : scope.row.status === 2 ? '待解决' : '未解决' }}</el-tag>
</template>
</el-table-column>
</el-table>
<div style="background-color: #fff;padding: 20px 0;">
<el-pagination layout="prev, pager, next, jumper,sizes" :total="totalRecords"
v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]"
@current-change="handlePageChange" @size-change="handleSizeChange"></el-pagination>
</div>
</div>
</template>
<style scoped>
.allAlarms {
background-color: #F2F8FC;
padding: 20px;
}
.right-align-row {
display: flex;
justify-content: flex-end;
}
</style>
<script setup>
import TitleComponent from '@/components/TitleComponent';
const pageSize = ref(20);
const totalRecords = ref(100);
const tableData = computed(() => {
return Array.from({ length: 15 }).map((_, index) => {
return {
id: index,
title: `逆变器温度过高`,
id: `INV-2023-003`,
level: `二级`,
alarmTime: '2025-09-18 18:00',
// 预计解决时间
resolveTime: '2025-09-19 18:00',
// 负责人
responsible: '李华(现场运维组)',
description: `AAAABBBCCCDE...`,
status: Math.floor(Math.random() * 3) + 1
}
})
})
</script>

View File

@ -0,0 +1,95 @@
<template>
<el-dialog :visible="dialogVisible" width="1200" id="custom-dialog" class="no-header-dialog normal">
<div class="back">
<div class="alarm-alert-content success">
<el-row>
<el-col :span="20">
<div class="top">
<div class="info">
<div class="title">通信中断</div>
<div class="alarm-id">告警IDINV-2023-003</div>
<div class="status-box">
<div class="status red">状态待处理</div>
<div class="last-update">最后更新刚刚</div>
</div>
</div>
<div class="info-box">
<div class="list">
<div class="item">
<div>
<div class="title">告警位置</div>
<div class="text">光伏区A区-3-08</div>
</div>
<div>
<div class="title">告警级别</div>
<div class="text">二级告警紧急</div>
</div>
</div>
<div class="item">
<div>
<div class="title">预计解决时间</div>
<div class="text">2025-09-19 18:00</div>
</div>
<div>
<div class="title">上报时间</div>
<div class="text">2025-09-18 18:00</div>
</div>
</div>
<div class="item">
<div>
<div class="title">设备负责人</div>
<div class="text">李华现场运维组</div>
</div>
<div>
<div class="title">通知方式</div>
<div class="text">系统消息短信电话</div>
</div>
</div>
</div>
</div>
</div>
<el-divider vertical border-style="dashed"></el-divider>
<!-- 进度 -->
<div class="progress-box" style="margin-bottom: 24px;">
<div class="title">处理进度</div>
<el-progress :text-inside="true" color="#ABABAB" :stroke-width="10" :percentage="3"
:show-text="false" />
</div>
<div class="notice-box">
<div class="item">
<div class="title active">告警产生并通知</div>
<div class="time">2025-09-19 18:05</div>
<div class="content">系统检测到告警并通知负责人</div>
</div>
<div class="item">
<div class="title">任务指派</div>
<div class="time"></div>
<div class="content">指派任务给相关责任人</div>
</div>
<div class="item">
<div class="title">开始处理</div>
<div class="time"></div>
<div class="content">运维人员开始处理告警</div>
</div>
<div class="item">
<div class="title">告警产生</div>
<div class="time"></div>
<div class="content">告警已解决并记录结果</div>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</el-dialog>
</template>
<script setup>
const props = defineProps({
dialogVisible: {
type: Boolean,
required: true, // 若外部必传设为true否则可加default: false
default: false // 可选:若外部可能不传,设置默认值
}
});
</script>

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
<template>
<el-card shadow="never" style="border-radius: 10px;">
<div style="margin-bottom: 20px;display: flex;align-items: center;justify-content: right;">
<span style="margin-right: 5px;color: rgba(113, 128, 150, 1);font-size: 14px;">
<span style="margin-right: 5px;color: rgba(113, 128, 150, 1);font-size: 14px;" @click="handleClick">
查看全部告警信息
</span>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="8"
@ -207,4 +207,11 @@
}
}
</style>
<script setup></script>
<script setup>
const router = useRouter();
const handleClick = () => {
console.log('查看全部告警信息');
router.push('/pvSystem/allAlarms');
}
</script>

View File

@ -2,13 +2,16 @@
<div class="cardItem">
<el-card>
<div class="tianqi"
style="display: flex; flex-direction: column; align-items: center; background-color: #fafafa; border-radius: 10px; padding-bottom: 40px">
<div>
<img src="/assets/Sunny.png" style="display: block; width: 100px; height: 100px" alt="" />
style="display: flex; flex-direction: column; align-items: center; background-color: #fafafa; border-radius: 10px; padding-bottom: 40px;padding: 20px;">
<div
style="width: 100%;display: flex;flex-direction: column;align-items: center;background-color: #FFFFFF;margin: 0 20px;border-radius: 15px;padding: 20px;">
<div>
<img src="/assets/Sunny.png" style="display: block; width: 100px; height: 100px" alt="" />
</div>
<div style="font-family: 'Alibaba-PuHuiTi-Bold'; font-size: 24px">31</div>
<div>晴朗</div>
<div style="color: rgba(154, 154, 154, 1); font-size: 14px">紫外线强度<span></span></div>
</div>
<div style="font-family: 'Alibaba-PuHuiTi-Bold'; font-size: 24px">31</div>
<div>晴朗</div>
<div style="color: rgba(154, 154, 154, 1); font-size: 14px">紫外线强度<span></span></div>
<div class="tianqi2">
<div class="item">
<div>

View File

@ -1,6 +1,8 @@
<template>
<el-card shadow="never" style="border-radius: 10px;">
<el-form :inline="true" :model="formInline" label-width="120" style="display: flex; justify-content: center;">
<el-form ref="formRef" :inline="true" :model="formInline" label-width="120"
style="display: flex; justify-content: center;">
<el-form-item label="规则编号">
<el-input v-model="formInline.user" placeholder="请输入规则编号" clearable />
</el-form-item>
@ -42,7 +44,7 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template #default="scope">
<el-button link type="primary">详情</el-button>
<el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
<el-button link type="danger">处理</el-button>
<el-button link type="warning">维护记录</el-button>
</template>
@ -53,11 +55,24 @@
<pagination :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</el-card>
<CustomDialogStatus v-model="CustomDialogStatusVisible" v-model:dialogVisible="dialogVisible" :height="dialogHeight"
close-on-click-modal />
<CustomDialogAlarm ref=" dialogAlarmRef" v-model="CustomDialogAlarmVisible" :height="dialogHeight"
@opened="adjustDialogHeight" close-on-click-modal />
</template>
<style lang="scss" scoped></style>
<script setup>
import CustomDialogStatus from './CustomDialogStatus.vue'
import CustomDialogAlarm from './CustomDialogAlarm.vue'
const formInline = ref({})
const total = ref(0);
const loading = ref(false);
const CustomDialogStatusVisible = ref(false);
const CustomDialogAlarmVisible = ref(false);
const listData = [
{ id: "INV-2023-001", shuchu: "12.8kw", xiaolv: "98.2%", wendu: "42℃", fadian: "158.5kWh", status: 1 },
{ id: "INV-2023-001", shuchu: "12.8kw", xiaolv: "98.2%", wendu: "42℃", fadian: "158.5kWh", status: 1 },
@ -83,6 +98,13 @@ const statusMap = {
const initFormData = {
};
const handleDetail = (row) => {
if (row.status === 1) {
CustomDialogStatusVisible.value = true;
} else {
CustomDialogAlarmVisible.value = true;
}
}
const data = reactive({
form: { ...initFormData },
queryParams: {

View File

@ -5,7 +5,8 @@
<TitleComponent title="XXX电站·逆变器综合监控" subtitle="实时监控X台逆变器运行状态、发电趋势及环境信息" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<el-col :span="12"
style="display: flex; justify-content: flex-end; align-items: center;margin-bottom: 20px;">
<!-- 子col1输入框容器 -->
<el-col :span="8">
<el-input placeholder="请输入逆变器...">
@ -32,7 +33,7 @@
</el-col>
</el-col>
</el-row>
<el-row style="display: flex; gap: 30px;" class="card">
<el-row style="display: flex; gap: 30px;margin: 20px 0;" class="card">
<el-col style="flex: 1;" class="item">
<div class="box" style="height: 100%;display: flex;">
<div class="left"
@ -161,7 +162,7 @@
</div>
</el-col>
</el-row>
<el-row>
<el-row style="margin: 20px 0;">
<el-col :span="15">
<el-row style="display: flex;flex-direction: column;height: 100%;">
<div>
@ -177,11 +178,15 @@
<QiXiang />
</el-col>
</el-row>
<el-row>
<div>
<el-row style="margin: 20px 0;"> <!-- gutter列之间的间距单位px控制垂直/水平间距 -->
<!-- 标题列占满12列栅格系统默认12列 -->
<el-col> <!-- span=12 表示占满一行宽度 -->
<TitleComponent title="逆变器运行状态" :fontLevel="2" />
</div>
<status style="width: 100%;" />
</el-col>
<!-- status组件列同样占满12列与标题垂直排列 -->
<el-col>
<status style="width: 100%;" /> <!-- 确保组件宽度100%不溢出 -->
</el-col>
</el-row>
<el-row style="display: flex;">
<el-col :span="6" style="display: flex;flex-direction: column;">

View File

@ -0,0 +1,156 @@
<template>
<el-card style="border-radius: 15px;">
<el-row gutter="35">
<el-col :span="8" class="status-card">
<div class="title">设备状态</div>
<!-- gutter设置为20创建左右间隙 -->
<el-row gutter="20" style="width: 100%;">
<!-- 一行2个每个占12格24/2=12 -->
<el-col :span="12">
<div class="item">
<div class="status">在线</div>
<div class="count" style="color: rgba(0, 184, 122, 1);">{{ data?.sumOnLine || 0 }}</div>
</div>
</el-col>
<el-col :span="12">
<div class="item">
<div class="status">离线</div>
<div class="count" style="color: rgba(102, 102, 102, 1);">{{ data?.sumOffLine || 0 }}</div>
</div>
</el-col>
<el-col :span="12">
<div class="item">
<div class="status">网络延迟</div>
<div class="count" style="color: rgba(255, 208, 35, 1);">8</div>
</div>
</el-col>
<el-col :span="12">
<div class="item">
<div class="status">故障</div>
<div class="count" style="color: rgba(227, 39, 39, 1);">1</div>
</div>
</el-col>
</el-row>
</el-col>
<el-col :span="16" class="alarm-card">
<div class="title">最近报警</div>
<el-row class="list">
<el-col>
<div class="item red">
<div class="sub-title">
异常移动检测
</div>
<div class="content">
A区b栋102 | 今天08:23
</div>
</div>
</el-col>
<el-col>
<div class="item yellow">
<div class="sub-title">
设备连接不稳定
</div>
<div class="content">
A区b栋102 | 今天08:23
</div>
</div>
</el-col>
<el-col>
<div class="item red">
<div class="sub-title">
异常移动检测
</div>
<div class="content">
A区b栋102 | 今天08:23
</div>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
</template>
<style scoped>
.title {
font-family: "Alibaba-PuHuiTi-Bold";
font-size: 20px;
font-weight: 400;
letter-spacing: 0px;
line-height: 24px;
color: rgb(0, 30, 59);
text-align: left;
vertical-align: top;
margin-bottom: 20px;
}
.status-card {
.item {
background: rgba(245, 245, 245, 0.2);
border: 1px solid rgba(230, 233, 238, 1);
margin-bottom: 20px;
border-radius: 15px;
padding: 15px 30px;
text-align: center;
.status {
color: rgba(175, 175, 175, 1);
font-size: 20px;
margin-bottom: 10px;
font-weight: 500;
}
.count {
font-size: 18px;
font-weight: bold;
text-align: center;
}
}
}
.alarm-card {
.item {
border: 1px solid #E6E9EE;
border-radius: 10px;
padding: 10px;
margin-bottom: 10px;
position: relative;
padding-left: 20px;
text-align: left;
}
.red {
::v-deep::before {
position: absolute;
content: '';
background-color: rgba(227, 39, 39, 1);
height: 100%;
width: 7px;
border-radius: 7px 0px 0px 7px;
top: 0;
left: 1px;
}
}
.yellow {
::v-deep::before {
position: absolute;
content: '';
background-color: rgba(255, 208, 35, 1);
height: 100%;
width: 7px;
border-radius: 7px 0px 0px 7px;
top: 0;
left: 1px;
}
}
}
</style>
<script setup>
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
</script>

View File

@ -0,0 +1,320 @@
<template>
<el-card style="border-radius: 15px;">
<div class="title-box">
视频管理
</div>
<el-row class="cunchu">
<el-col>
<div class="subtitle">存储状态</div>
</el-col>
<el-col style="position: relative;">
<div ref="chartRef" style="height: 220px;width: 100%;"></div>
<div class="text">别担心还有很多存储空间</div>
</el-col>
</el-row>
<el-row>
<el-col class="lxcc">
<div class="top">
<div class="subtitle">录像存储设置</div>
<div class="edit">编辑</div>
</div>
<div class="content">
<div class="item">
<div class="title">
存储模式
</div>
<div class="text">
循环覆盖
</div>
</div>
<div class="item">
<div class="title">
保留天数
</div>
<div class="text">
30
</div>
</div>
<div class="item">
<div class="title">
录像质量
</div>
<div class="text">
高清1080P
</div>
</div>
<div class="item">
<div class="title">
备份记录
</div>
<div class="text">
开启
</div>
</div>
<div class="item">
<div class="title">
备份时间
</div>
<div class="text">
每日02:00
</div>
</div>
</div>
</el-col>
</el-row>
<el-row>
<el-col>
<el-row>
<el-col>
<div class="subtitle">历史视频查询</div>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-select placeholder="全部摄像头">
</el-select>
</el-col>
<el-col :span="12">
<el-date-picker v-model="value1" type="daterange" range-separator="至" style="width: 100%;"
start-placeholder="开始" end-placeholder="结束" :size="size" />
</el-col>
<el-col :span="4">
<el-button type="primary">搜索</el-button>
</el-col>
</el-row>
</el-col>
<el-col>
<el-row>
<el-table :data="data" height="200px" max-height="200px">
<el-table-column label="摄像头" prop="name" width="100">
</el-table-column>
<el-table-column label="日期" prop="date" width="120">
</el-table-column>
<el-table-column label="时长" prop="duration">
</el-table-column>
<el-table-column label="大小" prop="size">
</el-table-column>
<el-table-column label="操作" fixed="right" width="100">
<template #default="scope">
<div class="svg-icon">
<img src="/assets/svg/play.svg" alt="">
<img src="/assets/svg/download.svg" alt="">
<img src="/assets/svg/delete.svg" alt=""></img>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination" v-if="activeTab !== 'record'">
<el-pagination layout="prev, pager, next, jumper" :total="totalRecords"
v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]"
@current-change="handlePageChange" @size-change="handleSizeChange"></el-pagination>
</div>
</el-row>
</el-col>
</el-row>
</el-card>
</template>
<style scoped lang="scss">
.title-box {
font-family: "Alibaba-PuHuiTi-Bold";
font-size: 20px;
font-weight: 400;
letter-spacing: 0px;
line-height: 24px;
color: rgba(0, 30, 59, 1);
text-align: left;
vertical-align: top;
}
.subtitle {
font-size: 16px;
color: #001E3B;
position: relative;
margin: 10px 0;
::v-deep::before {
position: absolute;
width: 5px;
height: 5px;
content: '';
border-radius: 50%;
background-color: rgba(24, 109, 245, 1);
top: 50%;
left: -8px;
transform: translateY(-50%);
}
}
.cunchu {
.text {
position: absolute;
bottom: 60px;
width: 100%;
color: rgba(113, 128, 150, 1);
font-size: 14px;
text-align: center;
}
}
.lxcc {
.top {
display: flex;
justify-content: space-between;
align-items: center;
.edit {
color: rgba(0, 30, 59, 1);
font-size: 14px;
cursor: pointer;
}
}
.content {
background-color: #F2F8FC;
border-radius: 10px;
.item {
display: flex;
border-bottom: 1px solid #E3EDFF;
padding: 10px 0;
text-align: center;
}
.title {
width: 50%;
}
.text {
width: 50%;
}
}
}
.svg-icon {
display: flex;
justify-content: space-around;
img {
width: 15px;
height: 15px;
display: block;
cursor: pointer;
}
}
</style>
<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
const chartRef = ref(null);
let myChart = null;
const pageSize = ref(20);
const totalRecords = ref(100);
const initChart = () => {
myChart = echarts.init(chartRef.value);
var option = {
// 调整图表整体位置,向上移动以减少底部空白
// 添加首尾单位标识
graphic: [
// 起始单位 (0TB)
{
type: 'text',
left: '24%',
top: '50%', // 上移文本位置
style: {
text: '0TB',
fontSize: 14,
color: '#666'
}
},
// 结束单位 (10TB)
{
type: 'text',
right: '24%',
top: '50%', // 上移文本位置
style: {
text: '10TB',
fontSize: 14,
color: '#666'
}
},
// 中间显示当前值和百分比(换行显示)
{
type: 'text',
left: 'center',
top: '40%', // 调整到48%位置
style: {
text: '6.68TB\n48%',
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
lineHeight: 20 // 增加行高以确保换行效果
}
}
],
series: [
{
type: 'gauge',
radius: '85%', // 适当减小半径
center: ['50%', '50%'], // 上移图表中心位置
startAngle: 170, // 开始角度(左侧)
endAngle: 10, // 结束角度(右侧)
max: 10, // 最大值设置为10TB
axisLine: {
lineStyle: {
width: 40, // 保持条的粗细
color: [
[0.67, '#4863FF'], // 当前值6.7TB对应67%
[1, '#E5E7EB'] // 未用部分颜色
]
}
},
// 隐藏所有刻度相关元素
axisTick: {
show: false
},
splitLine: {
show: false
},
axisLabel: {
show: false
},
// 隐藏指针
pointer: {
show: false
},
// 隐藏默认详情
detail: {
show: false
},
// 当前值6.7TB
data: [{ value: 6.7 }]
}
]
};
myChart.setOption(option);
};
// 监听窗口大小变化,重新绘制图表
const handleResize = () => {
if (myChart) {
myChart.resize();
}
};
const data = computed(() => {
return Array.from({ length: 10 }, (_, i) => ({
name: `摄像头${i + 1}`,
date: '25.03.12-10:00',
duration: '4小时',
size: '2.4GB'
}));
});
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
// 组件卸载时移除事件监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>

View File

@ -0,0 +1,515 @@
<template>
<el-row style="height: 100%;">
<el-card style="width: 100%;border-radius: 12px;height: 100%;">
<div style="display: flex;width: 100%;justify-content: flex-end;align-items: center;padding-bottom: 15px;">
<TitleComponent title="实时视频监控" subtitle="查看所有已完成的巡检记录,跟进巡检报告" />
<span style="color: rgba(24, 109, 245, 1);display: flex;align-items: center; cursor: pointer;">
<span>
2025/06/30 12:00
刷新数据
</span>
<el-icon>
<Refresh />
</el-icon>
</span>
</div>
<div class="video-container">
<el-row :gutter="20">
<!-- 扩展布局左侧大视频 + 右侧小视频列 -->
<template v-if="isExpanded">
<!-- 左侧大视频 16 -->
<el-col :span="16" class="video-wrapper">
<div class="item large" @click="() => { isExpanded = false; }" ref="bigVideoRef"
:id="`bigVideo`">
<div class="title" v-if="isExpanded"
style="display: flex;justify-content: space-between;">
<!-- <div class="text">
{{ videoList[activeIndex].name }}
</div>
<div class="tools">
<img src="/assets/svg/huanyuan.svg" alt=""></img>
<img src="/assets/svg/quanpin.svg" alt=""></img>
<img src="/assets/svg/jietu.svg" alt="">
</div> -->
</div>
</div>
<!-- 大视频的切换视图按钮 -->
<div class="video-action-btn">
<el-button type="primary" @click.stop="() => {
isExpanded = false;
}">切换视图</el-button>
</div>
</el-col>
<!-- 右侧小视频列 8 -->
<el-col :span="8">
<el-row :gutter="20">
<el-col :span="24" v-for="i in 3" :key="i" class="video-wrapper">
<div class="item small" @click="() => {
// 计算要显示的视频索引 - 按照当前视频后面三个排序
const displayIndex = (activeIndex + i) % videoList.length;
activeIndex = displayIndex;
}" :id="`smallVideo-expanded-${i}`">
<!-- <div class="title">{{ videoList[(activeIndex + i) % videoList.length].name }}
</div> -->
</div>
<!-- 小视频的两个按钮切换视图和查看视频 -->
<div class="video-action-btn expanded">
<el-button type="success" size="small" @click.stop="() => {
const displayIndex = (activeIndex + i) % videoList.length;
activeIndex = displayIndex;
// 不需要改变isExpanded因为已经在扩展布局
}">查看视频</el-button>
</div>
</el-col>
</el-row>
</el-col>
</template>
<!-- 普通布局所有视频均匀排列 -->
<template v-else>
<el-col :span="8" v-for="(item, index) in videoList" :key="index" class="video-wrapper">
<!-- 视频容器 -->
<div class="item" :id="`smallVideo-${index + 1}`" :ref="el => smallVideoRefs[index] = el">
<!-- <div class="title">{{ item.name }}</div> -->
</div>
<!-- 按钮放在最外层与视频容器同级 -->
<div class="video-action-btn">
<el-button type="primary" @click.stop="() => {
activeIndex = index;
isExpanded = true;
}">切换视图</el-button>
</div>
</el-col>
</template>
</el-row>
<el-row v-if="!isExpanded">
<div class="pagination" v-if="activeTab !== 'record'">
<el-pagination layout="prev, pager, next, jumper" :total="totalRecords"
v-model:current-page="pageStart" v-model:page-size="pageSize" :page-sizes="[20, 50, 100]"
@current-change="handlePageChange" @size-change="handleSizeChange"></el-pagination>
</div>
</el-row>
</div>
</el-card>
</el-row>
</template>
<script setup lang="ts">
import { Refresh } from '@element-plus/icons-vue';
import EZUIKit from 'ezuikit-js';
// import TitleComponent from '@/components/TitleComponent';
import { getToken, getMonitoringList } from '@/api/securitySurveillance/index.js';
import { ref, onMounted, watch, nextTick, onUnmounted } from 'vue';
import { useUserStore } from '@/store/modules/user';
const activeIndex = ref(0); // 初始选中第一个视频
const isExpanded = ref(true); // 初始为扩展
const accessToken = ref('')
const pageStart = ref(1);
const pageSize = ref(4); // 默认请求4个视频扩展布局
const totalRecords = ref(0);
const activeTab = ref('live');
const bigVideoRef = ref<HTMLDivElement>(null);
const smallVideoRefs = ref<Array<HTMLDivElement | null>>([]); // 使用数组存储多个视频容器引用
const currentProject = computed(() => useUserStore().selectedProject);
const videoList = ref([]);
// 存储第二页的数据,用于处理扩展视图右边视频不足的情况
const nextPageVideoList = ref([]);
// 标记是否已经使用了下一页的数据
const hasUsedNextPageData = ref(false);
const StructureEZUIKitPlayer = (item: any, index: number, isBig = false) => {
// 添加输入参数的安全检查
if (!item || typeof item !== 'object') {
console.error('无效的视频项数据:', item);
return;
}
const containerId = isBig ? 'bigVideo' : isExpanded.value ? `smallVideo-expanded-${index + 1}` : `smallVideo-${index + 1}`;
const container = document.getElementById(containerId);
// 先销毁旧的播放器实例(如果存在)
if (item.player) {
try {
// 添加安全检查确保destroy方法存在
if (typeof item.player.destroy === 'function') {
// 尝试移除所有事件监听器
if (item.player.off) {
item.player.off('*');
}
item.player.destroy();
}
}
catch (error) {
console.error('销毁播放器失败:', error);
}
item.player = null;
}
if (container && accessToken.value && item.deviceSerial) {
try {
item.player = new EZUIKit.EZUIKitPlayer({
audio: '0',
id: containerId,
accessToken: accessToken.value,
url: `ezopen://open.ys7.com/${item.deviceSerial}/1.hd.live`,
template: "pcLive",
width: container.clientWidth,
height: container.clientHeight,
plugin: ['talk']
});
} catch (error) {
console.error('创建播放器失败:', error);
item.player = null;
}
} else {
console.error(`创建播放器失败,缺少必要条件: container=${!!container}, accessToken=${!!accessToken.value}, deviceSerial=${!!item.deviceSerial}`);
}
};
// 获取萤石云Token
const getTokenData = async () => {
const { data } = await getToken()
accessToken.value = data
}
// 获取摄像头列表
const getMonitoringListData = async () => {
// 根据当前视图类型设置请求数量
const currentPageSize = isExpanded.value ? 4 : 9;
const { data: { object, sum }, } = await getMonitoringList({
pageStart: pageStart.value,
pageSize: currentPageSize,
isflow: true,
projectId: currentProject.value?.id,
})
totalRecords.value = Number(sum)
// 确保object是数组如果不是则使用空数组
videoList.value = Array.isArray(object) ? object : []
}
// 获取下一页视频数据
const getNextPageData = async () => {
const { data: { object, sum } } = await getMonitoringList({
pageStart: pageStart.value + 1,
isflow: true,
pageSize: 3 // 只需要3个视频
})
// 确保object是数组如果不是则使用空数组
nextPageVideoList.value = Array.isArray(object) ? object : [];
// 标记已经使用了下一页的数据
hasUsedNextPageData.value = true;
}
const getData = async () => {
// 先清理所有播放器
cleanupPlayers();
// 不再每次都获取tokentoken只在组件挂载时获取一次
await getMonitoringListData()
// 不再重置activeIndex保留用户之前选择的视频索引
// 等待DOM更新后初始化视频
await nextTick();
initVideo()
}
const initVideo = async () => {
// 先清理所有视频容器的内容,避免残留
if (bigVideoRef.value) {
bigVideoRef.value.innerHTML = '';
}
// 清理所有小视频容器的内容
smallVideoRefs.value.forEach(ref => {
if (ref) {
ref.innerHTML = '';
}
});
// 确保videoList是数组如果不是则使用空数组
const safeVideoList = Array.isArray(videoList.value) ? videoList.value : [];
// 确保nextPageVideoList是数组如果不是则使用空数组
const safeNextPageVideoList = Array.isArray(nextPageVideoList.value) ? nextPageVideoList.value : [];
if (isExpanded.value) {
// 扩展布局初始化大视频和右侧3个小视频
// 安全检查确保activeIndex在有效范围内
const safeActiveIndex = Math.min(Math.max(activeIndex.value, 0), safeVideoList.length - 1);
if (safeVideoList.length > 0 && safeVideoList[safeActiveIndex] && typeof safeVideoList[safeActiveIndex] === 'object') {
StructureEZUIKitPlayer(safeVideoList[safeActiveIndex], 0, true);
}
// 检查当前视频后面是否有足够的视频
const remainingVideos = safeVideoList.length - safeActiveIndex - 1;
if (remainingVideos >= 3) {
// 当前页后面有足够的视频,直接使用当前页的数据
for (let i = 0; i < 3; i++) {
const videoIndex = safeActiveIndex + 1 + i;
if (safeVideoList[videoIndex] && typeof safeVideoList[videoIndex] === 'object') {
// 修复索引使用i而不是i+1确保正确对应smallVideo-expanded-1, 2, 3
StructureEZUIKitPlayer(safeVideoList[videoIndex], i);
}
}
} else {
// 当前页后面视频不足3个需要获取下一页的数据
await getNextPageData();
// 重新获取安全的视频列表
const updatedSafeNextPageVideoList = Array.isArray(nextPageVideoList.value) ? nextPageVideoList.value : [];
// 使用当前页后面的视频和下一页的前几个视频
let displayCount = 0;
// 先显示当前页后面的视频
for (let i = safeActiveIndex + 1; i < safeVideoList.length && displayCount < 3; i++) {
if (safeVideoList[i] && typeof safeVideoList[i] === 'object') {
// 修复索引使用displayCount而不是displayCount+1
StructureEZUIKitPlayer(safeVideoList[i], displayCount);
displayCount++;
}
}
// 再显示下一页的视频补充到3个
for (let i = 0; i < updatedSafeNextPageVideoList.length && displayCount < 3; i++) {
if (updatedSafeNextPageVideoList[i] && typeof updatedSafeNextPageVideoList[i] === 'object') {
// 修复索引使用displayCount而不是displayCount+1
StructureEZUIKitPlayer(updatedSafeNextPageVideoList[i], displayCount);
displayCount++;
}
}
}
} else {
// 普通布局:如果之前在扩展视图中使用了下一页数据,现在需要更新页码
if (hasUsedNextPageData.value) {
pageStart.value += 1;
// 重新获取第二页的9个视频数据
await getMonitoringListData();
hasUsedNextPageData.value = false;
}
// 初始化所有视频,添加更严格的类型检查
safeVideoList.forEach((item, index) => {
if (item && typeof item === 'object') {
StructureEZUIKitPlayer(item, index);
} else {
console.warn(`跳过无效的视频项: ${index}`);
}
});
}
}
const handlePageChange = (page: number) => {
pageStart.value = page;
// 这里可以添加分页逻辑
getData()
}
const handleSizeChange = (size: number) => {
// 根据当前视图类型设置合适的页面大小
// 扩展视图固定为4普通视图固定为9
pageSize.value = isExpanded.value ? 4 : 9;
pageStart.value = 1;
getData()
}
// 清理所有播放器实例
const cleanupPlayers = () => {
// 清理当前页视频的播放器
videoList.value.forEach(item => {
if (item && item.player && typeof item.player.destroy === 'function') {
try {
// 尝试移除所有事件监听器
if (item.player.off) {
item.player.off('*');
}
// 销毁播放器实例
item.player.destroy();
}
catch (error) {
console.error('销毁播放器失败:', error);
}
// 确保播放器引用被清除
item.player = null;
}
});
// 清理下一页视频的播放器
nextPageVideoList.value.forEach(item => {
if (item && item.player && typeof item.player.destroy === 'function') {
try {
// 尝试移除所有事件监听器
if (item.player.off) {
item.player.off('*');
}
// 销毁播放器实例
item.player.destroy();
}
catch (error) {
console.error('销毁下一页视频播放器失败:', error);
}
// 确保播放器引用被清除
item.player = null;
}
});
}
// 监听isExpanded变化根据不同情况处理数据请求
watch(isExpanded, async (newValue, oldValue) => {
// 保存当前activeIndex的值
const currentActiveIndex = activeIndex.value;
// 从扩展视图切换到普通视图需要重新请求9个视频
if (newValue === false && oldValue === true) {
// 同步更新页面大小为9
pageSize.value = 9;
// 清理所有播放器实例
cleanupPlayers();
// 等待DOM更新
await nextTick();
// 重新请求9个视频数据
await getMonitoringListData();
// 再次等待DOM更新
await nextTick();
// 初始化视频
initVideo();
// 恢复保存的activeIndex值
activeIndex.value = currentActiveIndex;
}
// 从普通视图切换到扩展视图,不需要重新请求数据
else if (newValue === true && oldValue === false) {
// 同步更新页面大小为4
pageSize.value = 4;
// 清理所有播放器实例
cleanupPlayers();
// 等待DOM更新完成后再初始化视频
await nextTick();
// 再次等待确保DOM完全渲染
await new Promise(resolve => setTimeout(resolve, 100));
// 初始化视频
initVideo();
}
});
watch(activeIndex, (newIndex) => {
if (isExpanded.value) {
// 当activeIndex变化时重新初始化扩展布局中的视频
initVideo();
}
});
onMounted(() => {
// 组件挂载时获取一次token
getTokenData().then(() => {
// token获取成功后获取视频数据
getData();
}).catch(error => {
console.error('获取token失败:', error);
});
});
// 组件卸载时销毁所有播放器
onUnmounted(() => {
cleanupPlayers();
});
</script>
<style scoped lang="scss">
.el-col {
margin-bottom: 20px;
}
// 视频包装器样式
.video-wrapper {
position: relative;
// 确保按钮在视频容器上方
}
.video-container {
.item {
height: 220px;
margin-bottom: 20px;
position: relative;
border: 2px solid rgba(45, 119, 249, 1);
cursor: pointer;
// 确保子元素正确定位
overflow: hidden;
img {
width: 100%;
height: 100%;
display: block;
}
.title {
position: absolute;
padding: 5px 0;
padding-left: 10px;
font-size: 14px;
border-radius: 8px 8px 0px 0px;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.73) 0%, rgba(0, 0, 0, 0.33) 100%);
width: 100%;
bottom: 0;
color: #fff;
z-index: 15;
}
}
// 视频动作按钮样式(放在最外层)
.video-action-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 20;
pointer-events: none; // 防止按钮阻止鼠标悬停事件
}
// 右侧小视频的按钮容器样式
.video-action-btn.expanded {
display: flex;
gap: 10px;
}
// 鼠标悬停在视频包装器上时显示按钮
.video-wrapper:hover .video-action-btn {
opacity: 1;
}
// 鼠标悬停在视频上时添加半透明背景效果
.video-wrapper:hover .item::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
z-index: 5;
}
// 按钮本身需要有点击事件
.video-action-btn button {
pointer-events: auto;
}
// 大视频样式(高度与右侧三个小视频总高度对齐,考虑间距)
.large {
height: calc(220px * 3 + 20px * 2 + 40px); // 高度为3个小视频高度加上2个间距
.tools {
display: flex;
img {
width: 20px;
height: 20px;
display: block;
margin: 0 10px;
}
}
}
// 小视频样式(保持原高度,适配右侧单列)
.small {
height: 235px;
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,65 @@
<template>
<div class="security-surveillance">
<el-row style="display: flex;align-items: center;">
<el-col :span="12">
<TitleComponent title="安防监控管理" subtitle="实时监控、历史录像查询与视频管理" />
</el-col>
<!-- 关键给内层 el-col 套上 el-row -->
<el-col :span="12" style="text-align: right;">
<el-row :gutter="16" justify="end"> <!-- gutter 可选单位px控制列间距 -->
<el-col :span="6" :push="3">
<el-input placeholder="搜索逆变器..." prefix-icon="search" />
</el-col>
<el-col :span="6" :push="3">
<el-select placeholder="请选择逆变器状态">
<el-option label="所有状态" value="0"></el-option>
</el-select>
</el-col>
<el-col :span="6">
<el-button type="primary">刷新数据<el-icon>
<Refresh />
</el-icon>
</el-button>
</el-col>
</el-row> <!-- 闭合内层 el-row -->
</el-col>
</el-row>
<el-row style="margin-top: 20px;">
<Top :data="data" />
</el-row>
<el-row style="margin-top: 20px;" :gutter="25">
<el-col :span="18">
<Spjk />
</el-col>
<el-col :span="6">
<Spgl />
</el-col>
</el-row>
<el-row style="margin-top: 20px;">
<el-col>
<Sbzt :data="data" />
</el-col>
</el-row>
</div>
</template>
<style scoped>
.security-surveillance {
padding: 20px;
background-color: #F2F8FC;
}
</style>
<script setup>
import TitleComponent from "@/components/TitleComponent";
import Top from "./components/top"
import Spjk from "./components/spjk"
import Spgl from "./components/spgl";
import Sbzt from "./components/sbzt";
import { getHomeScreenData } from "@/api/securitySurveillance";
const data = ref(null)
onMounted(() => {
getHomeScreenData().then(res => {
data.value = res.data
})
})
</script>

View File

@ -0,0 +1,253 @@
<template>
<div class="detaildata-container">
<div class="title-container">
<div class="title-left">
<TitleComponent title="发电量同比分析" :font-level="2" />
</div>
<div class="title-right">
<el-input
placeholder="请输入搜索内容"
style="width: 200px;"
prefix-icon="Search"
/>
</div>
</div>
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
stripe
border
>
<el-table-column prop="datetime" label="日期" align="center" />
<el-table-column prop="prbs" label="发电量(Kwh)" align="center">
<template #default="scope">
<span>{{ scope.row.prbs }}</span>
</template>
</el-table-column>
<el-table-column prop="prz" label="同比(%)" align="center">
<template #default="scope">
<span :class="{
'text-red': scope.row.prz < 0,
'text-green': scope.row.prz > 0
}">
{{ scope.row.prz > 0 ? '+' : '' }}{{ scope.row.prz }}
</span>
</template>
</el-table-column>
<el-table-column prop="prz2" label="环比(%)" align="center">
<template #default="scope">
<span :class="{
'text-red': scope.row.prz2 < 0,
'text-green': scope.row.prz2 > 0
}">
{{ scope.row.prz2 > 0 ? '+' : '' }}{{ scope.row.prz2 }}
</span>
</template>
</el-table-column>
<el-table-column prop="status" label="设备状态" align="center">
<template #default="scope">
<el-tag
:type="scope.row.status === '正常' ? 'success' : 'warning'"
size="small"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button
type="text"
size="small"
@click="handleDetail(scope.row)"
class="text-blue"
>
详情
</el-button>
<el-button
type="text"
size="small"
@click="handleExport(scope.row)"
class="text-blue"
>
导出
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
// 定义表格数据类型
interface TableRow {
datetime: string
prbs: string | number
prz: number
prz2: number
status: string
}
// 响应式数据
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(293)
const tableData = ref<TableRow[]>([])
// 模拟数据生成函数
const generateMockData = (page: number, size: number): TableRow[] => {
const data: TableRow[] = []
const startIndex = (page - 1) * size
// 生成不同的日期
const baseDate = new Date(2023, 5, 30)
for (let i = 0; i < size; i++) {
const index = startIndex + i
if (index >= total.value) break
// 生成不同的日期
const currentDate = new Date(baseDate)
currentDate.setDate(baseDate.getDate() - index)
const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`
// 随机生成正负数,展示不同颜色效果
const randomValue1 = (Math.random() - 0.5) * 10
const randomValue2 = (Math.random() - 0.5) * 10
data.push({
datetime: dateStr,
prbs: (Math.random() * 100 + 150).toFixed(1), // 150-250之间的随机数
prz: Number(randomValue1.toFixed(1)),
prz2: Number(randomValue2.toFixed(1)),
status: i % 8 === 2 ? '预警' : '正常'
})
}
return data
}
// 处理分页大小变化
const handleSizeChange = (size: number) => {
pageSize.value = size
loadData()
}
// 处理当前页码变化
const handleCurrentChange = (current: number) => {
currentPage.value = current
loadData()
}
// 处理详情按钮点击
const handleDetail = (row: TableRow) => {
ElMessage.info('查看详情: ' + row.datetime)
// 实际项目中这里应该跳转到详情页或显示详情对话框
}
// 处理导出按钮点击
const handleExport = (row: TableRow) => {
ElMessage.info('导出数据: ' + row.datetime)
// 实际项目中这里应该调用导出API
}
// 加载数据
const loadData = () => {
loading.value = true
// 模拟API请求延迟
setTimeout(() => {
tableData.value = generateMockData(currentPage.value, pageSize.value)
loading.value = false
}, 500)
}
// 组件挂载时加载数据
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.detaildata-container {
padding: 16px;
background: #fff;
}
.title-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 16px;
}
.title-left {
display: flex;
align-items: center;
}
.title-right {
display: flex;
align-items: center;
}
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
align-items: center;
}
.text-red {
color: #f56c6c;
}
.text-green {
color: #67c23a;
}
.text-blue {
color: #1890ff;
}
.el-button--text {
padding: 0;
height: auto;
font-size: 14px;
}
// 响应式布局
@media screen and (max-width: 1200px) {
.detaildata-container {
padding: 12px;
}
.el-table {
font-size: 13px;
}
.el-table-column {
&:not(:first-child):not(:last-child) {
width: 90px !important;
}
}
}
</style>

View File

@ -0,0 +1,183 @@
<template>
<div class="duibifenxi-bar-container">
<div class="title">
<TitleComponent title="发电量同比分析" :font-level="2" />
<el-select placeholder="请选择线路" style="width: 150px;">
<el-option label="A线路" value="all"></el-option>
</el-select>
</div>
<div ref="chartRef" class="chart-container"></div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
// 定义组件props
interface CompareData {
dates: string[]
currentPeriodData: number[]
lastYearData: number[]
}
const props = defineProps<{
compareData?: CompareData
}>()
const chartRef = ref<HTMLElement>()
let chartInstance: echarts.ECharts | null = null
// 默认数据
const defaultCompareData: CompareData = {
dates: ['1号', '2号', '3号', '4号', '5号', '6号', '7号'],
currentPeriodData: [90, 80, 75, 89, 60, 76, 73],
lastYearData: [60, 53, 65, 76, 69, 52, 65]
}
// 初始化图表
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const data = props.compareData || defaultCompareData
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function (params: any) {
const current = params[0]
const lastYear = params[1]
let result = `${current.name}<br/>`
result += `${current.marker}${current.seriesName}: ${current.value}Kwh<br/>`
result += `${lastYear.marker}${lastYear.seriesName}: ${lastYear.value}Kwh`
return result
}
},
legend: {
data: ['当前周期', '去年同期'],
textStyle: {
color: '#333'
},
top: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.dates,
axisLine: {
lineStyle: {
color: '#d9d9d9'
}
},
axisLabel: {
color: '#666'
}
},
yAxis: {
type: 'value',
axisLine: {
show: false
},
axisLabel: {
color: '#666',
formatter: '{value} Kwh',
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
},
series: [
{
name: '当前周期',
type: 'bar',
data: data.currentPeriodData,
itemStyle: {
color: '#1890ff'
},
barWidth: '30%',
emphasis: {
focus: 'series'
}
},
{
name: '去年同期',
type: 'bar',
data: data.lastYearData,
itemStyle: {
color: '#52c41a'
},
barWidth: '30%',
emphasis: {
focus: 'series'
}
}
]
}
chartInstance.setOption(option)
}
// 响应式处理
const handleResize = () => {
chartInstance?.resize()
}
// 组件挂载
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
// 组件卸载
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
chartInstance?.dispose()
})
</script>
<style scoped lang="scss">
.duibifenxi-bar-container {
padding: 10px;
background: #fff;
height: 100%;
display: flex;
flex-direction: column;
min-height: 300px;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
width: 100%;
}
.chart-container {
width: 100%;
height: 100%;
min-height: 280px;
}
// 响应式调整
@media screen and (max-width: 768px) {
.duibifenxi-bar-container {
padding: 5px;
min-height: 250px;
}
.chart-container {
min-height: 230px;
}
}
</style>

View File

@ -0,0 +1,187 @@
<template>
<div class="tongbifenxi-line-container">
<div class="title">
<TitleComponent title="发电量同比分析" :font-level="2" />
<el-select placeholder="请选择线路" style="width: 150px;">
<el-option label="A线路" value="all"></el-option>
</el-select>
</div>
<div ref="chartDomRef" class="chart-container"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import * as echarts from 'echarts';
import TitleComponent from '@/components/TitleComponent/index.vue';
const chartDomRef = ref<HTMLElement | null>(null);
const chartInstance = ref<echarts.ECharts | null>(null);
const initChart = () => {
if (!chartDomRef.value) return;
chartInstance.value = echarts.init(chartDomRef.value);
// 写死的数据
const dates = ['1号', '2号', '3号', '4号', '5号', '6号', '7号'];
const growthRates = [1.50, 1.20, 0.50, 0.80, 0.90, 0.30, -2.00];
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
backgroundColor: '#67c23a',
borderWidth: 0,
textStyle: {
color: '#fff',
fontSize: 14
},
formatter: (params: any) => {
return `${params.name}\n环比增长率${params.value}%`;
},
padding: [10, 15]
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: dates,
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#d9d9d9'
}
},
axisLabel: {
color: '#666'
}
}
],
yAxis: [
{
type: 'value',
min: -2,
max: 2,
axisLabel: {
color: '#666',
formatter: '{value}%'
},
axisLine: {
show: true,
lineStyle: {
color: '#d9d9d9'
}
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed'
}
}
}
],
series: [
{
name: '环比增长率',
type: 'line',
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(103, 194, 58, 0.3)'
},
{
offset: 1,
color: 'rgba(103, 194, 58, 0.05)'
}
])
},
lineStyle: {
color: '#67c23a',
width: 3
},
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#67c23a',
borderColor: '#fff',
borderWidth: 2
},
emphasis: {
focus: 'series',
itemStyle: {
color: '#67c23a',
borderColor: '#fff',
borderWidth: 3,
shadowBlur: 10,
shadowColor: 'rgba(103, 194, 58, 0.5)'
},
},
data: growthRates,
smooth: true
}
]
};
chartInstance.value.setOption(option);
};
const handleResize = () => {
chartInstance.value?.resize();
};
onMounted(() => {
initChart();
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
chartInstance.value?.dispose();
});
</script>
<style scoped>
.tongbifenxi-line-container {
width: 100%;
height: 100%;
min-height: 300px;
padding: 10px;
box-sizing: border-box;
background: #fff;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
width: 100%;
}
.chart-container {
width: 100%;
height: 100%;
min-height: 280px;
}
@media (max-width: 768px) {
.tongbifenxi-line-container {
padding: 5px;
min-height: 250px;
}
.chart-container {
min-height: 230px;
}
}
</style>

View File

@ -0,0 +1,200 @@
<template>
<div class="zonglan-container">
<!-- 循环生成统计卡片 -->
<div v-for="card in statCards" :key="card.id" class="stat-card">
<div class="card-header">
<span class="card-title">{{ card.title }}</span>
<el-tooltip content="查看详情" placement="top">
<el-icon>
<Warning />
</el-icon>
</el-tooltip>
</div>
<div class="card-content">
<div class="stat-value">{{ card.value }}</div>
<div class="stat-footer">
<span class="trend-indicator up">
<img src="/src/assets/demo/up.png" alt="up" class="trend-icon"> {{ card.trendChange }}
</span>
<el-select v-model="card.selectedTimeRange" placeholder="选择时间范围" style="width: 120px; font-size: 12px;">
<el-option label="Today" value="today"></el-option>
<el-option label="This Week" value="week"></el-option>
<el-option label="This Month" value="month"></el-option>
<el-option label="This Year" value="year"></el-option>
</el-select>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 统计卡片数据
interface StatCard {
id: string;
title: string;
value: string;
trendChange: string;
selectedTimeRange: string;
}
const statCards = ref<StatCard[]>([
{
id: 'power-total',
title: '总发电量',
value: '2,456.8',
trendChange: '4.2%',
selectedTimeRange: 'today'
},
{
id: 'year-on-year',
title: '同比增长率',
value: '3.8%',
trendChange: '0.5%',
selectedTimeRange: 'today'
},
{
id: 'month-on-month',
title: '环比增长率',
value: '2.1%',
trendChange: '0.3%',
selectedTimeRange: 'today'
},
{
id: 'efficiency',
title: '运行效率',
value: '98.6%',
trendChange: '1.2%',
selectedTimeRange: 'today'
}
]);
</script>
<style scoped>
.zonglan-container {
display: flex;
gap: 20px;
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.stat-card {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.card-title {
font-size: 14px;
color: #666;
font-weight: 500;
}
.info-icon {
color: #c0c4cc;
cursor: pointer;
font-size: 16px;
transition: color 0.3s;
}
.info-icon:hover {
color: #409eff;
}
.card-content {
position: relative;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
line-height: 1.2;
padding-bottom: 10px;
border-bottom: 1px solid #ebeef5;
}
.stat-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.trend-indicator {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 500;
}
.trend-indicator.up {
color: #67c23a;
}
.trend-indicator.down {
color: #f56c6c;
}
.trend-indicator i {
margin-right: 4px;
font-size: 12px;
}
.trend-icon {
margin-right: 4px;
vertical-align: middle;
}
.time-range {
font-size: 12px;
color: #909399;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.zonglan-container {
gap: 15px;
}
.stat-card {
padding: 15px;
}
.stat-value {
font-size: 24px;
}
}
@media (max-width: 768px) {
.zonglan-container {
flex-direction: column;
gap: 12px;
}
.stat-card {
padding: 15px;
}
.stat-value {
font-size: 22px;
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="power-fenxi-container">
<!-- 标题栏 -->
<el-row :gutter="24">
<el-col :span="12">
<TitleComponent title="电量分析" subtitle="测量在电解过程中消耗的电荷量" />
</el-col>
<!-- 外层col控制整体宽度并右对齐同时作为flex容器 -->
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
<el-col :span="4">
<el-button type="primary">
导出数据
<el-icon class="el-icon--right">
<UploadFilled />
</el-icon>
</el-button>
</el-col>
</el-col>
</el-row>
<!-- 第一排总览组件 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="24">
<zonglan></zonglan>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="18">
<TitleComponent title="发电量对比分析" :font-level="2" />
</el-col>
<el-col :span="3">
<el-select placeholder="请选择时间" style="width: 100%;">
<el-option label="今天" value="all"></el-option>
</el-select>
</el-col>
<el-col :span="3">
<el-date-picker v-model="value1" type="daterange" range-separator="至" start-placeholder="开始"
end-placeholder="结束" style="width: 100%;" />
</el-col>
</el-row>
<el-row :gutter="20" class="mb-4">
<el-col :span="12">
<el-card>
<DuibifenxiBar></DuibifenxiBar>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<tongbifenxiLine></tongbifenxiLine>
</el-card>
</el-col>
</el-row>
<!-- 第三排详细数据组件 -->
<el-row :gutter="20">
<el-col :span="24">
<el-card>
<detaildata></detaildata>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import TitleComponent from '@/components/TitleComponent/index.vue';
import detaildata from '@/views/shengchanManage/powerfenxi/components/detaildata.vue'
import tongbifenxiLine from '@/views/shengchanManage/powerfenxi/components/tongbifenxiLine.vue';
import DuibifenxiBar from '@/views/shengchanManage/powerfenxi/components/duibifenxiBar.vue';
import zonglan from '@/views/shengchanManage/powerfenxi/components/zonglan.vue';
</script>
<style scoped>
.power-fenxi-container {
padding: 20px;
background-color: rgba(242, 248, 252, 1);
}
.mb-4 {
margin-bottom: 20px;
}
</style>

View File

@ -15,92 +15,113 @@
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="创建时间" style="width: 308px">
<el-date-picker
v-model="dateRange"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button type="primary" icon="Search" @click="handleQuery" v-hasPermi="['system:role:query']">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="hover">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:delete']" type="danger" plain :disabled="ids.length === 0" @click="handleDelete()">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
</el-row>
</template>
<el-table ref="roleTableRef" border v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="false" label="角色编号" prop="roleId" width="120" />
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="200" />
<el-table-column label="显示顺序" prop="roleSort" width="100" />
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" style="">
<el-card shadow="hover">
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
<el-tree
ref="deptTreeRef"
class="mt-2"
node-key="id"
:data="deptOptions"
:props="{ label: 'label', children: 'children' }"
:expand-on-click-node="false"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</el-col>
<el-col :lg="20" :xs="24">
<el-card shadow="hover">
<template #header>
<el-row :gutter="10">
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()"
>修改</el-button
>
</el-col>
<el-col :span="1.5">
<el-button v-hasPermi="['system:role:delete']" type="danger" plain :disabled="ids.length === 0" @click="handleDelete()"
>删除</el-button
>
</el-col>
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
</el-row>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table ref="roleTableRef" v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="false" label="角色编号" prop="roleId" width="120" />
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="200" />
<el-table-column label="显示顺序" prop="roleSort" width="100" />
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime">
<template #default="scope">
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template #default="scope">
<el-tooltip v-if="scope.row.roleId !== 1" content="修改" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="删除" placement="top">
<el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" @click="handleAuthUser(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-table-column fixed="right" label="操作" width="180">
<template #default="scope">
<el-tooltip v-if="scope.row.roleId !== 1" content="修改" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="删除" placement="top">
<el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)"></el-button>
</el-tooltip>
<el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top">
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" @click="handleAuthUser(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
</el-col>
</el-row>
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">
<el-form ref="roleFormRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="所属部门" prop="deptId">
<el-cascader
:options="deptOptions"
v-model="form.deptId"
placeholder="请选择所属部门"
clearable
filterable
:show-all-levels="false"
:props="{ value: 'id', emitPath: false, checkStrictly: true }"
>
</el-cascader>
</el-form-item>
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
</el-form-item>
@ -124,7 +145,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限">
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand(Boolean($event), 'menu')">展开/折叠</el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
<el-tree
@ -135,9 +156,12 @@
node-key="id"
:check-strictly="!form.menuCheckStrictly"
empty-text="加载中请稍候"
:props="{ label: 'label', children: 'children' } as any"
:props="{ label: 'label', children: 'children' }"
></el-tree>
</el-form-item>
<el-form-item label="是否为特殊角色">
<el-switch v-model="form.isSpecial" active-value="1" inactive-value="0" active-text="是" inactive-text="否"> </el-switch>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</el-form-item>
@ -165,7 +189,7 @@
</el-select>
</el-form-item>
<el-form-item v-show="form.dataScope === '2'" label="数据权限">
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">展开/折叠</el-checkbox>
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand(Boolean($event), 'dept')">展开/折叠</el-checkbox>
<el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
<el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
<el-tree
@ -177,7 +201,7 @@
node-key="id"
:check-strictly="!form.deptCheckStrictly"
empty-text="加载中请稍候"
:props="{ label: 'label', children: 'children' } as any"
:props="{ label: 'label', children: 'children' }"
></el-tree>
</el-form-item>
</el-form>
@ -196,6 +220,8 @@ import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updat
import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
import { RoleVO, RoleForm, RoleQuery, DeptTreeOption } from '@/api/system/role/types';
import { MenuTreeOption, RoleMenuTree } from '@/api/system/menu/types';
import api from '@/api/system/user';
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
const router = useRouter();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@ -214,8 +240,11 @@ const menuExpand = ref(false);
const menuNodeAll = ref(false);
const deptExpand = ref(true);
const deptNodeAll = ref(false);
const deptOptions = ref<DeptTreeOption[]>([]);
const deptOptions = ref<DeptTreeVO[]>([]);
const enabledDeptOptions = ref<DeptTreeVO[]>([]);
const openDataScope = ref(false);
const deptName = ref('');
/** 数据范围选项*/
const dataScopeOptions = ref([
@ -232,8 +261,9 @@ const roleFormRef = ref<ElFormInstance>();
const dataScopeRef = ref<ElFormInstance>();
const menuRef = ref<ElTreeInstance>();
const deptRef = ref<ElTreeInstance>();
const deptTreeRef = ref<ElTreeInstance>();
const initForm: RoleForm = {
const initForm = {
roleId: undefined,
roleSort: 1,
status: '0',
@ -244,17 +274,22 @@ const initForm: RoleForm = {
remark: '',
dataScope: '1',
menuIds: [],
deptIds: []
deptId: '',
isSpecial: null,
deptIds: [],
roleSource: '1'
};
const data = reactive<PageData<RoleForm, RoleQuery>>({
const data = reactive({
form: { ...initForm },
queryParams: {
pageNum: 1,
pageSize: 10,
roleName: '',
roleKey: '',
status: ''
deptId: '',
status: '',
roleSource: '1'
},
rules: {
roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
@ -269,6 +304,18 @@ const dialog = reactive<DialogOption>({
title: ''
});
/** 通过条件过滤节点 */
const filterNode = (value: string, data: any) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/** 节点单击事件 */
const handleNodeClick = (data: DeptVO) => {
queryParams.value.deptId = data.id as string;
handleQuery();
};
/**
* 查询角色列表
*/
@ -293,6 +340,9 @@ const handleQuery = () => {
const resetQuery = () => {
dateRange.value = ['', ''];
queryFormRef.value?.resetFields();
queryParams.value.pageNum = 1;
queryParams.value.deptId = undefined;
deptTreeRef.value?.setCurrentKey(undefined);
handleQuery();
};
/**删除按钮操作 */
@ -323,7 +373,7 @@ const handleSelectionChange = (selection: RoleVO[]) => {
/** 角色状态修改 */
const handleStatusChange = async (row: RoleVO) => {
const text = row.status === '0' ? '启用' : '停用';
let text = row.status === '0' ? '启用' : '停用';
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?');
await changeRoleStatus(row.roleId, row.status);
@ -340,17 +390,17 @@ const handleAuthUser = (row: RoleVO) => {
/** 查询菜单树结构 */
const getMenuTreeselect = async () => {
const res = await menuTreeselect();
const res = await menuTreeselect({ menuSource: '1' });
menuOptions.value = res.data;
};
/** 所有部门节点数据 */
const getDeptAllCheckedKeys = (): any => {
// 目前被选中的部门节点
const checkedKeys = deptRef.value?.getCheckedKeys();
let checkedKeys = deptRef.value?.getCheckedKeys();
// 半选中的部门节点
const halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
let halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
if (halfCheckedKeys) {
checkedKeys?.unshift(...halfCheckedKeys);
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
}
return checkedKeys;
};
@ -390,28 +440,28 @@ const handleUpdate = async (row?: RoleVO) => {
};
/** 根据角色ID查询菜单树结构 */
const getRoleMenuTreeselect = (roleId: string | number) => {
return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
return roleMenuTreeselect(roleId, { menuSource: '1' }).then((res): RoleMenuTree => {
menuOptions.value = res.data.menus;
return res.data;
});
};
/** 根据角色ID查询部门树结构 */
const getRoleDeptTreeSelect = async (roleId: string | number) => {
const res = await deptTreeSelect(roleId);
const res = await deptTreeSelect(roleId, { roleSource: '1' });
deptOptions.value = res.data.depts;
return res.data;
};
/** 树权限(展开/折叠)*/
const handleCheckedTreeExpand = (value: boolean, type: string) => {
if (type == 'menu') {
const treeList = menuOptions.value;
let treeList = menuOptions.value;
for (let i = 0; i < treeList.length; i++) {
if (menuRef.value) {
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
}
}
} else if (type == 'dept') {
const treeList = deptOptions.value;
let treeList = deptOptions.value;
for (let i = 0; i < treeList.length; i++) {
if (deptRef.value) {
deptRef.value.store.nodesMap[treeList[i].id].expanded = value;
@ -438,11 +488,11 @@ const handleCheckedTreeConnect = (value: any, type: string) => {
/** 所有菜单节点数据 */
const getMenuAllCheckedKeys = (): any => {
// 目前被选中的菜单节点
const checkedKeys = menuRef.value?.getCheckedKeys();
let checkedKeys = menuRef.value?.getCheckedKeys();
// 半选中的菜单节点
const halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
let halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
if (halfCheckedKeys) {
checkedKeys?.unshift(...halfCheckedKeys);
checkedKeys?.unshift.apply(checkedKeys, halfCheckedKeys);
}
return checkedKeys;
};
@ -496,8 +546,28 @@ const cancelDataScope = () => {
form.value = { ...initForm };
openDataScope.value = false;
};
/** 查询部门下拉树结构 */
const getDeptTree = async () => {
const res = await api.deptTreeSelect({ isShow: '1' });
deptOptions.value = res.data;
enabledDeptOptions.value = filterDisabledDept(res.data);
};
/** 过滤禁用的部门 */
const filterDisabledDept = (deptList: DeptTreeVO[]) => {
return deptList.filter((dept) => {
if (dept.disabled) {
return false;
}
if (dept.children && dept.children.length) {
dept.children = filterDisabledDept(dept.children);
}
return true;
});
};
onMounted(() => {
getDeptTree(); // 初始化部门数据
getList();
});
</script>

View File

@ -440,7 +440,7 @@ import router from '@/router';
import { syrenwulist, syrenwuDetail, addsyrenwu, updatesyrenwu } from '@/api/zhinengxunjian/shiyan/renwu';
import { shiyanlist } from '@/api/zhinengxunjian/shiyan';
import { xunjianUserlist } from '@/api/zhinengxunjian/xunjian/index';
import { addjiedian } from '@/api/zhinengxunjian/jiedian/index';
import { addjiedian, updatejiedian } from '@/api/zhinengxunjian/jiedian/index';
// 引入Element Plus组件提示/空状态/骨架屏/弹窗)
import { ElMessage, ElEmpty, ElSkeleton, ElForm, ElMessageBox, ElDialog } from 'element-plus';
@ -831,16 +831,45 @@ const mapApiToView = (apiData) => {
// 生成试验阶段信息
const getTestStage = () => {
try {
// 优先查找nodes数组中status为2的第一条数据
// 优先查找nodes数组中处于执行中或失败的节点来确定当前试验阶段
if (apiData && apiData.nodes && Array.isArray(apiData.nodes)) {
const firstStatusTwoNode = apiData.nodes.find((node) => {
// 确保node存在且有status属性
// 查找执行中状态的节点
const executingNode = apiData.nodes.find((node) => {
if (!node || node.status === undefined) return false;
// 处理status可能是字符串或数字的情况
return node.status === '2' || node.status === 2;
});
if (firstStatusTwoNode && firstStatusTwoNode.name) {
return firstStatusTwoNode.name;
// 如果有执行中的节点根据code判断阶段
if (executingNode && executingNode.code !== undefined) {
const stepName = executingNode.name || '未命名步骤';
return `${executingNode.code}步(${stepName})`;
}
// 查找失败状态的节点
const failedNode = apiData.nodes.find((node) => {
if (!node || node.status === undefined) return false;
return node.status === '3' || node.status === 3;
});
// 如果有失败的节点根据code判断阶段
if (failedNode && failedNode.code !== undefined) {
const stepName = failedNode.name || '未命名步骤';
return `${failedNode.code}步(${stepName})`;
}
// 查找已完成的节点,确定最后完成的阶段
const completedNodes = apiData.nodes.filter((node) => {
if (!node || node.status === undefined) return false;
return node.status === '4' || node.status === 4;
});
if (completedNodes.length > 0) {
// 按code排序取最大的code
completedNodes.sort((a, b) => Number(b.code) - Number(a.code));
if (completedNodes[0].code !== undefined) {
const stepName = completedNodes[0].name || '未命名步骤';
return `${completedNodes[0].code}步(${stepName})`;
}
}
}
// 如果没有找到符合条件的nodes数据检查是否有明确的试验阶段信息
@ -1014,9 +1043,34 @@ const handleAction = async (task) => {
return node.status === '2' || node.status === 2;
});
if (firstUnfinishedNode) {
// 使用updatejiedian接口更新节点状态构造完整的节点信息数组
const nodeUpdateParams = [
{
...firstUnfinishedNode,
status: '3',
updateTime: new Date().toISOString(),
// 确保包含所有必需字段
createDept: firstUnfinishedNode.createDept || 0,
createBy: firstUnfinishedNode.createBy || 0,
createTime: firstUnfinishedNode.createTime || new Date().toISOString(),
updateBy: firstUnfinishedNode.updateBy || 0,
params: firstUnfinishedNode.params || {
property1: 'string',
property2: 'string'
},
module: firstUnfinishedNode.module || 'string',
orderId: firstUnfinishedNode.orderId || 0,
code: firstUnfinishedNode.code || 0,
name: firstUnfinishedNode.name || 'string',
intendedPurpose: firstUnfinishedNode.intendedPurpose || 'string',
intendedTime: firstUnfinishedNode.intendedTime || new Date().toISOString(),
finishTime: firstUnfinishedNode.finishTime || '',
remark: firstUnfinishedNode.remark || ''
}
];
await updatejiedian(nodeUpdateParams);
// 更新本地数据以反映最新状态
firstUnfinishedNode.status = '3';
// 确保更新到updateParams中
updateParams.nodes = [...taskDetails.nodes]; // 创建新数组以确保引用变更被检测到
}
}
} catch (innerError) {
@ -1050,16 +1104,39 @@ const handleAction = async (task) => {
// 将失败的步骤状态改回2未完成
if (taskDetails.nodes && Array.isArray(taskDetails.nodes)) {
// 创建新数组以确保引用变更被检测到
const updatedNodes = taskDetails.nodes.map((node) => {
if (node.status === '3' || node.status === 3) {
return { ...node, status: '2' };
}
return node;
const failedNodes = taskDetails.nodes.filter((node) => {
return node.status === '3' || node.status === 3;
});
// 更新taskDetails和updateParams
taskDetails.nodes = updatedNodes;
updateParams.nodes = updatedNodes;
// 构造包含所有失败节点的完整信息数组
const nodeUpdateParams = failedNodes.map((failedNode) => ({
...failedNode,
status: '2',
updateTime: new Date().toISOString(),
// 确保包含所有必需字段
createDept: failedNode.createDept || 0,
createBy: failedNode.createBy || 0,
createTime: failedNode.createTime || new Date().toISOString(),
updateBy: failedNode.updateBy || 0,
params: failedNode.params || {
property1: 'string',
property2: 'string'
},
module: failedNode.module || 'string',
orderId: failedNode.orderId || 0,
code: failedNode.code || 0,
name: failedNode.name || 'string',
intendedPurpose: failedNode.intendedPurpose || 'string',
intendedTime: failedNode.intendedTime || new Date().toISOString(),
finishTime: failedNode.finishTime || '',
remark: failedNode.remark || ''
}));
// 一次性调用updatejiedian接口更新所有节点
await updatejiedian(nodeUpdateParams);
// 更新本地数据以反映最新状态
for (const failedNode of failedNodes) {
failedNode.status = '2';
}
}
break;
default: