This commit is contained in:
Teo
2025-05-27 09:16:44 +08:00
parent cbb62f2bf0
commit 933fe4d74a
9 changed files with 1723 additions and 58 deletions

View File

@ -0,0 +1,81 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { ProgressCategoryVO, ProgressCategoryForm, ProgressCategoryQuery } from '@/api/progress/plan/types';
/**
* 查询进度类别列表
* @param query
* @returns {*}
*/
export const listProgressCategory = (query?: ProgressCategoryQuery): AxiosPromise<ProgressCategoryVO[]> => {
return request({
url: '/progress/progressCategory/list',
method: 'get',
params: query
});
};
/**
* 查询进度类别详细
* @param id
*/
export const getProgressCategory = (id: string | number): AxiosPromise<ProgressCategoryVO> => {
return request({
url: '/progress/progressCategory/' + id,
method: 'get'
});
};
/**
* 新增进度类别
* @param data
*/
export const addProgressCategory = (data: ProgressCategoryForm) => {
return request({
url: '/progress/progressCategory',
method: 'post',
data: data
});
};
/**
* 修改进度类别
* @param data
*/
export const updateProgressCategory = (data: ProgressCategoryForm) => {
return request({
url: '/progress/progressCategory',
method: 'put',
data: data
});
};
/**
* 删除进度类别
* @param id
*/
export const delProgressCategory = (id: string | number | Array<string | number>) => {
return request({
url: '/progress/progressCategory/' + id,
method: 'delete'
});
};
/**
* 查询设施-方阵列表
* @param data
*/
export const getProjectSquare = (projectId: string) => {
return request({
url: '/facility/matrix/list',
method: 'get',
params: {projectId}
});
};
export const lastTime=()=>{}
export const workScheduleAddPlan=()=>{}
export const workScheduleList=()=>{}
export const workScheduleDel=()=>{}
export const workScheduleSubmit=()=>{}

View File

@ -0,0 +1,96 @@
export interface ProgressCategoryVO {
/**
* 主键id
*/
id: string | number;
/**
* 父类别id
*/
pid: string | number;
/**
* 类别名称
*/
name: string;
/**
* 计量方式1数量 2百分比
*/
unitType: string;
/**
* 项目id0表示共用
*/
projectId: string | number;
/**
* 子对象
*/
children: ProgressCategoryVO[];
}
export interface ProgressCategoryForm extends BaseEntity {
/**
* 主键id
*/
id?: string | number;
/**
* 父类别id
*/
pid?: string | number;
/**
* 类别名称
*/
name?: string;
/**
* 计量方式1数量 2百分比
*/
unitType?: string;
/**
* 项目id0表示共用
*/
projectId?: string | number;
matrixId?: string | number;
}
export interface ProgressCategoryQuery {
/**
* 父类别id
*/
pid?: string | number;
/**
* 类别名称
*/
name?: string;
/**
* 计量方式1数量 2百分比
*/
unitType?: string;
/**
* 项目id0表示共用
*/
projectId?: string | number;
/**
* 日期范围参数
*/
params?: any;
/**
* 方阵id
*/
matrixId?: string | number;
}

View File

@ -112,6 +112,32 @@ export const addProjectSquare = (data: any) => {
}); });
}; };
/**
* 通过GeoJson新增设施-箱变
* @param data
*/
export const addBoxTransformer = (data: any) => {
return request({
url: '/facility/boxTransformer/geoJson',
method: 'post',
data: data
});
};
/**
* 通过GeoJson新增设施-逆变器
* @param data
*/
export const addInverter = (data: any) => {
return request({
url: '/facility/inverter/geoJson',
method: 'post',
data: data
});
};
/** /**
* 删除项目 * 删除项目
* @param id * @param id

View File

@ -12,16 +12,16 @@
@node-click="isMenuVisible = false" @node-click="isMenuVisible = false"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span @dblclick="handlePosition(data)">{{ data.name }}</span> <span @dblclick="handlePosition(data, node)">{{ data.name }}</span>
</template> </template>
</el-tree-v2> </el-tree-v2>
<div> <div>
<div class="ol-map" id="olMap"></div> <div class="ol-map" id="olMap"></div>
<div class="h15" v-if="!selectLayer.length"></div> <div class="h15 mt-2" v-if="!selectLayer.length"></div>
<div class="m-0 c-white text-3 flex" v-else> <div class="m-0 c-white text-3 flex w-237.5 mt-2 flex-wrap" v-else>
<p <p
v-for="(item, index) in selectLayer" v-for="(item, index) in selectLayer"
class="pl-xl border-rd pr mt-2 p-3 w-111 mr-1 bg-#909399 flex items-center cursor-pointer justify-between" class="pl-xl border-rd pr p-3 w-111 mr-1 bg-#909399 flex items-center cursor-pointer justify-between"
@click="delLayer(index)" @click="delLayer(index)"
> >
{{ item.location.name + '被选中为' + item.option }} {{ item.location.name + '被选中为' + item.option }}
@ -35,6 +35,8 @@
<el-radio value="1" size="large">光伏板</el-radio> <el-radio value="1" size="large">光伏板</el-radio>
<el-radio value="2" size="large">桩点/支架</el-radio> <el-radio value="2" size="large">桩点/支架</el-radio>
<el-radio value="3" size="large">方阵</el-radio> <el-radio value="3" size="large">方阵</el-radio>
<el-radio value="4" size="large">逆变器</el-radio>
<el-radio value="5" size="large">箱变</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</div> </div>
@ -50,6 +52,12 @@
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('方阵')"> <li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('方阵')">
<i class="fa-solid fa-times mr-2"></i>方阵 <i class="fa-solid fa-times mr-2"></i>方阵
</li> </li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('逆变器')">
<i class="fa-solid fa-times mr-2"></i>逆变器
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('箱变')">
<i class="fa-solid fa-times mr-2"></i>箱变
</li>
<li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('名称')"> <li class="px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" @click="handleMenuItemClick('名称')">
<i class="fa-solid fa-times mr-2"></i>名称 <i class="fa-solid fa-times mr-2"></i>名称
</li> </li>
@ -78,8 +86,9 @@ import { Circle, Style, Stroke, Fill, Icon, Text } from 'ol/style'; // OpenLayer
import LineString from 'ol/geom/LineString'; // OpenLayers的线几何类用于表示线状的地理数据 import LineString from 'ol/geom/LineString'; // OpenLayers的线几何类用于表示线状的地理数据
import Polygon from 'ol/geom/Polygon'; // OpenLayers的多边形几何类用于表示面状的地理数据 import Polygon from 'ol/geom/Polygon'; // OpenLayers的多边形几何类用于表示面状的地理数据
import * as turf from '@turf/turf'; import * as turf from '@turf/turf';
import { FeatureCollection } from 'geojson';
import { TreeInstance } from 'element-plus'; import { TreeInstance } from 'element-plus';
import { addProjectFacilities, addProjectPilePoint, addProjectSquare, listDXFProject } from '@/api/project/project'; import { addProjectFacilities, addProjectPilePoint, addProjectSquare, listDXFProject, addInverter, addBoxTransformer } from '@/api/project/project';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const props = defineProps({ const props = defineProps({
@ -121,15 +130,16 @@ const jsonData = computed(() => {
return arr; // treeData.value; return arr; // treeData.value;
}); });
console.log(jsonData); console.log(jsonData);
const handlePosition = (data: any, node) => {
const handlePosition = (data: any) => {
//切换中心点 //切换中心点
centerPosition.value = fromLonLat(turf.center(data).geometry.coordinates); const featureCollection: FeatureCollection = { type: 'FeatureCollection', features: treeData.value[data.index].features } as FeatureCollection;
console.log(turf.center(data));
centerPosition.value = fromLonLat(turf.center(featureCollection).geometry.coordinates);
map.getView().setCenter(centerPosition.value); map.getView().setCenter(centerPosition.value);
}; };
const handleCheckChange = (data: any, bool) => { const handleCheckChange = (data: any, bool) => {
// 处理树形结构的选中变化
let features = treeData.value[data.index].features; let features = treeData.value[data.index].features;
if (isMenuVisible.value) isMenuVisible.value = false; if (isMenuVisible.value) isMenuVisible.value = false;
if (bool) { if (bool) {
@ -308,11 +318,16 @@ const showMenu = (event: MouseEvent, data) => {
// 处理菜单项点击事件的方法 // 处理菜单项点击事件的方法
const handleMenuItemClick = (option: string) => { const handleMenuItemClick = (option: string) => {
isMenuVisible.value = false; isMenuVisible.value = false;
if (selectLayer.value.length == 2) {
ElMessage.warning('最多只能选择两个图层'); if (selectLayer.value.some((item) => item.location.name === contextMenu.value.name)) {
return; return proxy?.$modal.msgError('已选择该图层,请勿重复选择');
}
if (selectLayer.value.some((item) => item.option !== '名称')) {
if (option !== '名称') return proxy?.$modal.msgError('只能选择一个类型');
} }
selectLayer.value.push({ location: contextMenu.value, option }); selectLayer.value.push({ location: contextMenu.value, option });
console.log('selectLayer.value', selectLayer.value);
emit('handleCheckChange', selectLayer.value); emit('handleCheckChange', selectLayer.value);
}; };
@ -350,62 +365,143 @@ const onUnmounted = () => {
window.removeEventListener('click', closeMenuOnClickOutside); window.removeEventListener('click', closeMenuOnClickOutside);
}; };
const addFacilities = async () => { // const addFacilities = async () => {
console.log(layerType.value); // console.log(layerType.value);
if (!layerType.value) { // if (!layerType.value) {
return proxy?.$modal.msgError('请选择图层类型'); // return proxy?.$modal.msgError('请选择图层类型');
} // }
if (!selectLayer.value.length) { // if (!selectLayer.value.length) {
return proxy?.$modal.msgError('请选择需要上传的图层'); // return proxy?.$modal.msgError('请选择需要上传的图层');
// }
// const data = {
// projectId: props.projectId,
// nameGeoJson: null,
// locationGeoJson: null,
// pointGeoJson: null
// };
// if (layerType.value == 1) {
// if (!checkOptions(selectLayer.value, '光伏板')) {
// return proxy?.$modal.msgError('请选择名称和光伏板');
// }
// loading.value = true;
// if (selectLayer.value[0].option == '名称') {
// data.nameGeoJson = treeData.value[selectLayer.value[0].location.index];
// data.locationGeoJson = treeData.value[selectLayer.value[1].location.index];
// } else {
// data.nameGeoJson = treeData.value[selectLayer.value[1].location.index];
// data.locationGeoJson = treeData.value[selectLayer.value[0].location.index];
// }
// await addProjectFacilities(data);
// await proxy?.$modal.msgSuccess('添加成功');
// } else if (layerType.value == 2) {
// if (selectLayer.value.length > 1) return proxy?.$modal.msgError('最多选择一个桩点/支架');
// if (selectLayer.value[0].option != '桩点/支架') return proxy?.$modal.msgError('请选择类型为桩点/支架');
// loading.value = true;
// data.pointGeoJson = treeData.value[selectLayer.value[0].location.index];
// await addProjectPilePoint(data);
// await proxy?.$modal.msgSuccess('添加成功');
// } else if (layerType.value == 3) {
// if (!checkOptions(selectLayer.value, '方阵')) {
// return proxy?.$modal.msgError('请选择名称和方阵');
// }
// loading.value = true;
// if (selectLayer.value[0].option == '名称') {
// data.nameGeoJson = treeData.value[selectLayer.value[0].location.index];
// data.locationGeoJson = treeData.value[selectLayer.value[1].location.index];
// } else {
// data.nameGeoJson = treeData.value[selectLayer.value[1].location.index];
// data.locationGeoJson = treeData.value[selectLayer.value[0].location.index];
// }
// await addProjectSquare(data);
// await proxy?.$modal.msgSuccess('添加成功');
// }
// reset();
// loading.value = false;
// };
type LayerConfig = {
optionB: string;
apiFunc: (data: any) => Promise<any>;
};
const LAYER_CONFIG: Record<number, LayerConfig> = {
1: { optionB: '光伏板', apiFunc: addProjectFacilities },
3: { optionB: '方阵', apiFunc: addProjectSquare },
4: { optionB: '逆变器', apiFunc: addInverter },
5: { optionB: '箱变', apiFunc: addBoxTransformer }
};
const showError = (msg: string) => proxy?.$modal.msgError(msg);
const showSuccess = (msg: string) => proxy?.$modal.msgSuccess(msg);
const getGeoJsonData = (nameOption = '名称', secondOption: string): { nameGeoJson: any[]; locationGeoJson: any | null } | null => {
const nameLayers = selectLayer.value.filter((item) => item.option === nameOption);
const secondLayer = selectLayer.value.find((item) => item.option === secondOption);
if (!nameLayers.length || !secondLayer) {
showError(`请选择${nameOption}${secondOption}`);
return null;
} }
const nameGeoJson = nameLayers.map((item) => treeData.value[item.location.index]);
const locationGeoJson = treeData.value[secondLayer.location.index];
return { nameGeoJson, locationGeoJson };
};
const handleTwoLayerUpload = async (optionB: string, apiFunc: (data: any) => Promise<any>) => {
const geoJson = getGeoJsonData('名称', optionB);
if (!geoJson) return;
const data = {
projectId: props.projectId,
nameGeoJson: geoJson.nameGeoJson,
locationGeoJson: geoJson.locationGeoJson,
pointGeoJson: null
};
loading.value = true;
await apiFunc(data);
await showSuccess('添加成功');
};
const handlePointUpload = async () => {
if (selectLayer.value.length > 1) return showError('最多选择一个桩点/支架');
if (selectLayer.value[0].option !== '桩点/支架') return showError('请选择类型为桩点/支架');
const data = { const data = {
projectId: props.projectId, projectId: props.projectId,
nameGeoJson: null, nameGeoJson: null,
locationGeoJson: null, locationGeoJson: null,
pointGeoJson: null pointGeoJson: treeData.value[selectLayer.value[0].location.index]
}; };
if (layerType.value == 1) {
if (!checkOptions(selectLayer.value, '光伏板')) {
return proxy?.$modal.msgError('请选择名称和光伏板');
}
loading.value = true; loading.value = true;
if (selectLayer.value[0].option == '名称') {
data.nameGeoJson = treeData.value[selectLayer.value[0].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[1].location.index];
} else {
data.nameGeoJson = treeData.value[selectLayer.value[1].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[0].location.index];
}
await addProjectFacilities(data);
await proxy?.$modal.msgSuccess('添加成功');
} else if (layerType.value == 2) {
if (selectLayer.value.length > 1) return proxy?.$modal.msgError('最多选择一个桩点/支架');
if (selectLayer.value[0].option != '桩点/支架') return proxy?.$modal.msgError('请选择类型为桩点/支架');
loading.value = true;
data.pointGeoJson = treeData.value[selectLayer.value[0].location.index];
await addProjectPilePoint(data); await addProjectPilePoint(data);
await proxy?.$modal.msgSuccess('添加成功'); await showSuccess('添加成功');
} else if (layerType.value == 3) { };
if (!checkOptions(selectLayer.value, '方阵')) {
return proxy?.$modal.msgError('请选择名称和方阵');
}
loading.value = true;
if (selectLayer.value[0].option == '名称') {
data.nameGeoJson = treeData.value[selectLayer.value[0].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[1].location.index];
} else {
data.nameGeoJson = treeData.value[selectLayer.value[1].location.index];
data.locationGeoJson = treeData.value[selectLayer.value[0].location.index];
}
await addProjectSquare(data); const addFacilities = async () => {
await proxy?.$modal.msgSuccess('添加成功'); if (!layerType.value) return showError('请选择图层类型');
if (!selectLayer.value.length) return showError('请选择需要上传的图层');
const config = LAYER_CONFIG[layerType.value];
try {
if (layerType.value == 2) {
await handlePointUpload();
} else if (config) {
await handleTwoLayerUpload(config.optionB, config.apiFunc);
} else {
showError('不支持的图层类型');
} }
} finally {
reset(); reset();
loading.value = false; loading.value = false;
}
}; };
const reset = () => { const reset = () => {
@ -446,7 +542,20 @@ watch(
onMounted(() => { onMounted(() => {
// 地图初始化 // 地图初始化
initOLMap(); initOLMap();
console.log(props.designId); // creatPoint(
// [
// [
// [107.13205125908726, 23.806785824010216],
// [107.13218187963494, 23.806867960389773],
// [107.13215698891558, 23.806902336258318],
// [107.13202636835067, 23.8068201998575],
// [107.13205125908726, 23.806785824010216]
// ]
// ],
// 'Polygon',
// '1',
// '测试方阵'
// );
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -0,0 +1,266 @@
<template>
<div class="daily_paper">
<el-dialog v-model="isShowDialog" @close="onCancel" width="1000px" :close-on-click-modal="false" :destroy-on-close="true">
<template #header>
<div v-drag="['.daily_paper .el-dialog', '.daily_paper .el-dialog__header']" style="font-size: 18px">{{ infoDetail.name }} 日报填写</div>
</template>
<div class="box">
<div class="box-left">
<el-table
v-loading="loading"
:data="tableData"
height="64vh"
:cell-style="{ textAlign: 'center' }"
:header-cell-style="{ textAlign: 'center' }"
border
:row-key="tableKey"
:expand-row-keys="expandRowKeys"
@expand-change="clickOpen"
>
<el-table-column type="expand">
<template #default="props">
<div style="margin-left: 45px" m="4">
<el-table
:border="true"
:data="props.row.detail"
:header-cell-style="{ 'text-align': 'center' }"
:cell-style="{ 'text-align': 'center' }"
highlight-current-row
>
<el-table-column label="序号" type="index" width="50px" />
<el-table-column label="计划日期" prop="date"> </el-table-column>
<el-table-column label="数量" prop="planNum" width="60" />
<el-table-column label="完成量" prop="finishedNum" width="60" />
<el-table-column label="操作" class-name="small-padding" width="80px" fixed="right">
<template #default="scope">
<el-button type="primary" link @click="handleDayAdd(scope.row, props.row)"
><el-icon><ele-Plus /></el-icon>日报</el-button
>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="序号" type="index" :index="indexMethod" width="50px" />
<el-table-column label="计划数量" prop="planNum" min-width="100px" />
<el-table-column label="开始时间" min-width="100px">
<template #default="scope">
<span>{{ scope.row.startAt.split(' ')[0] }}</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getWorkList"
/>
</div>
<div class="box_right">
<div class="box_form" v-if="formDetail.submitTime">
<span>{{ dayDetail.date }}</span>
<el-form size="large" ref="formRef" :model="formDetail" :rules="rules" label-width="110px">
<el-form-item label="实际完成数量" prop="finishedNum">
<el-row>
<el-col :span="18">
<el-input type="number" :min="0" v-model="formDetail.finishedNum" placeholder="请输入实际完成数量"> </el-input
></el-col>
</el-row> </el-form-item
></el-form>
<div class="footer_box">
<el-button @click="onAddSubmit" type="primary" size="large"> </el-button>
</div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="onCancel" size="large"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, toRefs, getCurrentInstance } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { workScheduleList, workScheduleDel, workScheduleSubmit } from '@/api/progress/plan';
const { proxy } = getCurrentInstance() as any;
const menuRef = ref();
const formRef = ref();
const expandRowKeys = ref([]);
const loading = ref(false);
const isShowDialog = ref(false);
const tableData = ref([]);
const total = ref(0);
const oldLength = ref(0);
const queryParams = reactive({
pageSize: 10,
pageNum: 1,
workId: ''
});
const infoDetail = reactive({
name: ''
});
const formDetail = reactive({
workId: '',
id: '',
submitTime: '',
finishedNum: 0
});
const dayDetail = reactive({
date: '',
finishedNum: 0
});
const rules = {
planNum: [{ required: true, message: '实际完成数量不能为空', trigger: 'blur' }]
};
const openDialog = (row: any) => {
queryParams.workId = row.work_id;
formDetail.workId = row.work_id;
infoDetail.name = row.name;
isShowDialog.value = true;
getWorkList(true);
};
const getWorkList = (bool = false) => {
loading.value = true;
workScheduleList(queryParams).then((res: any) => {
if (res.code === 0) {
tableData.value = res.data.list.map((item: any, index: number) => ({ ...item, index: index + 1 }));
if (bool) oldLength.value = res.data.list.length;
total.value = res.data.total;
}
loading.value = false;
});
};
const closeDialog = () => {
emit('getProgressList');
isShowDialog.value = false;
};
const onCancel = () => {
closeDialog();
};
const handleDelete = (row: any) => {
ElMessageBox.confirm('你确定要删除所选计划?', '温馨提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
workScheduleDel({ ids: [row.id] }).then((res: any) => {
if (res.code === 0) {
ElMessage.success('删除成功');
getWorkList();
} else {
ElMessage.error(res.message);
}
});
})
.catch(() => {});
};
const indexMethod = (index: number) => {
const currentPage = queryParams.pageNum - 1;
return index + 1 + (currentPage > 0 ? currentPage * queryParams.pageSize : 0);
};
const handleDayAdd = (row: any, obj: any) => {
formDetail.id = obj.id;
formDetail.submitTime = row.date;
formDetail.finishedNum = row.finishedNum;
Object.assign(dayDetail, row);
};
const onAddSubmit = () => {
dayDetail.finishedNum = Number(dayDetail.finishedNum);
workScheduleSubmit(formDetail).then((res: any) => {
if (res.code === 0) {
ElMessage.success('添加成功');
dayDetail.finishedNum = formDetail.finishedNum;
formDetail.submitTime = '';
formDetail.finishedNum = 0;
} else {
ElMessage.error(res.message);
}
});
};
const tableKey = (row: any) => row.id;
const clickOpen = (row: any) => {
const idx = expandRowKeys.value.indexOf(row.id);
if (idx !== -1) expandRowKeys.value.splice(idx, 1);
else expandRowKeys.value.push(row.id);
expandRowKeys.value = [...new Set(expandRowKeys.value)];
};
const emit = defineEmits(['getProgressList']);
</script>
<style lang="scss" scoped>
.daily_paper {
width: 100%;
.box {
display: flex;
width: 100%;
justify-content: space-between;
padding: 0 10px;
.box-left {
width: 46%;
float: left;
}
.box_right {
width: 48%;
float: left;
border: 1px solid #ebeef5;
height: 64vh;
margin-left: 20px;
display: flex;
align-items: center;
justify-content: center;
.box_form {
width: 80%;
background-color: aliceblue;
padding: 10px;
> span {
display: block;
width: 100%;
font-size: 20px;
display: grid;
place-items: center;
margin-bottom: 20px;
}
.footer_box {
margin: 20px auto 0;
width: 100%;
display: grid;
place-items: center;
}
}
}
}
.pagination-container {
padding: 10px 10px !important;
}
.el-drawer__title {
font-size: 18px;
}
}
</style>

View File

@ -0,0 +1,292 @@
<template>
<div class="new-bar-chart-add">
<el-dialog v-model="isShowDialog" width="800px" :close-on-click-modal="false" :destroy-on-close="true">
<template #header>
<div v-drag="['.new-bar-chart-add .el-dialog', '.new-bar-chart-add .el-dialog__header']" style="font-size: 18px">
{{ infoDetail.name }} 计划创建
</div>
</template>
<el-row>
<el-col :span="14">
<el-form size="large" ref="formRef" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="开始时间" prop="start_at">
<el-date-picker
v-model="formData.start_at"
placeholder="选择开始时间"
type="date"
clearable
value-format="YYYY-MM-DD"
size="large"
:disabled-date="pickerOptionsStart"
:disabled="remainingNumAt.remainingNum === 0"
/>
</el-form-item>
<el-form-item label="结束时间" prop="end_at">
<el-date-picker
v-model="formData.end_at"
placeholder="选择结束时间"
type="date"
clearable
value-format="YYYY-MM-DD"
size="large"
:disabled-date="pickerOptionsEnd"
:disabled="remainingNumAt.remainingNum === 0"
/>
</el-form-item>
<el-form-item label="计划数量" prop="planNum">
<el-row>
<el-col :span="18">
<el-input
v-model="formData.planNum"
type="number"
:min="0"
:max="remainingNumAt.remainingNum"
:disabled="remainingNumAt.remainingNum === 0"
placeholder="请输入计划数量"
>
<template #append>
<span style="font-size: 12px">最大值{{ remainingNumAt.remainingNum }}</span>
</template>
</el-input>
</el-col>
<el-button type="primary" @click="onAverage" :disabled="!formData.planNum || tableData.length === 0" style="margin-left: 10px"
>均值</el-button
>
</el-row>
</el-form-item>
</el-form>
</el-col>
<el-col :span="10">
<el-table
style="margin-left: 10px; margin-bottom: 10px"
height="250"
:data="paginatedData"
border
empty-text="暂无计划"
:cell-style="{ textAlign: 'center' }"
:header-cell-style="{ textAlign: 'center' }"
>
<el-table-column type="index" :index="indexMethod" label="序号" width="60" />
<el-table-column prop="date" label="日期" width="130" />
<el-table-column prop="planNum" label="数量">
<template #default="scope">
<el-input-number v-model="scope.row.planNum" :value-on-clear="0" :min="0" :max="scope.row.max" @change="onChangeSum" size="small" />
</template>
</el-table-column>
</el-table>
<el-pagination
style="margin-left: 10px"
background
layout="total, prev, pager, next"
:total="tableData.length"
:page-size="pageSize"
:current-page="currentPage"
@current-change="(val) => (currentPage = val)"
/>
</el-col>
</el-row>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" size="large" @click="onSubmit">确 定</el-button>
<el-button size="large" @click="closeDialog">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, computed } from 'vue';
import { ElMessage, FormInstance } from 'element-plus';
import { workScheduleAddPlan, lastTime } from '@/api/progress/plan';
// Form refs
const formRef = ref<FormInstance | null>(null);
// State
const isShowDialog = ref(false);
const currentPage = ref(1);
const pageSize = 10;
const formData = reactive({
start_at: '',
end_at: '',
planNum: 0
});
const rules = {
start_at: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
end_at: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
planNum: [{ required: true, message: '计划数量不能为空', trigger: 'blur' }]
};
const infoDetail = reactive({
name: '',
work_id: '',
is_delay: 0
});
const remainingNumAt = reactive({
endAt: '',
remainingNum: 100
});
const tableData = ref<{ date: string; planNum: number; max: number }[]>([]);
// Computed paginated data
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize;
return tableData.value.slice(start, start + pageSize);
});
// Watch date changes
watch(
() => [formData.start_at, formData.end_at],
([start, end]) => {
if (start && end) {
const dates = generateDateRange(start, end);
tableData.value = dates.map((date) => ({
date,
planNum: 0,
max: remainingNumAt.remainingNum
}));
} else {
tableData.value = [];
}
}
);
// Date helpers
const generateDateRange = (start: string, end: string): string[] => {
const result: string[] = [];
const startDate = new Date(start);
const endDate = new Date(end);
while (startDate <= endDate) {
result.push(startDate.toISOString().split('T')[0]);
startDate.setDate(startDate.getDate() + 1);
}
return result;
};
// Picker rules
const pickerOptionsStart = (date: Date) => {
if (formData.end_at) return date.getTime() > new Date(formData.end_at).getTime();
if (remainingNumAt.endAt) return date.getTime() < new Date(remainingNumAt.endAt).getTime();
return false;
};
const pickerOptionsEnd = (date: Date) => {
if (formData.start_at) return date.getTime() < new Date(formData.start_at).getTime();
if (remainingNumAt.endAt) return date.getTime() < new Date(remainingNumAt.endAt).getTime();
return false;
};
// Average calculation
const onAverage = () => {
const total = formData.planNum;
const len = tableData.value.length;
const avg = Math.floor(total / len);
let remainder = total % len;
tableData.value.forEach((row) => {
row.planNum = avg + (remainder > 0 ? 1 : 0);
remainder--;
});
};
// Total sum validation
const onChangeSum = () => {
let total = tableData.value.reduce((sum, item) => sum + item.planNum, 0);
if (total > remainingNumAt.remainingNum) {
tableData.value.forEach((item) => {
item.max = item.planNum;
});
}
formData.planNum = total;
};
// Index method
const indexMethod = (index: number) => {
return index + 1 + (currentPage.value - 1) * pageSize;
};
// Submit handler
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate((valid) => {
if (!valid) return;
const payload = {
workId: infoDetail.work_id,
planNum: formData.planNum,
scheduledTime: tableData.value,
is_delay: infoDetail.is_delay === 1 ? 1 : undefined
};
workScheduleAddPlan(payload).then((res: any) => {
if (res.code === 0) {
ElMessage.success('添加成功');
closeDialog();
} else {
ElMessage.error(res.message);
}
});
});
};
// Dialog controls
const openDialog = (row: typeof infoDetail) => {
Object.assign(infoDetail, row);
isShowDialog.value = true;
resetForm();
fetchLastTime(row);
};
const closeDialog = () => {
isShowDialog.value = false;
};
const resetForm = () => {
formData.start_at = '';
formData.end_at = '';
formData.planNum = 0;
tableData.value = [];
};
const fetchLastTime = (row: typeof infoDetail) => {
lastTime({ workId: row.work_id, is_delay: row.is_delay }).then((res: any) => {
if (res.code === 0) {
Object.assign(remainingNumAt, res.data);
}
});
};
// Export function if needed externally
defineExpose({ openDialog });
</script>
<style scoped lang="scss">
.new-bar-chart-add {
.el-form-item--large .el-form-item__content {
line-height: 32px !important;
}
.el-input-number--small {
width: 90px;
}
.el-input-group__append {
padding: 0 10px;
}
.el-form {
background: aliceblue;
padding: 60px 10px;
}
.el-row {
align-items: center;
width: 100%;
}
}
</style>

View File

@ -0,0 +1,383 @@
<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="pid" label-width="100">
<!-- <el-input v-model="queryParams.pid" placeholder="请选择" clearable /> -->
<el-cascader
:options="matrixOptions"
placeholder="请选择"
@change="handleChange"
:props="{ value: 'id', label: 'matrixName' }"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Download" @click="handleQuery">导出周报</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="['progress:progressCategory:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template> -->
<el-table
ref="progressCategoryTableRef"
v-loading="loading"
:data="progressCategoryList"
row-key="id"
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
border
>
<el-table-column label="" width="50" type="expand">
<template #default="scope">
<el-card class="pl-25" shadow="hover">
<el-table :data="scope.row.children" border>
<el-table-column label="名称" align="center" prop="name" width="150" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="{ row }">
<dict-tag :options="progress_status" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="是否延期" align="center" prop="isDelay" width="100">
<template #default="{ row }">
<el-tag :type="row.isDelay == '1' ? 'danger' : 'primary'">{{ row.isDelay == '1' ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="计量方式" align="center" prop="unitType" width="100">
<template #default="{ row }">
<dict-tag :options="progress_unit_type" :value="row.unitType" />
</template>
</el-table-column>
<el-table-column label="总量" align="center" prop="total" width="100" />
<el-table-column label="总进度" align="center" prop="projectId">
<template #default="{ row }">
<el-progress :text-inside="true" :stroke-width="20" :percentage="row.total" status="success" />
</template>
</el-table-column>
<el-table-column label="计划总量" align="center" prop="planTotal" width="100" />
<el-table-column label="计划中" align="center" prop="projectId">
<template #default="{ row }">
<el-progress :text-inside="true" :stroke-width="20" :percentage="row.planTotal" />
</template>
</el-table-column>
<el-table-column label="对比" align="center" prop="unitType" width="100" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template #default="scope">
<el-button
type="warning"
icon="Download"
link
size="small"
@click="openDialog(scope.row, 'importDataStatus', '上传数据')"
v-hasPermi="['progress:progressCategory:add']"
>
导入数据
</el-button>
<el-button
type="warning"
icon="Download"
link
size="small"
@click="openDialog(scope.row, 'importTableStatus', '上传表格')"
v-hasPermi="['progress:progressCategory:add']"
>
导入表格
</el-button>
<el-button
type="success"
icon="Plus"
link
size="small"
@click="openDialog(scope.row, 'planStatus', scope.row.name + '计划创建')"
v-hasPermi="['progress:progressCategory:add']"
>
计划
</el-button>
<el-button
type="primary"
icon="Plus"
link
size="small"
@click="openDialog(scope.row, 'dailyStatus', scope.row.name + '日报填写')"
v-hasPermi="['progress:progressCategory:add']"
>
日报
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
</el-table-column>
<el-table-column label="名称" align="center" prop="name" width="150" />
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="{ row }">
<dict-tag :options="progress_status" :value="row.status" />
</template>
</el-table-column>
<el-table-column label="总进度" align="center" prop="projectId">
<template #default="{ row }">
<el-progress :text-inside="true" :stroke-width="20" :percentage="row.total" status="success" />
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 导入数据对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.importDataStatus" width="500px" append-to-body>
<file-upload class="pl-20 pt" v-model="dialog.file" :limit="20" :file-size="50" :file-type="['shp', 'shx', 'dbf']" />
<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="dialog.title" v-model="dialog.importTableStatus" width="800px" append-to-body>
<file-upload class="pl-20 pt" v-model="dialog.file" :limit="20" :file-size="50" :file-type="['xls', 'xlsx']" />
<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>
<!-- 创建计划对话框 -->
<CreatePlan ref="planRef"></CreatePlan>
<CreateDaily ref="dailyRef"></CreateDaily>
</div>
</template>
<script setup name="ProgressCategory" lang="ts">
import {
listProgressCategory,
getProgressCategory,
delProgressCategory,
addProgressCategory,
updateProgressCategory,
getProjectSquare
} from '@/api/progress/plan/index';
import { ProgressCategoryVO, ProgressCategoryQuery, ProgressCategoryForm } from '@/api/progress/plan/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { progress_unit_type, progress_status } = toRefs<any>(proxy?.useDict('progress_unit_type', 'progress_status'));
import { useUserStoreHook } from '@/store/modules/user';
import CreatePlan from './component/createPlan.vue';
import CreateDaily from './component/createDaily.vue';
type ProgressCategoryOption = {
id: number;
name: string;
children?: ProgressCategoryOption[];
};
// 获取用户 store
const userStore = useUserStoreHook();
// 从 store 中获取项目列表和当前选中的项目
const currentProject = computed(() => userStore.selectedProject);
const progressCategoryList = ref<ProgressCategoryVO[]>([]);
const progressCategoryOptions = ref<ProgressCategoryOption[]>([]);
const buttonLoading = ref(false);
const showSearch = ref(true);
const isExpandAll = ref(true);
const loading = ref(false);
const planRef = ref<InstanceType<typeof CreatePlan>>();
const dailyRef = ref<InstanceType<typeof CreateDaily>>();
const queryFormRef = ref<ElFormInstance>();
const progressCategoryFormRef = ref<ElFormInstance>();
const progressCategoryTableRef = ref<ElTableInstance>();
const matrixOptions = ref([
{
id: currentProject.value.id,
matrixName: currentProject.value.name,
children: []
}
]);
const dialog = reactive<any>({
dailyStatus: false,
planStatus: false,
importDataStatus: false,
importTableStatus: false,
title: '',
file: ''
});
const initFormData: ProgressCategoryForm = {
id: undefined,
pid: undefined,
name: undefined,
matrixId: undefined,
unitType: undefined,
projectId: currentProject.value.id
};
const data = reactive<PageData<ProgressCategoryForm, ProgressCategoryQuery>>({
form: { ...initFormData },
queryParams: {
pid: undefined,
name: undefined,
unitType: undefined,
projectId: currentProject.value.id,
matrixId: undefined,
params: {}
},
rules: {
id: [{ required: true, message: '主键id不能为空', trigger: 'blur' }],
pid: [{ required: true, message: '父类别id不能为空', trigger: 'blur' }],
name: [{ required: true, message: '类别名称不能为空', trigger: 'blur' }],
unitType: [{ required: true, message: '计量方式不能为空', trigger: 'change' }],
projectId: [{ required: true, message: '项目id不能为空', trigger: 'blur' }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询进度类别列表 */
const getList = async () => {
if (!queryParams.value.matrixId) {
const res = await getProjectSquare(currentProject.value.id);
console.log('🚀 ~ getList ~ res:', res);
matrixOptions.value[0].children = res.rows;
queryParams.value.matrixId = res.rows[0].id;
}
loading.value = true;
const res = await listProgressCategory(queryParams.value);
const data = proxy?.handleTree<ProgressCategoryVO>(res.data, 'id', 'pid');
if (data) {
progressCategoryList.value = data;
loading.value = false;
}
};
/** 查询进度类别下拉树结构 */
const getTreeselect = async () => {
const res = await listProgressCategory();
progressCategoryOptions.value = [];
const data: ProgressCategoryOption = { id: 0, name: '顶级节点', children: [] };
data.children = proxy?.handleTree<ProgressCategoryOption>(res.data, 'id', 'pid');
progressCategoryOptions.value.push(data);
};
// 取消按钮
const cancel = () => {
reset();
dialog.visible = false;
};
// 表单重置
const reset = () => {
form.value = { ...initFormData };
progressCategoryFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
getList();
};
/** 级联选择器改变事件 */
const handleChange = (value: number[]) => {
queryParams.value.matrixId = value[1];
getList();
};
/** 导入按钮操作 */
const openDialog = (row: ProgressCategoryVO, status: string, title: string) => {
reset();
// getTreeselect();
if (row != null && row.id) {
form.value.pid = row.id;
} else {
form.value.pid = 0;
}
dialog[status] = true;
dialog.title = title;
};
/** 展开/折叠操作 */
const handleToggleExpandAll = () => {
isExpandAll.value = !isExpandAll.value;
toggleExpandAll(progressCategoryList.value, isExpandAll.value);
};
/** 展开/折叠操作 */
const toggleExpandAll = (data: ProgressCategoryVO[], status: boolean) => {
data.forEach((item) => {
progressCategoryTableRef.value?.toggleRowExpansion(item, status);
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
});
};
/** 修改按钮操作 */
const handleUpdate = async (row: ProgressCategoryVO) => {
reset();
await getTreeselect();
if (row != null) {
form.value.pid = row.pid;
}
const res = await getProgressCategory(row.id);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = '修改进度类别';
};
/** 提交按钮 */
const submitForm = () => {
progressCategoryFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateProgressCategory(form.value).finally(() => (buttonLoading.value = false));
} else {
await addProgressCategory(form.value).finally(() => (buttonLoading.value = false));
}
proxy?.$modal.msgSuccess('操作成功');
dialog.visible = false;
getList();
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row: ProgressCategoryVO) => {
await proxy?.$modal.confirm('是否确认删除进度类别编号为"' + row.id + '"的数据项?');
loading.value = true;
await delProgressCategory(row.id).finally(() => (loading.value = false));
await getList();
proxy?.$modal.msgSuccess('删除成功');
};
onMounted(() => {
getList();
});
//监听项目id刷新数据
const listeningProject = watch(
() => currentProject.value.id,
(nid, oid) => {
queryParams.value.projectId = nid;
form.value.projectId = nid;
getList();
}
);
onUnmounted(() => {
listeningProject();
});
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long